@@ -264,7 +264,7 @@ export function FactoidsResults({ result, discoveredSources, onDiscuss, onSave,
{/* Factoids */}
diff --git a/src/components/features/generate/GenerateResults.tsx b/src/components/features/generate/GenerateResults.tsx
index 7a03fa408..42ae220c3 100644
--- a/src/components/features/generate/GenerateResults.tsx
+++ b/src/components/features/generate/GenerateResults.tsx
@@ -8,7 +8,7 @@ import { Separator } from '@/components/ui/separator';
import { Markdown } from '@/components/ui/markdown';
import type { GenerateOutput, Contention } from '@/lib/ai/schemas/generate';
import { toast } from 'sonner';
-import { formatEuropeanDateTime } from '@/lib/date-format';
+import { formatDecimal, formatEuropeanDateTime } from '@/lib/date-format';
import { ExportToolbar } from '@/components/features/ExportToolbar';
import { ResultHeader } from '@/components/features/tools/ResultHeader';
import { CopyResultButton, SaveResultAction } from '@/components/features/tools/result-actions';
@@ -83,7 +83,7 @@ function ArgumentCard({ contention, index, unverified }: { contention: Contentio
function formatDuration(ms: number | undefined, t: (key: string, options?: Record
) => string) {
if (!ms || ms < 0) return null;
if (ms < 1000) return t('generateResults.durationMs', { value: ms });
- const value = (ms / 1000).toFixed(ms < 10000 ? 1 : 0);
+ const value = formatDecimal(ms / 1000, ms < 10000 ? 1 : 0);
return t('generateResults.durationS', { value });
}
diff --git a/src/lib/date-format.ts b/src/lib/date-format.ts
index 7148d5b7a..ed6bc7512 100644
--- a/src/lib/date-format.ts
+++ b/src/lib/date-format.ts
@@ -10,17 +10,23 @@ interface LocaleFormatters {
number: Intl.NumberFormat;
}
+// Spanish and Hungarian leave four-digit numbers ungrouped by default; group
+// them too so a limit reads "1.200" or "1 200", never "1200". The ES2021 lib
+// types `useGrouping` as a boolean only.
+const ALWAYS_GROUP = { useGrouping: 'always' } as unknown as Intl.NumberFormatOptions;
+
function createFormatters(language: AppLanguage): LocaleFormatters {
const locale = toIntlLocale(language);
return {
+ // A month name keeps the date unambiguous: "23 Oct 2026", not "23/10/2026".
date: new Intl.DateTimeFormat(locale, {
- day: '2-digit',
- month: '2-digit',
+ day: 'numeric',
+ month: 'short',
year: 'numeric',
}),
dateTime: new Intl.DateTimeFormat(locale, {
- day: '2-digit',
- month: '2-digit',
+ day: 'numeric',
+ month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
@@ -31,7 +37,7 @@ function createFormatters(language: AppLanguage): LocaleFormatters {
minute: '2-digit',
hour12: false,
}),
- number: new Intl.NumberFormat(locale),
+ number: new Intl.NumberFormat(locale, ALWAYS_GROUP),
};
}
@@ -47,6 +53,10 @@ function activeLanguage(): AppLanguage {
return isAppLanguage(i18n.resolvedLanguage) ? i18n.resolvedLanguage : 'en';
}
+function notAvailable(language: AppLanguage): string {
+ return i18n.t('common.notAvailable', { lng: language });
+}
+
function toValidDate(value: Date | number | string): Date | null {
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
// Date-only strings (e.g. "2026-05-20") are parsed as UTC midnight by spec.
@@ -60,20 +70,20 @@ function toValidDate(value: Date | number | string): Date | null {
export function formatEuropeanDate(
value: Date | number | string,
- fallback = 'N/A',
+ fallback?: string,
language: AppLanguage = activeLanguage(),
): string {
const date = toValidDate(value);
- return date ? FORMATTERS[language].date.format(date) : fallback;
+ return date ? FORMATTERS[language].date.format(date) : fallback ?? notAvailable(language);
}
export function formatEuropeanDateTime(
value: Date | number | string,
- fallback = 'N/A',
+ fallback?: string,
language: AppLanguage = activeLanguage(),
): string {
const date = toValidDate(value);
- return date ? FORMATTERS[language].dateTime.format(date) : fallback;
+ return date ? FORMATTERS[language].dateTime.format(date) : fallback ?? notAvailable(language);
}
export function formatEuropeanTime(
@@ -89,11 +99,16 @@ export function formatNumber(value: number, language: AppLanguage = activeLangua
return FORMATTERS[language].number.format(value);
}
+/** A number with exactly `digits` decimals in the language's notation: "1.5" in English, "1,5" in Spanish and Hungarian. */
+export function formatDecimal(value: number, digits: number, language: AppLanguage = activeLanguage()): string {
+ return new Intl.NumberFormat(toIntlLocale(language), { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
+}
+
export function formatRelativeDate(
value: Date | number | string,
language: AppLanguage = activeLanguage(),
- fallback = 'N/A',
+ fallback?: string,
): string {
const date = toValidDate(value);
- return date ? formatDistanceToNow(date, { addSuffix: true, locale: DATE_FNS_LOCALES[language] }) : fallback;
+ return date ? formatDistanceToNow(date, { addSuffix: true, locale: DATE_FNS_LOCALES[language] }) : fallback ?? notAvailable(language);
}
diff --git a/src/lib/pdf-export-utils.ts b/src/lib/pdf-export-utils.ts
index 795107709..f1c0c10e8 100644
--- a/src/lib/pdf-export-utils.ts
+++ b/src/lib/pdf-export-utils.ts
@@ -227,7 +227,7 @@ async function createPDFDocument(): Promise {
doc.setFont(PDF_FONT, 'normal');
doc.setTextColor(...COLORS.secondary);
doc.text(
- `${i18n.t('export.generatedBy')} • ${formatEuropeanDate(new Date(), 'N/A', language)} • ${i18n.t('export.page')} ${i} ${i18n.t('export.of')} ${pageCount}`,
+ `${i18n.t('export.generatedBy')} • ${formatEuropeanDate(new Date(), undefined, language)} • ${i18n.t('export.page')} ${i} ${i18n.t('export.of')} ${pageCount}`,
pageWidth / 2,
pageHeight - 10,
{ align: 'center' }
@@ -629,9 +629,9 @@ export async function exportFactoidsToPdf(result: FactoidsOutput): Promise {
const pdf2 = pdf; // Access outer scope
pdf2.addParagraph(`${i + 1}. ${f.shortClaim}`);
- pdf2.addParagraph(` ${i18n.t('export.context')}: ${f.context || 'N/A'}`, 5);
- pdf2.addParagraph(` ${i18n.t('export.limitations')}: ${f.limitations || 'N/A'}`, 5);
- pdf2.addParagraph(` ${i18n.t('export.howToUse')}: ${f.howToUse || 'N/A'}`, 5);
+ pdf2.addParagraph(` ${i18n.t('export.context')}: ${f.context || i18n.t('common.notAvailable')}`, 5);
+ pdf2.addParagraph(` ${i18n.t('export.limitations')}: ${f.limitations || i18n.t('common.notAvailable')}`, 5);
+ pdf2.addParagraph(` ${i18n.t('export.howToUse')}: ${f.howToUse || i18n.t('common.notAvailable')}`, 5);
if (f.citation?.url) {
pdf2.addParagraph(` ${i18n.t('export.source')}: ${f.citation.url}`, 5);
}
diff --git a/src/locales/en.json b/src/locales/en.json
index ef4c60129..b5e7ca6ad 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -247,7 +247,8 @@
"deleted": "Deleted",
"discuss": "Discuss",
"retry": "Retry",
- "undo": "Undo"
+ "undo": "Undo",
+ "notAvailable": "N/A"
},
"aiClient": {
"inputShortened": "Input was shortened",
@@ -1229,7 +1230,13 @@
"exportError": "An unexpected error occurred during export",
"weaknessesCount_one": "{{count}} issue found, sorted by severity",
"weaknessesCount_other": "{{count}} issues found, sorted by severity",
- "resultTitle": "Critique"
+ "resultTitle": "Critique",
+ "criticalCount_one": "{{count}} Critical",
+ "criticalCount_other": "{{count}} Critical",
+ "majorCount_one": "{{count}} Major",
+ "majorCount_other": "{{count}} Major",
+ "minorCount_one": "{{count}} Minor",
+ "minorCount_other": "{{count}} Minor"
},
"brainstormForm": {
"motion": "Motion / Topic",
@@ -1297,7 +1304,6 @@
"unverified": "Unverified",
"pending": "Pending",
"importantNotes": "Important Notes",
- "found": "Found",
"factoids": "Factoids",
"factoidsDesc": "Evidence points with citations",
"findMore": "Find More Evidence",
@@ -1322,7 +1328,11 @@
"exportError": "An unexpected error occurred during export",
"noVerified": "No verified matching sources were found. Try a broader source type or timeframe.",
"useSuggestion": "Search this",
- "resultTitle": "Evidence"
+ "resultTitle": "Evidence",
+ "foundCount_one": "Found {{count}} factoid",
+ "foundCount_other": "Found {{count}} factoids",
+ "verifiedCount_one": "{{count}} Verified",
+ "verifiedCount_other": "{{count}} Verified"
},
"askChat": {
"title": "Ask Your Coach",
@@ -1480,7 +1490,7 @@
"displayLabel": "Timer display",
"speechProgress": "{{speech}} progress",
"selectFormat": "Select Format",
- "formatStandard": "Standard (4-6-6-3)",
+ "formatStandard": "School 4-6-6-3",
"formatLD": "Lincoln-Douglas",
"formatPF": "Public Forum",
"formatPolicy": "Policy",
@@ -1669,7 +1679,6 @@
"deleteCaseLabel": "Delete {{title}}",
"load": "Load",
"resume": "Resume",
- "messages": "messages",
"caseDeleted": "Saved case deleted.",
"deleteCaseDesc": "This removes the saved case from your history.",
"deleteCaseTitle": "Delete saved case?",
@@ -1707,7 +1716,6 @@
"unsupportedFile": "Unsupported file",
"importPreview": "Import preview",
"saveImported": "Save imported source",
- "words": "words",
"folder": "Folder",
"generalFolder": "General",
"textSource": "Text",
@@ -1748,7 +1756,6 @@
"titlePlaceholder": "Source title...",
"originPlaceholder": "URL or reference...",
"contentPlaceholder": "Paste source content...",
- "sourcesCount": "sources",
"emptyTitle": "No Sources Yet",
"emptyDesc": "Add sources to ground AI responses in real evidence.",
"manualEntry": "Manual Entry",
@@ -1787,7 +1794,7 @@
"restoreFailed": "Could not restore the source",
"deleteFailed": "Could not delete the source.",
"readingFile": "Reading file…",
- "summary": "{{total}} sources · {{active}} active · {{folders}} folders",
+ "summary": "{{sources}} · {{active}} · {{folders}}",
"mergeFolderTitle": "Merge into {{name}}?",
"mergeFolders": "Merge folders",
"mergeFailed": "Could not merge the folders. Try again.",
@@ -1800,7 +1807,15 @@
"sourceSaved": "Saved {{title}} to the Library",
"duplicateUrlDesc": "This page is already in this debate's Library.",
"loadingSources": "Loading sources…",
- "folderUpdateFailed": "Could not change this folder"
+ "folderUpdateFailed": "Could not change this folder",
+ "sourceCount_one": "{{count}} source",
+ "sourceCount_other": "{{count}} sources",
+ "activeCount_one": "{{count}} active",
+ "activeCount_other": "{{count}} active",
+ "folderCount_one": "{{count}} folder",
+ "folderCount_other": "{{count}} folders",
+ "wordCount_one": "{{formatted}} word",
+ "wordCount_other": "{{formatted}} words"
},
"autoSavedSources": {
"undo": "Undo auto-saved sources",
@@ -1824,7 +1839,8 @@
"goToApp": "Go to App",
"openApp": "Open App",
"sharedOn": "Shared on",
- "views": "views"
+ "viewCount_one": "{{count}} view",
+ "viewCount_other": "{{count}} views"
},
"privacyPage": {
"title": "Privacy Policy",
@@ -2358,7 +2374,7 @@
"deleted": "Debate deleted",
"deleteFailed": "Could not delete the debate",
"lastActivity": "Last activity {{when}}",
- "counts": "{{sources}} sources ({{active}} active) · {{outputs}} outputs",
+ "counts": "{{sources}} ({{active}}) · {{outputs}}",
"menuAria": "Current debate: {{motion}}",
"open": "Open debate",
"useOneOff": "Use one-off mode",
@@ -2435,7 +2451,13 @@
"switching": "Switching…",
"deleting": "Deleting…",
"creating": "Creating…",
- "loadingCurrent": "Loading your current debate…"
+ "loadingCurrent": "Loading your current debate…",
+ "activeCount_one": "{{count}} active",
+ "activeCount_other": "{{count}} active",
+ "outputCount_one": "{{count}} output",
+ "outputCount_other": "{{count}} outputs",
+ "sourceCount_one": "{{count}} source",
+ "sourceCount_other": "{{count}} sources"
},
"debateContext": {
"using": "Using {{list}}",
diff --git a/src/locales/es.json b/src/locales/es.json
index 728ded605..393fb08c9 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -247,7 +247,8 @@
"deleted": "Eliminado",
"discuss": "Discutir",
"retry": "Reintentar",
- "undo": "Deshacer"
+ "undo": "Deshacer",
+ "notAvailable": "N/D"
},
"aiClient": {
"inputShortened": "La entrada se acortó",
@@ -476,7 +477,7 @@
"signOutFailed": "No se pudo cerrar la sesión. Inténtalo de nuevo.",
"aiModels": "Modelos de IA",
"aiModelsDesc": "Selecciona tu modelo preferido desde cada página de función usando el selector del encabezado.",
- "admin": "Admin",
+ "admin": "Administración",
"adminDesc": "Abre el panel de administración para uso, usuarios, límites y códigos promocionales.",
"openAdmin": "Abrir panel de admin",
"developer": "Desarrollador",
@@ -603,7 +604,7 @@
"createDemoCode": "Crear código demo",
"createDemoCodeDescription": "Emite un código de un solo uso para una cuenta demo. El código solo aparece después de crearlo.",
"label": "Etiqueta",
- "labelPlaceholder": "p. ej., Demo escolar de septiembre",
+ "labelPlaceholder": "p. ej. Demo escolar de septiembre",
"emailOptional": "Email asignado (opcional)",
"expiryOptional": "Caducidad (opcional)",
"maxUsesOptional": "Usos máximos (opcional)",
@@ -743,7 +744,7 @@
"promos": {
"createTitle": "Crear código promocional",
"code": "Código",
- "codePlaceholder": "p. ej., CIT2026",
+ "codePlaceholder": "p. ej. CIT2026",
"type": "Tipo",
"types": {
"extended_trial": "Prueba ampliada",
@@ -794,7 +795,7 @@
"currentEnds": "La prueba actual termina el {{date}}",
"days": "Días",
"notes": "Notas (opcional)",
- "notesPlaceholder": "p. ej., socio de torneo o solicitud de soporte",
+ "notesPlaceholder": "p. ej. socio de torneo o solicitud de soporte",
"cancel": "Cancelar",
"action_one": "Regalar {{count}} día",
"action_other": "Regalar {{count}} días",
@@ -925,7 +926,7 @@
"sampleSparringTitle": "Plan de la ronda",
"sampleOpeningLabel": "Apertura",
"sampleOpening": "Expón la regla, la excepción y la medida del éxito.",
- "sampleResponseLabel": "Respuesta",
+ "sampleResponseLabel": "Réplica",
"sampleResponse": "Compara el tiempo de aprendizaje con el coste de hacer cumplir la regla."
}
},
@@ -944,7 +945,7 @@
"crossfire": "Interrogatorio",
"crossfireDesc": "Practica preguntas y respuestas precisas en un intercambio cronometrado.",
"sparring": "Sparring",
- "sparringDesc": "Practica una ronda cronometrada contra una oponente y revisa la tarjeta de puntuación.",
+ "sparringDesc": "Practica una ronda cronometrada contra un oponente y revisa la tarjeta de puntuación.",
"library": "Biblioteca de fuentes",
"libraryDesc": "Gestiona fuentes que fundamentan las respuestas de IA con evidencia real.",
"timer": "Cronómetro de debate",
@@ -1085,7 +1086,7 @@
},
"generateForm": {
"motion": "Moción / tema",
- "motionPlaceholder": "ej., Esta cámara cree que la inteligencia artificial creará más empleos de los que destruirá",
+ "motionPlaceholder": "p. ej. Esta cámara cree que la inteligencia artificial creará más empleos de los que destruirá",
"side": "Lado",
"sidePro": "A favor",
"sideCon": "En contra",
@@ -1229,11 +1230,17 @@
"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",
- "resultTitle": "Crítica"
+ "resultTitle": "Crítica",
+ "criticalCount_one": "{{count}} crítico",
+ "criticalCount_other": "{{count}} críticos",
+ "majorCount_one": "{{count}} mayor",
+ "majorCount_other": "{{count}} mayores",
+ "minorCount_one": "{{count}} menor",
+ "minorCount_other": "{{count}} menores"
},
"brainstormForm": {
"motion": "Moción / tema",
- "motionPlaceholder": "ej., La Renta Básica Universal debería implementarse globalmente",
+ "motionPlaceholder": "p. ej. La Renta Básica Universal debería implementarse globalmente",
"side": "Lado a explorar",
"sideBoth": "Explorar ambos lados",
"sidePro": "Solo a favor",
@@ -1274,7 +1281,7 @@
},
"factoidsForm": {
"claim": "Declaración",
- "claimPlaceholder": "ej., La energía renovable es más rentable que los combustibles fósiles",
+ "claimPlaceholder": "p. ej. La energía renovable es más rentable que los combustibles fósiles",
"direction": "Postura",
"directionFor": "A favor",
"directionAgainst": "En contra",
@@ -1282,7 +1289,7 @@
"regionPlaceholder": "Global por defecto",
"sourceCount": "Fuentes",
"timeframe": "Período (opcional)",
- "timeframePlaceholder": "ej., 2020-2024, Última década",
+ "timeframePlaceholder": "p. ej. 2020-2024, Última década",
"sourceType": "Tipo de fuente",
"sourceAny": "Cualquier fuente confiable",
"sourceAcademic": "Solo académicas",
@@ -1297,7 +1304,6 @@
"unverified": "No verificado",
"pending": "Pendiente",
"importantNotes": "Notas importantes",
- "found": "Encontrados",
"factoids": "Datos",
"factoidsDesc": "Puntos de evidencia con citas",
"findMore": "Buscar más evidencia",
@@ -1322,7 +1328,11 @@
"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",
- "resultTitle": "Evidencia"
+ "resultTitle": "Evidencia",
+ "foundCount_one": "{{count}} dato encontrado",
+ "foundCount_other": "{{count}} datos encontrados",
+ "verifiedCount_one": "{{count}} verificado",
+ "verifiedCount_other": "{{count}} verificados"
},
"askChat": {
"title": "Pregunta a tu coach",
@@ -1349,7 +1359,7 @@
"followUp": "Preguntas de seguimiento:",
"pros": "Pros",
"cons": "Contras",
- "placeholder": "Pregunta a tu coach de debate...",
+ "placeholder": "Pregunta al coach…",
"inputLabel": "Mensaje para tu coach de debate",
"placeholderListening": "Escuchando...",
"saveToLibrary": "Guardar en biblioteca",
@@ -1476,14 +1486,14 @@
"debateCurrentDesc": "Todo en DebateLab pertenece a tu debate actual: su moción, sus fuentes y tus resultados. Ábrelo o cámbialo aquí."
},
"timer": {
- "title": "Temporizador de debate",
- "displayLabel": "Pantalla del temporizador",
+ "title": "Cronómetro de debate",
+ "displayLabel": "Pantalla del cronómetro",
"speechProgress": "Progreso de {{speech}}",
"selectFormat": "Seleccionar formato",
- "formatStandard": "Estándar (4-6-6-3)",
+ "formatStandard": "Formato escolar 4-6-6-3",
"formatLD": "Lincoln-Douglas",
- "formatPF": "Foro público",
- "formatPolicy": "Política",
+ "formatPF": "Public Forum",
+ "formatPolicy": "Policy",
"timeUp": "¡Se acabó el tiempo!",
"speaking": "Hablando",
"paused": "Pausado",
@@ -1495,7 +1505,7 @@
"timeUpSignal": "Señal de tiempo agotado",
"twoBeeps": "Dos pitidos",
"spaceStartStop": "Espacio para iniciar/detener",
- "reset": "Reiniciar temporizador",
+ "reset": "Reiniciar cronómetro",
"resetKey": "R para reiniciar",
"arrowsChange": "Flechas para cambiar orador",
"speeches": {
@@ -1535,7 +1545,7 @@
},
"disableSound": "Desactivar sonido",
"enableSound": "Activar sonido",
- "resetDone": "Temporizador reiniciado"
+ "resetDone": "Cronómetro reiniciado"
},
"flowchart": {
"title": "Diagrama de argumentos",
@@ -1669,7 +1679,6 @@
"deleteCaseLabel": "Eliminar {{title}}",
"load": "Cargar",
"resume": "Reanudar",
- "messages": "mensajes",
"caseDeleted": "Se eliminó el caso guardado.",
"deleteCaseDesc": "Esto eliminará el caso guardado de tu historial.",
"deleteCaseTitle": "¿Eliminar el caso guardado?",
@@ -1705,7 +1714,6 @@
"chooseFile": "Elegir archivo",
"importPreview": "Vista previa de importación",
"saveImported": "Guardar fuente importada",
- "words": "palabras",
"folder": "Carpeta",
"generalFolder": "General",
"textSource": "Texto",
@@ -1743,7 +1751,6 @@
"titlePlaceholder": "Título de la fuente...",
"originPlaceholder": "URL o referencia...",
"contentPlaceholder": "Pega el contenido de la fuente...",
- "sourcesCount": "fuentes",
"emptyTitle": "Sin fuentes aún",
"emptyDesc": "Añade fuentes para fundamentar las respuestas de IA con evidencia real.",
"manualEntry": "Entrada manual",
@@ -1787,7 +1794,7 @@
"restoreFailed": "No se pudo restaurar la fuente",
"deleteFailed": "No se pudo eliminar la fuente.",
"readingFile": "Leyendo el archivo…",
- "summary": "{{total}} fuentes · {{active}} activas · {{folders}} carpetas",
+ "summary": "{{sources}} · {{active}} · {{folders}}",
"mergeFolderTitle": "¿Fusionar con {{name}}?",
"mergeFolders": "Fusionar carpetas",
"mergeFailed": "No se pudieron fusionar las carpetas. Inténtalo de nuevo.",
@@ -1800,7 +1807,15 @@
"sourceSaved": "{{title}} se guardó en la biblioteca",
"duplicateUrlDesc": "Esta página ya está en la biblioteca de este debate.",
"loadingSources": "Cargando fuentes…",
- "folderUpdateFailed": "No se pudo cambiar esta carpeta"
+ "folderUpdateFailed": "No se pudo cambiar esta carpeta",
+ "sourceCount_one": "{{count}} fuente",
+ "sourceCount_other": "{{count}} fuentes",
+ "activeCount_one": "{{count}} activa",
+ "activeCount_other": "{{count}} activas",
+ "folderCount_one": "{{count}} carpeta",
+ "folderCount_other": "{{count}} carpetas",
+ "wordCount_one": "{{formatted}} palabra",
+ "wordCount_other": "{{formatted}} palabras"
},
"autoSavedSources": {
"undo": "Deshacer fuentes autoguardadas",
@@ -1824,7 +1839,8 @@
"goToApp": "Ir a la app",
"openApp": "Abrir app",
"sharedOn": "Compartido el",
- "views": "vistas"
+ "viewCount_one": "{{count}} vista",
+ "viewCount_other": "{{count}} vistas"
},
"privacyPage": {
"title": "Política de privacidad",
@@ -1862,7 +1878,7 @@
"s2": "Descripción del servicio",
"p2": "DebateLab es una herramienta de preparación de debates impulsada por IA que proporciona generación de argumentos, análisis y funciones de coaching.",
"s3": "Requisitos de cuenta",
- "p3": "Puedes iniciar sesión con Google, mediante un enlace enviado por email o con un código demo emitido por un administrador. Los códigos demo proporcionan acceso temporal a una cuenta designada. Eres responsable de toda la actividad de tu cuenta.",
+ "p3": "Puedes iniciar sesión con Google, con un código de seis dígitos enviado por email o con un código demo emitido por un administrador. Los códigos demo proporcionan acceso temporal a una cuenta designada. Eres responsable de toda la actividad de tu cuenta.",
"s4": "Suscripción y facturación",
"p4": "Las pruebas gratuitas duran 3 días con solicitudes limitadas. Las suscripciones Pro cuestan $5/mes más impuestos, facturadas vía Stripe. Cancela en cualquier momento.",
"s5": "Uso aceptable",
@@ -2004,7 +2020,7 @@
"format": {
"standard": "Formato escolar 4-6-6-3",
"ld": "Lincoln-Douglas",
- "pf": "Foro público"
+ "pf": "Public Forum"
},
"formatDescription": {
"standard": "El formato del colegio: Introductor, R1, R2 y Conclusor para ambos lados.",
@@ -2037,7 +2053,7 @@
"difficulty": {
"easy": "Calentamiento",
"medium": "Torneo",
- "hard": "Implacable"
+ "hard": "Despiadado"
},
"roleGuidance": {
"introductor": "Abre con un exordio, define los términos clave, fija el marco y la tesis, y presenta la hoja de ruta. Reserva los argumentos completos para R1.",
@@ -2358,7 +2374,7 @@
"deleted": "Debate eliminado",
"deleteFailed": "No se pudo eliminar el debate",
"lastActivity": "Última actividad {{when}}",
- "counts": "{{sources}} fuentes ({{active}} activas) · {{outputs}} resultados",
+ "counts": "{{sources}} ({{active}}) · {{outputs}}",
"menuAria": "Debate actual: {{motion}}",
"open": "Abrir debate",
"useOneOff": "Usar modo puntual",
@@ -2416,13 +2432,13 @@
"factoids": "Datos",
"chat": "Chat de preguntas",
"flowchart": "Diagrama",
- "crossfire": "Contrainterrogatorio",
+ "crossfire": "Interrogatorio",
"sparring": "Sparring",
"source": "Fuente"
},
"formats": {
"custom": "Personalizado",
- "bp": "Parlamentario británico",
+ "bp": "British Parliamentary",
"policy": "Policy",
"classroom": "Aula"
},
@@ -2435,7 +2451,13 @@
"switching": "Cambiando…",
"deleting": "Eliminando…",
"creating": "Creando…",
- "loadingCurrent": "Cargando tu debate actual…"
+ "loadingCurrent": "Cargando tu debate actual…",
+ "activeCount_one": "{{count}} activa",
+ "activeCount_other": "{{count}} activas",
+ "outputCount_one": "{{count}} resultado",
+ "outputCount_other": "{{count}} resultados",
+ "sourceCount_one": "{{count}} fuente",
+ "sourceCount_other": "{{count}} fuentes"
},
"debateContext": {
"using": "Usando {{list}}",
@@ -2557,7 +2579,7 @@
},
"shortAnswer": {
"question": "¿Cuál es la mejor herramienta de IA para practicar debate?",
- "answer": "Depende de lo que necesites. En {{asOf}}, DebateLab encaja con estudiantes y debatientes de competición que quieren un solo lugar para construir casos, encontrar puntos débiles, comprobar evidencia y hacer rondas de práctica cronometradas, en inglés, español o húngaro: prueba gratis de 3 días sin tarjeta y después $5/mes + impuestos. Si quieres practicar gratis contra un oponente de IA en estilo World Schools o Parlamentario Asiático, debatelab.ai, un producto distinto, dice ser gratuito. Si sobre todo necesitas ideas o ayuda para escribir sobre cualquier tema, un asistente general como ChatGPT es más flexible.",
+ "answer": "Depende de lo que necesites. En {{asOf}}, DebateLab encaja con estudiantes y debatientes de competición que quieren un solo lugar para construir casos, encontrar puntos débiles, comprobar evidencia y hacer rondas de práctica cronometradas, en inglés, español o húngaro: prueba gratis de 3 días sin tarjeta y después $5/mes + impuestos. Si quieres practicar gratis contra un oponente de IA en estilo World Schools o Asian Parliamentary, debatelab.ai, un producto distinto, dice ser gratuito. Si sobre todo necesitas ideas o ayuda para escribir sobre cualquier tema, un asistente general como ChatGPT es más flexible.",
"compare": "Comparar herramientas de IA para debate"
},
"sessionQuota": {
diff --git a/src/locales/hu.json b/src/locales/hu.json
index 33d5b20a9..dab956f00 100644
--- a/src/locales/hu.json
+++ b/src/locales/hu.json
@@ -187,7 +187,7 @@
"copyPrivateSource": "Másolás a privát könyvtárból",
"searchEvidence": "Bizonyíték keresése",
"insertCitation": "Hivatkozás beszúrása",
- "useInContext": "Használat az AI-kontextusban",
+ "useInContext": "Használat az MI-kontextusban",
"teamThreads": "Csapatbeszélgetések",
"threadName": "Beszélgetés neve",
"createThread": "Létrehozás",
@@ -199,13 +199,13 @@
"practiceFailed": "A gyakorlási elem nem menthető.",
"sharedToolScope": "Közös munkatér-menet · {{document}}",
"returnToWorkspace": "Vissza a munkatérhez",
- "suggestRewrite": "AI-átírás javaslata",
+ "suggestRewrite": "MI-átírás javaslata",
"sharePrivateResult": "Privát eredmény megosztása",
"shareResult": "Megosztás",
"resumePractice": "Folytatás",
"contextSummary": "Bevonva: kijelölt dokumentum, {{included}} forrás{{thread}}. Kihagyva: {{omitted}} forrás.",
"contextThread": ", közös beszélgetés",
- "contextDocuments": "AI-kontextus dokumentumai",
+ "contextDocuments": "MI-kontextus dokumentumai",
"proposal": {
"insert": "Javasolt beszúrás",
"delete": "Javasolt törlés",
@@ -247,7 +247,8 @@
"deleted": "Törölve",
"discuss": "Megvitatás",
"retry": "Újrapróbálás",
- "undo": "Visszavonás"
+ "undo": "Visszavonás",
+ "notAvailable": "nincs adat"
},
"aiClient": {
"inputShortened": "A bemenet rövidítve lett",
@@ -393,7 +394,7 @@
"flowchartOtherDebate": "A folyamatábra csak a jelenlegi vitában létrehozott esetet fogadja. Hozd létre itt az esetet, hogy elküldhesd."
},
"attack": {
- "emptyTitle": "Kegyetlen mód",
+ "emptyTitle": "Kíméletlen 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",
"working": "Az eseted átnézése…",
@@ -476,7 +477,7 @@
"signOutFailed": "Nem sikerült kijelentkezni. Próbáld újra.",
"aiModels": "MI modellek",
"aiModelsDesc": "A kívánt modellt az egyes funkcióoldalak fejlécében lévő modellválasztóval állíthatod be.",
- "admin": "Admin",
+ "admin": "Adminisztráció",
"adminDesc": "Admin irányítópult megnyitása használathoz, felhasználókhoz, limitekhez és promókódokhoz.",
"openAdmin": "Admin panel megnyitása",
"developer": "Fejlesztői",
@@ -534,7 +535,7 @@
"subscriptionEnded": "A Pro-előfizetésed véget ért. Fizess elő újra az MI-eszközök további használatához."
},
"login": {
- "title": "Bejelentkezés a DebateLab-ba",
+ "title": "Bejelentkezés a DebateLabba",
"subtitle": "Jelentkezz be Google-lal, e-maillel vagy adminisztrátor által kiadott demókóddal.",
"google": "Folytatás Google-lal",
"emailTab": "E-mail",
@@ -580,7 +581,7 @@
"redirecting": "Átirányítás...",
"failed": "Nem sikerült elindítani a bejelentkezést",
"tryAgain": "Próbáld újra.",
- "backHome": "Vissza a DebateLabhez",
+ "backHome": "Vissza a DebateLabhoz",
"sessionExpired": "A munkameneted lejárt. Jelentkezz be újra.",
"errors": {
"demoNotFound": "Nincs ilyen demókód.",
@@ -732,7 +733,7 @@
"confirmMakeAdmin": "Adminná teszed: {{email}}?",
"confirmMakeAdminDesc": "Az adminok megnyithatják az adminoldalt, és módosíthatják mások szerepkörét és csomagját.",
"confirmBan": "Letiltod: {{email}}?",
- "confirmBanDesc": "Addig nem használhatja a DebateLabet, amíg fel nem oldod a tiltást.",
+ "confirmBanDesc": "Addig nem használhatja a DebateLabot, amíg fel nem oldod a tiltást.",
"confirmUnban": "Feloldod a tiltást: {{email}}?",
"confirmRemoveAdmin": "Elveszed az admin jogot: {{email}}?",
"confirmRoleDesc": "Átlagos felhasználó lesz.",
@@ -925,15 +926,15 @@
"sampleSparringTitle": "Fordulóterv",
"sampleOpeningLabel": "Nyitás",
"sampleOpening": "Mondd el a szabályt, a kivételt és a siker mércéjét.",
- "sampleResponseLabel": "Válasz",
+ "sampleResponseLabel": "Reagálás",
"sampleResponse": "Hasonlítsd össze a tanulási időt a szabály betartatásának költségével."
}
},
"features": {
"title": "Minden, ami a győzelemhez kell",
- "generate": "Eset generátor",
+ "generate": "Esetgenerátor",
"generateDesc": "Építs teljes eseteket strukturált érvekkel, bizonyítékokkal és ellen-stratégiákkal.",
- "attack": "Eset támadó",
+ "attack": "Esettámadó",
"attackDesc": "Teszteld az érveidet és találd meg a gyengeségeket az ellenfelek előtt.",
"brainstorm": "Ötletelés",
"brainstormDesc": "Fedezz fel szokatlan szögeket és találj friss érvelési utakat.",
@@ -947,7 +948,7 @@
"sparringDesc": "Gyakorolj egy időzített fordulót ellenféllel, majd nézd át az értékelőlapot.",
"library": "Forráskönyvtár",
"libraryDesc": "Kezeld a forrásokat, amelyek valós bizonyítékokkal alapozzák meg az MI válaszait.",
- "timer": "Vita időmérő",
+ "timer": "Vitaidőmérő",
"timerDesc": "Gyakorolj versenypontos időzítéssel.",
"flowchart": "Folyamatábra",
"flowchartDesc": "Térképezd az érvstruktúrákat és cáfolati kapcsolatokat vizuálisan.",
@@ -1098,7 +1099,7 @@
"moreOptions": "További opciók",
"length": "Eset hossza",
"lengthBrief": "Rövid",
- "lengthStandard": "Standard",
+ "lengthStandard": "Normál",
"lengthComprehensive": "Átfogó",
"audience": "Szint",
"audienceBeginner": "Kezdő",
@@ -1115,7 +1116,7 @@
"startVoice": "Hangbevitel indítása",
"stopVoice": "Hangbevitel leállítása",
"dictate": "Diktáld az indítványt",
- "styleNote": "A írásstílust befolyásolja; a kutatást nem változtatja meg."
+ "styleNote": "Az írásstílust befolyásolja; a kutatást nem változtatja meg."
},
"generateResults": {
"incomplete": "Generálás befejezetlen",
@@ -1229,7 +1230,13 @@
"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",
- "resultTitle": "Kritika"
+ "resultTitle": "Kritika",
+ "criticalCount_one": "{{count}} kritikus",
+ "criticalCount_other": "{{count}} kritikus",
+ "majorCount_one": "{{count}} jelentős",
+ "majorCount_other": "{{count}} jelentős",
+ "minorCount_one": "{{count}} kisebb",
+ "minorCount_other": "{{count}} kisebb"
},
"brainstormForm": {
"motion": "Indítvány / téma",
@@ -1297,7 +1304,6 @@
"unverified": "Nem ellenőrzött",
"pending": "Függőben",
"importantNotes": "Fontos megjegyzések",
- "found": "Találat",
"factoids": "Tény",
"factoidsDesc": "Bizonyítékok hivatkozásokkal",
"findMore": "További bizonyítékok keresése",
@@ -1322,10 +1328,14 @@
"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",
- "resultTitle": "Bizonyítékok"
+ "resultTitle": "Bizonyítékok",
+ "foundCount_one": "{{count}} tényt találtunk",
+ "foundCount_other": "{{count}} tényt találtunk",
+ "verifiedCount_one": "{{count}} ellenőrzött",
+ "verifiedCount_other": "{{count}} ellenőrzött"
},
"askChat": {
- "title": "Kérdezd az edződ",
+ "title": "Kérdezd az edződet",
"subtitle": "Kapj válaszokat vitakérdésekre",
"startConversation": "Indíts beszélgetést",
"startDesc": "Kérdezz bármit vitastratégiáról, érvekről, vagy kérj segítséget az esetedhez.",
@@ -1349,7 +1359,7 @@
"followUp": "Követő kérdések:",
"pros": "Mellette",
"cons": "Ellene",
- "placeholder": "Kérdezd a vitaedződ...",
+ "placeholder": "Kérdezd a vitaedződet…",
"inputLabel": "Üzenet a vitaedződnek",
"placeholderListening": "Hallgatás...",
"saveToLibrary": "Mentés a könyvtárba",
@@ -1459,7 +1469,7 @@
"freePeriod_other": "{{count}} napig"
},
"onboarding": {
- "welcome": "Üdvözlünk a DebateLab-ban! 🎉",
+ "welcome": "Üdvözlünk a DebateLabban! 🎉",
"ready": "Készen állsz! 🚀",
"readyDesc": "Fedezd fel a többi funkciót: Érvek támadása, Edző chat és Tények kutatása.",
"back": "Vissza",
@@ -1476,14 +1486,14 @@
"debateCurrentDesc": "A DebateLabban minden az aktuális vitádhoz tartozik: a tételéhez, a forrásaihoz és az eredményeidhez. Itt nyithatod meg vagy cserélheted le."
},
"timer": {
- "title": "Vita időmérő",
+ "title": "Vitaidőmérő",
"displayLabel": "Időmérő kijelző",
"speechProgress": "{{speech}} előrehaladása",
"selectFormat": "Formátum választása",
- "formatStandard": "Standard (4-6-6-3)",
+ "formatStandard": "Iskolai 4-6-6-3",
"formatLD": "Lincoln-Douglas",
- "formatPF": "Nyilvános fórum",
- "formatPolicy": "Politikai",
+ "formatPF": "Public Forum",
+ "formatPolicy": "Policy",
"timeUp": "Lejárt az idő!",
"speaking": "Beszél",
"paused": "Szüneteltetve",
@@ -1503,7 +1513,7 @@
"intro": "Bevezető",
"r1": "Cáfolat 1",
"r2": "Cáfolat 2",
- "conc": "Összegző"
+ "conc": "Záró"
},
"ld": {
"ac": "Igenlő felépítés",
@@ -1539,7 +1549,7 @@
},
"flowchart": {
"title": "Érvelési folyamatábra",
- "load": "Betöltés",
+ "load": "Megnyitás",
"save": "Mentés",
"addNode": "Csomópont hozzáadása",
"edit": "Szerkesztés",
@@ -1667,9 +1677,8 @@
"sparringDeleted": "A vitapárbaj forduló törölve.",
"sparringDeleteFailed": "Nem sikerült törölni a vitapárbaj fordulót.",
"deleteCaseLabel": "{{title}} törlése",
- "load": "Betöltés",
+ "load": "Megnyitás",
"resume": "Folytatás",
- "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?",
@@ -1705,7 +1714,6 @@
"chooseFile": "Fájl választása",
"importPreview": "Import előnézet",
"saveImported": "Importált forrás mentése",
- "words": "szó",
"folder": "Mappa",
"generalFolder": "Általános",
"textSource": "Szöveg",
@@ -1732,7 +1740,7 @@
"importFailed": "Import sikertelen",
"importFailedDesc": "Nem sikerült importálni ezt a forrást.",
"duplicateSource": "A forrás már a könyvtárban van",
- "duplicateSourceDesc": "Ez a URL vagy fájl már mentve van.",
+ "duplicateSourceDesc": "Ez az URL vagy fájl már mentve van.",
"fileReadFailed": "Nem sikerült beolvasni ezt a fájlt.",
"saveToLibrary": "Mentés a könyvtárba",
"addNewTitle": "Új forrás hozzáadása",
@@ -1743,7 +1751,6 @@
"titlePlaceholder": "Forrás címe...",
"originPlaceholder": "URL vagy hivatkozás...",
"contentPlaceholder": "Illeszd be a forrás tartalmát...",
- "sourcesCount": "forrás",
"emptyTitle": "Még nincsenek források",
"emptyDesc": "Adj hozzá forrásokat az MI válaszok valós bizonyítékokkal való megalapozásához.",
"manualEntry": "Kézi bevitel",
@@ -1787,7 +1794,7 @@
"restoreFailed": "Nem sikerült visszaállítani a forrást",
"deleteFailed": "Nem sikerült törölni a forrást.",
"readingFile": "Fájl beolvasása…",
- "summary": "{{total}} forrás · {{active}} aktív · {{folders}} mappa",
+ "summary": "{{sources}} · {{active}} · {{folders}}",
"mergeFolderTitle": "Összevonod ezzel: {{name}}?",
"mergeFolders": "Mappák összevonása",
"mergeFailed": "Nem sikerült összevonni a mappákat. Próbáld újra.",
@@ -1800,7 +1807,15 @@
"sourceSaved": "A(z) {{title}} bekerült a könyvtárba",
"duplicateUrlDesc": "Ez az oldal már szerepel ennek a vitának a könyvtárában.",
"loadingSources": "Források betöltése…",
- "folderUpdateFailed": "Nem sikerült módosítani ezt a mappát"
+ "folderUpdateFailed": "Nem sikerült módosítani ezt a mappát",
+ "sourceCount_one": "{{count}} forrás",
+ "sourceCount_other": "{{count}} forrás",
+ "activeCount_one": "{{count}} aktív",
+ "activeCount_other": "{{count}} aktív",
+ "folderCount_one": "{{count}} mappa",
+ "folderCount_other": "{{count}} mappa",
+ "wordCount_one": "{{formatted}} szó",
+ "wordCount_other": "{{formatted}} szó"
},
"autoSavedSources": {
"undo": "Automatikus források visszavonása",
@@ -1824,7 +1839,8 @@
"goToApp": "Ugrás az appba",
"openApp": "App megnyitása",
"sharedOn": "Megosztva",
- "views": "megtekintés"
+ "viewCount_one": "{{count}} megtekintés",
+ "viewCount_other": "{{count}} megtekintés"
},
"privacyPage": {
"title": "Adatvédelmi irányelvek",
@@ -1862,7 +1878,7 @@
"s2": "Szolgáltatás leírása",
"p2": "A DebateLab egy MI-alapú vitafelkészítő eszköz, amely érvgenerálást, elemzést és coaching funkciókat biztosít.",
"s3": "Fiókkövetelmények",
- "p3": "Google-lal, e-mailben kapott bejelentkezési hivatkozással vagy adminisztrátor által kiadott demókóddal jelentkezhetsz be. A demókódok ideiglenes hozzáférést adnak egy kijelölt fiókhoz. Felelős vagy a fiókod alatt végzett minden tevékenységért.",
+ "p3": "Google-lal, e-mailben kapott hatjegyű kóddal vagy adminisztrátor által kiadott demókóddal jelentkezhetsz be. A demókódok ideiglenes hozzáférést adnak egy kijelölt fiókhoz. Felelős vagy a fiókod alatt végzett minden tevékenységért.",
"s4": "Előfizetés és számlázás",
"p4": "Az ingyenes próba 3 napig tart korlátozott kérésekkel. A Pro előfizetés $5/hó plusz adó, Stripe-on keresztül számlázva. Bármikor lemondható a Beállításokból.",
"s5": "Elfogadható használat",
@@ -2007,19 +2023,19 @@
"pf": "Public Forum"
},
"formatDescription": {
- "standard": "Az iskolai formátum: Introductor, R1, R2 és Conclusor mindkét oldalon.",
+ "standard": "Az iskolai formátum: bevezető, R1, R2 és záró beszéd mindkét oldalon.",
"ld": "Értékalapú forduló konstruktív beszédekkel és két cáfolati szakasszal.",
"pf": "Konstruktív beszédek, cáfolatok, összegzések és végső fókusz mindkét oldalnak."
},
"turns": {
- "standard-1": "Pro Introductor",
- "standard-2": "Kontra Introductor",
+ "standard-1": "Pro bevezető",
+ "standard-2": "Kontra bevezető",
"standard-3": "Pro R1",
"standard-4": "Kontra R1",
"standard-5": "Pro R2",
"standard-6": "Kontra R2",
- "standard-7": "Pro Conclusor",
- "standard-8": "Kontra Conclusor",
+ "standard-7": "Pro záró",
+ "standard-8": "Kontra záró",
"ld-1": "Affirmatív érvelés",
"ld-2": "Negatív érvelés",
"ld-3": "Első affirmatív cáfolat",
@@ -2186,7 +2202,7 @@
"label": "Nyelv",
"title": "Nyelv",
"uiLanguage": "Felület nyelve",
- "outputLanguage": "AI kimenet nyelve"
+ "outputLanguage": "MI-kimenet nyelve"
},
"crossfire": {
"openFromResult": "Megnyitás a Keresztkérdezésben",
@@ -2358,7 +2374,7 @@
"deleted": "Vita törölve",
"deleteFailed": "Nem sikerült törölni a vitát",
"lastActivity": "Utolsó tevékenység: {{when}}",
- "counts": "{{sources}} forrás ({{active}} aktív) · {{outputs}} eredmény",
+ "counts": "{{sources}} ({{active}}) · {{outputs}}",
"menuAria": "Aktuális vita: {{motion}}",
"open": "Vita megnyitása",
"useOneOff": "Egyszeri mód használata",
@@ -2410,20 +2426,20 @@
"neutral": "Semleges"
},
"kinds": {
- "case": "Esetépítés",
+ "case": "Eset",
"attack": "Támadás",
"brainstorm": "Ötletelés",
"factoids": "Tények",
"chat": "Kérdező csevegés",
"flowchart": "Folyamatábra",
"crossfire": "Keresztkérdezés",
- "sparring": "Edzőmeccs",
+ "sparring": "Vitapárbaj",
"source": "Forrás"
},
"formats": {
"custom": "Egyéni",
- "bp": "Brit parlamenti",
- "policy": "Szakpolitikai",
+ "bp": "British Parliamentary",
+ "policy": "Policy",
"classroom": "Tantermi"
},
"offerTitle": "Indítasz egy vitát ezzel a tétellel?",
@@ -2435,7 +2451,13 @@
"switching": "Váltás…",
"deleting": "Törlés…",
"creating": "Létrehozás…",
- "loadingCurrent": "Az aktuális vitád betöltése…"
+ "loadingCurrent": "Az aktuális vitád betöltése…",
+ "activeCount_one": "{{count}} aktív",
+ "activeCount_other": "{{count}} aktív",
+ "outputCount_one": "{{count}} eredmény",
+ "outputCount_other": "{{count}} eredmény",
+ "sourceCount_one": "{{count}} forrás",
+ "sourceCount_other": "{{count}} forrás"
},
"debateContext": {
"using": "Felhasználva: {{list}}",
@@ -2454,7 +2476,7 @@
"instructionsUsed": "A vita beállításai és utasításai benne vannak.",
"instructionsNotUsed": "A vita beállításai benne vannak. A szabad szöveges utasításokat itt nem használjuk, így nem módosíthatják a gyakorlat menetét.",
"sourcesHeading": "Könyvtári források",
- "sparringNoSources": "Az élő edzőmeccsek csak az átiratot, a témát és a vita beállításait használják.",
+ "sparringNoSources": "Az élő vitapárbajok csak az átiratot, a témát és a vita beállításait használják.",
"sourcesOff": "A forráskönyvtár ki van kapcsolva ennél a kérésnél.",
"noSources": "Ehhez a vitához még nincs forrás a könyvtárban.",
"resultsHeading": "Mentett eredmények",
@@ -2557,7 +2579,7 @@
},
"shortAnswer": {
"question": "Melyik a legjobb MI-alapú vitagyakorló eszköz?",
- "answer": "Attól függ, mire van szükséged. {{asOf}} állapot szerint a DebateLab azoknak a diákoknak és versenyvitázóknak való, akik egy helyen szeretnének eseteket építeni, gyenge pontokat találni, bizonyítékokat ellenőrizni és időzített gyakorlófordulókat futtatni angolul, spanyolul vagy magyarul: 3 napos ingyenes próba bankkártya nélkül, utána $5/hó + adó. Ha ingyen szeretnél MI-ellenféllel gyakorolni World Schools vagy ázsiai parlamentáris stílusban, a debatelab.ai, egy másik termék, azt írja magáról, hogy ingyenes. Ha főleg ötletelésre vagy írási segítségre van szükséged bármilyen témában, egy általános asszisztens, például a ChatGPT rugalmasabb.",
+ "answer": "Attól függ, mire van szükséged. {{asOf}} állapot szerint a DebateLab azoknak a diákoknak és versenyvitázóknak való, akik egy helyen szeretnének eseteket építeni, gyenge pontokat találni, bizonyítékokat ellenőrizni és időzített gyakorlófordulókat futtatni angolul, spanyolul vagy magyarul: 3 napos ingyenes próba bankkártya nélkül, utána $5/hó + adó. Ha ingyen szeretnél MI-ellenféllel gyakorolni World Schools vagy Asian Parliamentary stílusban, a debatelab.ai, egy másik termék, azt írja magáról, hogy ingyenes. Ha főleg ötletelésre vagy írási segítségre van szükséged bármilyen témában, egy általános asszisztens, például a ChatGPT rugalmasabb.",
"compare": "MI-alapú vitaeszközök összehasonlítása"
},
"sessionQuota": {
@@ -2604,12 +2626,12 @@
"description": "Ez az oldal nem létezik a {{brand}} webhelyén."
},
"tools": {
- "title": "Ingyenes vitaeszközök: időzítő, indítványok, jegyzetlapok | {{brand}}",
- "description": "Ingyenes, fiók nélkül használható vitaeszközök: formátumhoz igazodó időzítő, indítványgenerátor, nyomtatható jegyzetlapok és beszédidő-kalkulátor."
+ "title": "Ingyenes vitaeszközök: időmérő, indítványok, jegyzetlapok | {{brand}}",
+ "description": "Ingyenes, fiók nélkül használható vitaeszközök: formátumhoz igazodó időmérő, indítványgenerátor, nyomtatható jegyzetlapok és beszédidő-kalkulátor."
},
"debateTimer": {
- "title": "Ingyenes vitaidőzítő PF, LD, Policy, BP és World Schools formátumhoz | {{brand}}",
- "description": "Ingyenes vitaidőzítő PF, LD, Policy, BP, World Schools és Asian Parliamentary beállításokkal: védett idő, POI-jelzés és felkészülési idő."
+ "title": "Ingyenes vitaidőmérő PF, LD, Policy, BP és World Schools formátumhoz | {{brand}}",
+ "description": "Ingyenes vitaidőmérő PF, LD, Policy, BP, World Schools és Asian Parliamentary beállításokkal: védett idő, POI-jelzés és felkészülési idő."
},
"motionGenerator": {
"title": "Indítványgenerátor: 300 gyakorló vitaindítvány | {{brand}}",
diff --git a/src/locales/tools/es.json b/src/locales/tools/es.json
index 33b2e3196..b684b09d7 100644
--- a/src/locales/tools/es.json
+++ b/src/locales/tools/es.json
@@ -116,7 +116,7 @@
"clearFilters": "Quitar filtros",
"empty": "Pulsa Sacar una moción para obtener una, o Sacar tres para un ejercicio de veto.",
"optionN": "Opción {{n}}",
- "infoSlide": "Info:",
+ "infoSlide": "Información:",
"copy": "Copiar moción",
"copyLink": "Copiar enlace",
"copied": "Copiado",
diff --git a/src/locales/tools/hu.json b/src/locales/tools/hu.json
index 1a3543774..78f9649af 100644
--- a/src/locales/tools/hu.json
+++ b/src/locales/tools/hu.json
@@ -121,7 +121,7 @@
"copyLink": "Link másolása",
"copied": "Másolva",
"copyPrompt": "Másold ki ezt a szöveget:",
- "prepCta": "Készülj fel a tétel mindkét oldalára a DebateLabben",
+ "prepCta": "Készülj fel a tétel mindkét oldalára a DebateLabban",
"topics": {
"education": "Oktatás",
"technology": "Technológia és MI",
diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx
index fa10bd7d4..b49147034 100644
--- a/src/pages/AdminPage.tsx
+++ b/src/pages/AdminPage.tsx
@@ -20,7 +20,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
-import { formatEuropeanDate, formatEuropeanDateTime, formatNumber } from '@/lib/date-format';
+import { formatDecimal, formatEuropeanDate, formatEuropeanDateTime, formatNumber } from '@/lib/date-format';
import { captureProductEvent } from '@/lib/observability';
import { AdminAnnouncements } from '@/components/admin/AdminAnnouncements';
@@ -463,9 +463,9 @@ export default function AdminPage() {
const { t } = useTranslation();
const formatKnownCost = (cost: number, unknownCostRequests = 0) => {
if (cost > 0 && unknownCostRequests > 0) {
- return t('admin.dashboard.knownPlusUnknownCost', { cost: cost.toFixed(4), count: unknownCostRequests });
+ return t('admin.dashboard.knownPlusUnknownCost', { cost: formatDecimal(cost, 4), count: unknownCostRequests });
}
- if (cost > 0) return `$${cost.toFixed(4)}`;
+ if (cost > 0) return `$${formatDecimal(cost, 4)}`;
if (unknownCostRequests > 0) return t('admin.dashboard.unknownCost');
return '$0.0000';
};
@@ -798,7 +798,7 @@ export default function AdminPage() {
- {stats?.totals.successRate.toFixed(1) || 100}%
+ {stats ? formatDecimal(stats.totals.successRate, 1) : 100}%
{t('admin.dashboard.successRateDescription')}
diff --git a/src/pages/CurrentDebatePage.tsx b/src/pages/CurrentDebatePage.tsx
index 63fb6288b..108c3c20a 100644
--- a/src/pages/CurrentDebatePage.tsx
+++ b/src/pages/CurrentDebatePage.tsx
@@ -9,7 +9,7 @@ import { EmptyState } from '@/components/ui/empty-state';
import { PageHeader } from '@/components/layout/PageHeader';
import { PRACTICE_TOOLS, PREPARE_TOOLS, ToolLink } from '@/components/features/tools/tool-links';
import { DebateForm } from '@/components/features/debate/DebateForm';
-import { sideLabelKey, useDebateCounts } from '@/components/features/debate/CurrentDebateStrip';
+import { debateCountsLabel, sideLabelKey, useDebateCounts } from '@/components/features/debate/CurrentDebateStrip';
import { useAllDebateItems, useOpenDebateItem, type DebateItem } from '@/components/features/debate/debate-items';
import { useToast } from '@/hooks/use-toast';
import { formatEuropeanDateTime, formatRelativeDate } from '@/lib/date-format';
@@ -110,7 +110,7 @@ export default function CurrentDebatePage() {
meta={<>
{t('debate.lastActivity', { when: formatRelativeDate(debate.lastActivityAt), defaultValue: 'Last activity {{when}}' })}
{' · '}
- {t('debate.counts', { sources: sourceCounts.sources, active: sourceCounts.activeSources, outputs: items.length, defaultValue: '{{sources}} sources ({{active}} active) · {{outputs}} outputs' })}
+ {debateCountsLabel(t, { ...sourceCounts, outputs: items.length })}
>}
actions={<>
setEditing((value) => !value)}>
diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx
index dac216047..424435430 100644
--- a/src/pages/HistoryPage.tsx
+++ b/src/pages/HistoryPage.tsx
@@ -204,7 +204,7 @@ export default function HistoryPage() {
{item.title}
- {item.data.metadata?.side?.toUpperCase() || 'N/A'}
+ {item.data.metadata?.side ? t(`debate.side.${item.data.metadata.side}`, item.data.metadata.side.toUpperCase()) : t('common.notAvailable')}
{formatEuropeanDateTime(item.createdAt)}
@@ -239,7 +239,7 @@ export default function HistoryPage() {
{item.title}
-
{item.data.length} {t('historyPage.messages')} • {formatEuropeanDateTime(item.createdAt)}
+
{t('askChat.messageCount', { count: item.data.length })} •{formatEuropeanDateTime(item.createdAt)}
handleLoadChat(item.id)}>
{t('historyPage.resume')}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index 4b6033224..8e8e3c4da 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -8,7 +8,7 @@ import { PageHeader } from '@/components/layout/PageHeader';
import { PRACTICE_TOOLS, PREPARE_TOOLS, ToolLink, UTILITY_TOOLS, type Tool } from '@/components/features/tools/tool-links';
import { formatRelativeDate } from '@/lib/date-format';
import { useCurrentDebate, useDebateStore } from '@/lib/stores/debate-store';
-import { useDebateCounts } from '@/components/features/debate/CurrentDebateStrip';
+import { debateCountsLabel, useDebateCounts } from '@/components/features/debate/CurrentDebateStrip';
function ToolGroup({ title, tools }: { title: string; tools: readonly Tool[] }) {
return (
@@ -70,7 +70,7 @@ function CurrentDebateSummary({ t }: { t: TFunction }) {
{t('debate.lastActivity', { when: formatRelativeDate(debate.lastActivityAt), defaultValue: 'Last activity {{when}}' })}
{' · '}
- {t('debate.counts', { sources: counts.sources, active: counts.activeSources, outputs: counts.outputs, defaultValue: '{{sources}} sources ({{active}} active) · {{outputs}} outputs' })}
+ {debateCountsLabel(t, counts)}
{oneOff && {t('debate.oneOffActive', 'One-off mode is on')}
}
diff --git a/src/pages/LibraryPage.tsx b/src/pages/LibraryPage.tsx
index ccaed2f5c..9e3b50535 100644
--- a/src/pages/LibraryPage.tsx
+++ b/src/pages/LibraryPage.tsx
@@ -10,7 +10,7 @@ import { Trash2, Plus, Globe, FileText, Database, ShieldCheck, ShieldAlert, Exte
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/hooks/use-toast';
-import { formatEuropeanDate } from '@/lib/date-format';
+import { formatEuropeanDate, formatNumber } from '@/lib/date-format';
import { importSourceFromUrl } from '@/lib/source-import';
import { findSavedPage } from '@/lib/source-url';
import { validateUrl } from '@/lib/validation/url';
@@ -250,6 +250,7 @@ export default function LibraryPage() {
folder: 'General',
isActive: true,
});
+ const draftWordCount = sourceDraft.content.trim().split(/\s+/).filter(Boolean).length;
const [importPreview, setImportPreview] = useState<{
title: string;
content: string;
@@ -688,7 +689,11 @@ export default function LibraryPage() {
openDebateDialog('switch')}>{t('debate.change', 'Change')}
- {t('libraryPage.summary', { total: sourceLibrary.length, active: activeSourceCount, folders: folders.length, defaultValue: '{{total}} sources · {{active}} active · {{folders}} folders' })}
+ {t('libraryPage.summary', {
+ sources: t('libraryPage.sourceCount', { count: sourceLibrary.length }),
+ active: t('libraryPage.activeCount', { count: activeSourceCount }),
+ folders: t('libraryPage.folderCount', { count: folders.length }),
+ })}
>
) : (
@@ -846,7 +851,7 @@ export default function LibraryPage() {
{t('libraryPage.importPreview')}
- {[importPreview.domain || importPreview.fileType || importPreview.origin, importPreview.wordCount ? `${importPreview.wordCount} ${t('libraryPage.words')}` : null].filter(Boolean).join(' • ')}
+ {[importPreview.domain || importPreview.fileType || importPreview.origin, importPreview.wordCount ? t('libraryPage.wordCount', { count: importPreview.wordCount, formatted: formatNumber(importPreview.wordCount) }) : null].filter(Boolean).join(' • ')}
void handleSavePreview()}>
@@ -942,7 +947,7 @@ export default function LibraryPage() {
{!firstLoadPending && (
- {visibleSources.length} {t('libraryPage.sourcesCount')}
+ {t('libraryPage.sourceCount', { count: visibleSources.length })}
)}
@@ -1160,7 +1165,7 @@ export default function LibraryPage() {
{sourceDraft.title || t('libraryPage.untitledSource', 'Untitled source')}
- {[getFolderName(sourceDraft.folder || 'General'), formatEuropeanDate(selectedSource.createdAt), `${sourceDraft.content.trim().split(/\s+/).filter(Boolean).length} ${t('libraryPage.words')}`].join(' • ')}
+ {[getFolderName(sourceDraft.folder || 'General'), formatEuropeanDate(selectedSource.createdAt), t('libraryPage.wordCount', { count: draftWordCount, formatted: formatNumber(draftWordCount) })].join(' • ')}
diff --git a/src/pages/SharedPage.tsx b/src/pages/SharedPage.tsx
index 6fbb00c8f..2a4b93c7f 100644
--- a/src/pages/SharedPage.tsx
+++ b/src/pages/SharedPage.tsx
@@ -174,7 +174,7 @@ export default function SharedPage() {
{item.item_type}
{t('sharedPage.sharedOn')} {formatEuropeanDate(item.created_at)}
- • {item.view_count} {t('sharedPage.views')}
+ • {t('sharedPage.viewCount', { count: item.view_count })}
{result ?? (
diff --git a/src/test/a11y/names-and-headings.test.tsx b/src/test/a11y/names-and-headings.test.tsx
index ef69bf381..5cbddb458 100644
--- a/src/test/a11y/names-and-headings.test.tsx
+++ b/src/test/a11y/names-and-headings.test.tsx
@@ -79,7 +79,7 @@ describe('accessible names and headings', () => {
rerender(
);
expect(screen.getByRole('button', { name: 'Nyelv' })).toBeInTheDocument();
expect(screen.getByRole('radiogroup', { name: 'Felület nyelve' })).toBeInTheDocument();
- expect(screen.getByRole('radiogroup', { name: 'AI kimenet nyelve' })).toBeInTheDocument();
+ expect(screen.getByRole('radiogroup', { name: 'MI-kimenet nyelve' })).toBeInTheDocument();
});
it('names Generate selects from their visible labels', () => {
diff --git a/src/test/components/LimitReachedModal.test.tsx b/src/test/components/LimitReachedModal.test.tsx
index 4ad1b3a7d..30fae3b42 100644
--- a/src/test/components/LimitReachedModal.test.tsx
+++ b/src/test/components/LimitReachedModal.test.tsx
@@ -101,7 +101,7 @@ describe('LimitReachedModal', () => {
subscription.billingDate = '2026-10-01T12:00:00.000Z';
const { onUpgrade } = renderModal('subscription_ended');
expect(screen.getByRole('heading', { name: 'Pro Subscription Ended' })).toBeInTheDocument();
- expect(screen.getByText(/Your Pro subscription ended on 01\/10\/2026/)).toBeInTheDocument();
+ expect(screen.getByText(/Your Pro subscription ended on 1 Oct 2026/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Resubscribe' }));
expect(onUpgrade).toHaveBeenCalled();
});
diff --git a/src/test/components/SubscriptionStatus.test.tsx b/src/test/components/SubscriptionStatus.test.tsx
index 926902480..55a3ebc7f 100644
--- a/src/test/components/SubscriptionStatus.test.tsx
+++ b/src/test/components/SubscriptionStatus.test.tsx
@@ -30,7 +30,7 @@ const CASES: Array<{
text: RegExp;
button: string;
}> = [
- { name: 'trial', props: { billingState: 'trial', billingDate: DATE, billingDateKind: 'ends' }, badge: 'Free Trial', date: 'Ends on 22/11/2026', text: /AI access is active/, button: 'Upgrade to Pro' },
+ { name: 'trial', props: { billingState: 'trial', billingDate: DATE, billingDateKind: 'ends' }, badge: 'Free Trial', date: 'Ends on 22 Nov 2026', text: /AI access is active/, button: 'Upgrade to Pro' },
{
// QB-02: an override on an expired trial works, so it is not "Expired".
name: 'extended access',
@@ -44,7 +44,7 @@ const CASES: Array<{
name: 'Pro renewing',
props: { billingState: 'pro', billingDate: DATE, billingDateKind: 'renews', hasOpenSubscription: true, hasStripeBilling: true, dailyLimit: 50 },
badge: 'Pro',
- date: 'Renews on 22/11/2026',
+ date: 'Renews on 22 Nov 2026',
text: /AI access is active/,
button: 'Manage Subscription',
},
@@ -53,7 +53,7 @@ const CASES: Array<{
name: 'Pro set to cancel',
props: { billingState: 'pro_canceling', billingDate: DATE, billingDateKind: 'cancels', hasOpenSubscription: true, hasStripeBilling: true, dailyLimit: 50 },
badge: 'Pro',
- date: 'Cancels on 22/11/2026',
+ date: 'Cancels on 22 Nov 2026',
text: /AI access is active/,
button: 'Manage Subscription',
},
@@ -63,7 +63,7 @@ const CASES: Array<{
props: { billingState: 'payment_failed', billingDate: DATE, billingDateKind: 'grace_ends', hasOpenSubscription: true, hasStripeBilling: true, dailyLimit: 50 },
badge: 'Payment failed',
date: null,
- text: /Payment failed: update your payment method to keep Pro\. Pro access continues until 22\/11\/2026/,
+ text: /Payment failed: update your payment method to keep Pro\. Pro access continues until 22 Nov 2026/,
button: 'Manage Subscription',
},
{
@@ -80,7 +80,7 @@ const CASES: Array<{
props: { billingState: 'subscription_ended', billingDate: DATE, billingDateKind: 'ended', hasStripeBilling: true, dailyLimit: 0 },
badge: 'Expired',
date: null,
- text: /Your Pro subscription ended on 22\/11\/2026\. Resubscribe/,
+ text: /Your Pro subscription ended on 22 Nov 2026\. Resubscribe/,
button: 'Resubscribe',
},
{ name: 'trial ended', props: { billingState: 'trial_ended', dailyLimit: 0 }, badge: 'Expired', date: null, text: /Your access has expired/, button: 'Upgrade to Pro' },
@@ -88,7 +88,7 @@ const CASES: Array<{
name: 'legacy free',
props: { billingState: 'legacy_free', billingDate: DATE, billingDateKind: 'ends', dailyLimit: 50 },
badge: 'Legacy Free',
- date: 'Ends on 22/11/2026',
+ date: 'Ends on 22 Nov 2026',
text: /AI access is active/,
button: 'Upgrade to Pro',
},
diff --git a/src/test/date-format.test.ts b/src/test/date-format.test.ts
new file mode 100644
index 000000000..f8e7a5e21
--- /dev/null
+++ b/src/test/date-format.test.ts
@@ -0,0 +1,35 @@
+import { beforeAll, expect, test } from 'vitest';
+import { loadAllAppLanguages } from '../i18n';
+import { formatDecimal, formatEuropeanDate, formatEuropeanDateTime, formatNumber } from '../lib/date-format';
+
+beforeAll(() => loadAllAppLanguages());
+
+const OCT_23 = new Date(2026, 9, 23, 14, 5);
+
+test('numbers group thousands in the active language, four-digit ones included', () => {
+ expect(`40/${formatNumber(1200, 'en')}`).toBe('40/1,200');
+ expect(`40/${formatNumber(1200, 'es')}`).toBe('40/1.200');
+ expect(`40/${formatNumber(1200, 'hu')}`).toBe('40/1 200');
+ expect(formatNumber(40, 'es')).toBe('40');
+});
+
+test('decimals use the language separator', () => {
+ expect(formatDecimal(1.25, 1, 'en')).toBe('1.3');
+ expect(formatDecimal(1.25, 1, 'es')).toBe('1,3');
+ expect(formatDecimal(1.25, 1, 'hu')).toBe('1,3');
+ expect(formatDecimal(12, 0, 'hu')).toBe('12');
+});
+
+test('dates name the month, so day and month cannot be confused', () => {
+ expect(formatEuropeanDate(OCT_23, undefined, 'en')).toBe('23 Oct 2026');
+ expect(formatEuropeanDate(OCT_23, undefined, 'es')).toBe('23 oct 2026');
+ expect(formatEuropeanDate(OCT_23, undefined, 'hu')).toBe('2026. okt. 23.');
+ expect(formatEuropeanDateTime(OCT_23, undefined, 'en')).toBe('23 Oct 2026, 14:05');
+});
+
+test('a missing date reads as not available in the language', () => {
+ expect(formatEuropeanDate('not a date', undefined, 'en')).toBe('N/A');
+ expect(formatEuropeanDate('not a date', undefined, 'es')).toBe('N/D');
+ expect(formatEuropeanDate('not a date', undefined, 'hu')).toBe('nincs adat');
+ expect(formatEuropeanDate('not a date', '—', 'hu')).toBe('—');
+});
diff --git a/src/test/i18n-untranslated.test.ts b/src/test/i18n-untranslated.test.ts
new file mode 100644
index 000000000..6ad3c560a
--- /dev/null
+++ b/src/test/i18n-untranslated.test.ts
@@ -0,0 +1,97 @@
+import { expect, test } from 'vitest';
+import en from '../locales/en.json';
+import es from '../locales/es.json';
+import hu from '../locales/hu.json';
+import toolsEn from '../locales/tools/en.json';
+import toolsEs from '../locales/tools/es.json';
+import toolsHu from '../locales/tools/hu.json';
+
+/**
+ * Spanish and Hungarian values that are meant to read exactly as in English,
+ * as `language namespace:key`. Anything else identical to English was most
+ * likely never translated. See the glossary in docs/standards/ui.md.
+ */
+const SAME_AS_ENGLISH: Record
= {
+ 'Product, plan and file-format names': [
+ 'es translation:home.title', 'hu translation:home.title',
+ 'es translation:settings.tierPro', 'hu translation:settings.tierPro',
+ 'es translation:pricing.pro', 'hu translation:pricing.pro',
+ 'es translation:pricingModal.pro', 'hu translation:pricingModal.pro',
+ 'es translation:common.pdf', 'hu translation:common.pdf',
+ 'es translation:common.docx', 'hu translation:common.docx',
+ 'es translation:common.googleDocs', 'hu translation:common.googleDocs',
+ 'es translation:sparring.scorecard.exportPdf', 'hu translation:sparring.scorecard.exportPdf',
+ 'es translation:libraryPage.urlSource', 'hu translation:libraryPage.urlSource',
+ 'es translation:login.demoCodePlaceholder', 'hu translation:login.demoCodePlaceholder',
+ ],
+ 'Competition format names stay in English in every language, as on the free tools pages': [
+ 'es translation:timer.formatLD', 'hu translation:timer.formatLD',
+ 'es translation:timer.formatPF', 'hu translation:timer.formatPF',
+ 'es translation:timer.formatPolicy', 'hu translation:timer.formatPolicy',
+ 'es translation:sparring.format.ld', 'hu translation:sparring.format.ld',
+ 'es translation:sparring.format.pf', 'hu translation:sparring.format.pf',
+ 'es translation:debate.formats.bp', 'hu translation:debate.formats.bp',
+ 'es translation:debate.formats.policy', 'hu translation:debate.formats.policy',
+ ],
+ 'Policy speech codes and school-format role codes': [
+ ...['1ac', '1nc', '2ac', '2nc', '1nr', '1ar', '2nr', '2ar'].flatMap((code) => [
+ `es translation:timer.speeches.policy.${code}`, `hu translation:timer.speeches.policy.${code}`,
+ ]),
+ 'hu translation:sparring.turns.standard-3', 'hu translation:sparring.turns.standard-5',
+ ],
+ 'Motion wording and paper size, named as printed on the sheet': [
+ 'es tools:motions.wordingResolved', 'hu tools:motions.wordingResolved',
+ 'hu tools:motions.wordingHouse',
+ 'hu tools:flow.letter',
+ ],
+ 'Exordio, the rhetoric term, and unit abbreviations': [
+ 'es translation:generateResults.exordio', 'hu translation:generateResults.exordio',
+ 'es translation:export.exordio', 'hu translation:export.exordio',
+ 'es translation:generateResults.durationMs', 'hu translation:generateResults.durationMs',
+ 'es translation:crossfire.duration3m', 'es translation:crossfire.duration5m', 'es translation:crossfire.duration8m',
+ ],
+ 'Sparring keeps its name in Spanish, where the word is used as is': [
+ 'es translation:nav.sparring', 'es translation:home.sparring', 'es translation:hero.preview.modeSparring',
+ 'es translation:features.sparring', 'es translation:historyPage.tabSparring', 'es translation:debate.kinds.sparring',
+ ],
+ 'Spanish words spelled the same as the English ones': [
+ 'es translation:common.error', 'es translation:ask.failed', 'es translation:settings.deleteFailed',
+ 'es translation:settings.portalError', 'es translation:admin.error', 'es translation:pricingModal.redeemError',
+ 'es translation:workspaces.original', 'es translation:login.emailTab', 'es translation:generateForm.toneFormal',
+ 'es translation:generateForm.roleIntro', 'es translation:generateForm.roleConc',
+ 'es translation:timer.speeches.standard.intro', 'es translation:timer.speeches.standard.conc',
+ 'es translation:brainstormResults.neutral', 'es translation:debate.side.neutral',
+ 'es translation:askChat.modeGeneral', 'es translation:libraryPage.generalFolder', 'es translation:sourceQuality.types.generic',
+ 'es translation:askChat.pros', 'es translation:historyPage.tabChats', 'es translation:sourceQuality.types.social',
+ 'es translation:debateContext.debateHeading', 'es tools:motions.types.actor',
+ ],
+ 'Hungarian words spelled the same as the English ones': [
+ 'hu tools:motions.topics.sport',
+ ],
+};
+
+function flat(catalog: object, prefix = ''): [string, string][] {
+ return Object.entries(catalog).flatMap(([key, value]): [string, string][] => {
+ const path = prefix + key;
+ return typeof value === 'string' ? [[path, value]] : value && typeof value === 'object' ? flat(value, path + '.') : [];
+ });
+}
+
+function sameAsEnglish(): string[] {
+ const catalogs = { translation: { en, es, hu }, tools: { en: toolsEn, es: toolsEs, hu: toolsHu } };
+ return Object.entries(catalogs).flatMap(([namespace, { en: english, ...others }]) => {
+ const englishValues = new Map(flat(english));
+ return Object.entries(others).flatMap(([language, catalog]) => flat(catalog)
+ // Placeholders, numbers and punctuation alone need no translation.
+ .filter(([key, value]) => value === englishValues.get(key) && /\p{L}{2}/u.test(value.replace(/\{\{[^}]*\}\}/g, '')))
+ .map(([key]) => `${language} ${namespace}:${key}`));
+ });
+}
+
+test('Spanish and Hungarian values match English only where that is intended', () => {
+ const allowed = new Set(Object.values(SAME_AS_ENGLISH).flat());
+ const found = sameAsEnglish();
+ expect(found.filter((entry) => !allowed.has(entry))).toEqual([]);
+ // An entry that no longer matches English has been translated: drop it from the list.
+ expect([...allowed].filter((entry) => !found.includes(entry))).toEqual([]);
+});
diff --git a/src/test/i18n.test.ts b/src/test/i18n.test.ts
index a49e844c1..8361ec836 100644
--- a/src/test/i18n.test.ts
+++ b/src/test/i18n.test.ts
@@ -1,5 +1,7 @@
import { expect, test } from 'vitest';
import i18n, { applyAppLanguage } from '../i18n';
+import { debateCountsLabel } from '../components/features/debate/CurrentDebateStrip';
+import type { AppLanguage } from '../lib/languages';
import en from '../locales/en.json';
import es from '../locales/es.json';
import hu from '../locales/hu.json';
@@ -26,6 +28,21 @@ test('Spanish and Hungarian free-tools catalogs match English keys', () => {
expect(keys(toolsHu).sort()).toEqual(english);
});
+test('count strings take the plural form of their count in every language', async () => {
+ const expected = {
+ en: ['1 source', '2 sources', '1 source (1 active) · 2 outputs'],
+ es: ['1 fuente', '2 fuentes', '1 fuente (1 activa) · 2 resultados'],
+ hu: ['1 forrás', '2 forrás', '1 forrás (1 aktív) · 2 eredmény'],
+ };
+ for (const [language, [one, two, counts]] of Object.entries(expected) as [AppLanguage, string[]][]) {
+ await applyAppLanguage(language);
+ expect(i18n.t('libraryPage.sourceCount', { count: 1 })).toBe(one);
+ expect(i18n.t('libraryPage.sourceCount', { count: 2 })).toBe(two);
+ expect(debateCountsLabel(i18n.t, { sources: 1, activeSources: 1, outputs: 2 })).toBe(counts);
+ }
+ await applyAppLanguage('en');
+});
+
test('a later language choice wins over one whose strings are still loading', async () => {
// Spanish has to be fetched; English is already there and applies at once.
const spanish = applyAppLanguage('es');
From 649f2cd739606a7c440191616898e3c3ecafb746 Mon Sep 17 00:00:00 2001
From: Adam
Date: Fri, 23 Oct 2026 11:47:19 +0000
Subject: [PATCH 2/5] Shorten the ES and HU Ask placeholders to fit 375 px
(IN-15)
At 375 px the Ask input leaves 131 px for text. The new placeholders measure 119 px (ES) and 110 px (HU) in the app font.
Co-Authored-By: Claude Opus 5.5
---
src/locales/es.json | 2 +-
src/locales/hu.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/locales/es.json b/src/locales/es.json
index 393fb08c9..310308562 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -1359,7 +1359,7 @@
"followUp": "Preguntas de seguimiento:",
"pros": "Pros",
"cons": "Contras",
- "placeholder": "Pregunta al coach…",
+ "placeholder": "Pregunta algo…",
"inputLabel": "Mensaje para tu coach de debate",
"placeholderListening": "Escuchando...",
"saveToLibrary": "Guardar en biblioteca",
diff --git a/src/locales/hu.json b/src/locales/hu.json
index dab956f00..13bcf49ad 100644
--- a/src/locales/hu.json
+++ b/src/locales/hu.json
@@ -1359,7 +1359,7 @@
"followUp": "Követő kérdések:",
"pros": "Mellette",
"cons": "Ellene",
- "placeholder": "Kérdezd a vitaedződet…",
+ "placeholder": "Mit kérdeznél?",
"inputLabel": "Üzenet a vitaedződnek",
"placeholderListening": "Hallgatás...",
"saveToLibrary": "Mentés a könyvtárba",
From 90e49851cafc68c037f0df2a595ce195e8bfae31 Mon Sep 17 00:00:00 2001
From: Adam
Date: Fri, 23 Oct 2026 11:56:13 +0000
Subject: [PATCH 3/5] Match the Hungarian flowchart Open button exactly in the
parity spec
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
On mobile, 'Menü megnyitása' also contains 'megnyitás', so the substring match found two buttons.
Co-Authored-By: Claude Opus 5.5
---
e2e/hungarian-parity.spec.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/e2e/hungarian-parity.spec.ts b/e2e/hungarian-parity.spec.ts
index 982dff47f..db7bafb6e 100644
--- a/e2e/hungarian-parity.spec.ts
+++ b/e2e/hungarian-parity.spec.ts
@@ -139,7 +139,7 @@ test.describe('Hungarian parity', () => {
await expect(page.getByRole('alert').filter({ hasText: 'Adj meg egy címet.' })).toBeVisible();
await page.goto('/app/flowchart', { waitUntil: 'domcontentloaded' });
- await page.getByRole('button', { name: 'Megnyitás' }).click();
+ await page.getByRole('button', { name: 'Megnyitás', exact: true }).click();
await expect(page.getByRole('dialog', { name: 'Mentett folyamatábrák' }).getByRole('button', { name: 'Bezárás' })).toBeVisible();
await page.keyboard.press('Escape');
// Deleting a node raises a toast, whose close button is localized too.
From c3ddf907185b70948cca7516dfc1adbe536ec7c2 Mon Sep 17 00:00:00 2001
From: Adam
Date: Fri, 23 Oct 2026 11:57:50 +0000
Subject: [PATCH 4/5] Address review round 1 (T13)
Exempt the English competition format names in the agentic language judge; restore the space after the bullet in History chat rows; hyphenate HU MI- compounds outside the legal pages and use one wording for the AI output language; retranslate onboarding.readyDesc from the current English; localize the admin zero cost; memoize the Library draft word count.
Co-Authored-By: Claude Opus 5.5
---
agentic/pages.e2e.ts | 2 +-
e2e/hungarian-parity.spec.ts | 2 +-
src/locales/es.json | 2 +-
src/locales/hu.json | 38 ++++++++++++++++++------------------
src/pages/AdminPage.tsx | 2 +-
src/pages/HistoryPage.tsx | 2 +-
src/pages/LibraryPage.tsx | 4 ++--
7 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/agentic/pages.e2e.ts b/agentic/pages.e2e.ts
index 446531a29..9be9bf0b3 100644
--- a/agentic/pages.e2e.ts
+++ b/agentic/pages.e2e.ts
@@ -30,7 +30,7 @@ const PUBLIC_PAGES = ["/", "/login", "/terms", "/privacy", "/expocit/sources"];
const NAMES: Record = { en: "English", es: "Spanish", hu: "Hungarian" };
const EXEMPT =
- "Product names such as DebateLab and ExpoCit, AI model names (Claude Sonnet 5), plan names (Pro), debate format codes, language names offered as choices (English, Español, Magyar), and user content do not count. Technical loanwords the app uses on purpose, such as email, chat, prompt and token, count as translated.";
+ "Product names such as DebateLab and ExpoCit, AI model names (Claude Sonnet 5), plan names (Pro), debate format codes and the competition format names kept in English in every language (Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary), language names offered as choices (English, Español, Magyar), and user content do not count. Technical loanwords the app uses on purpose, such as email, chat, prompt and token, count as translated.";
async function checkPage(browser: Browser, agent: Agent, language: Language) {
await expect(browser.locator("html")).toHaveAttribute("lang", language);
diff --git a/e2e/hungarian-parity.spec.ts b/e2e/hungarian-parity.spec.ts
index db7bafb6e..dbf95e3d0 100644
--- a/e2e/hungarian-parity.spec.ts
+++ b/e2e/hungarian-parity.spec.ts
@@ -150,7 +150,7 @@ test.describe('Hungarian parity', () => {
await expect(page.locator('[data-sonner-toast] [data-close-button]')).toHaveAttribute('aria-label', 'Bezárás');
await page.goto('/app/timer', { waitUntil: 'domcontentloaded' });
- await expect(page.getByRole('timer', { name: 'Időmérő kijelző' })).toBeVisible();
+ await expect(page.getByRole('timer', { name: 'Időmérő-kijelző' })).toBeVisible();
expect(errors, errors.join('\n')).toEqual([]);
});
});
diff --git a/src/locales/es.json b/src/locales/es.json
index 310308562..298133ba4 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -1471,7 +1471,7 @@
"onboarding": {
"welcome": "¡Bienvenido a DebateLab! 🎉",
"ready": "¡Estás listo! 🚀",
- "readyDesc": "Explora las otras funciones: Atacar argumentos, Chat con Coach e Investigar datos.",
+ "readyDesc": "Elige el modo que se ajuste a lo que necesitas para la próxima ronda.",
"back": "Atrás",
"close": "Cerrar",
"getStarted": "¡Comenzar!",
diff --git a/src/locales/hu.json b/src/locales/hu.json
index 13bcf49ad..0ae44c2d9 100644
--- a/src/locales/hu.json
+++ b/src/locales/hu.json
@@ -441,9 +441,9 @@
"chat": {},
"settings": {
"title": "Beállítások",
- "aiOutputLanguage": "Nyelv (felület + MI kimenet)",
+ "aiOutputLanguage": "Nyelv (felület + MI-kimenet)",
"uiLanguage": "Felület nyelve",
- "outputLanguage": "MI kimeneti nyelv",
+ "outputLanguage": "MI-kimenet nyelve",
"theme": "Téma",
"light": "Világos",
"dark": "Sötét",
@@ -475,7 +475,7 @@
"signInDemo": "Bejelentkezés: demókód",
"signOut": "Kijelentkezés",
"signOutFailed": "Nem sikerült kijelentkezni. Próbáld újra.",
- "aiModels": "MI modellek",
+ "aiModels": "MI-modellek",
"aiModelsDesc": "A kívánt modellt az egyes funkcióoldalak fejlécében lévő modellválasztóval állíthatod be.",
"admin": "Adminisztráció",
"adminDesc": "Admin irányítópult megnyitása használathoz, felhasználókhoz, limitekhez és promókódokhoz.",
@@ -997,11 +997,11 @@
"proPrice": "$5",
"proPeriod": "/hó + adó",
"recommended": "Ajánlott",
- "trialFeature1": "Kezdő MI felkészülési hozzáférés",
+ "trialFeature1": "Kezdő MI-felkészülési hozzáférés",
"trialFeature2": "Mind a {{modes}} felkészülési mód és a {{utilities}} segédeszköz",
"trialFeature3": "Kezdő modellek",
"trialFeature4": "3 napos hozzáférés",
- "proFeature1": "Bővített MI felkészülési hozzáférés",
+ "proFeature1": "Bővített MI-felkészülési hozzáférés",
"proFeature2": "Mind a {{modes}} felkészülési mód és a {{utilities}} segédeszköz",
"proFeature3": "Mind a 10 modell, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini",
"proFeature4": "Elsőbbségi támogatás",
@@ -1020,7 +1020,7 @@
"q1": "Mennyibe kerül a DebateLab?",
"a1": "{{asOf}} állapot szerint a DebateLab 3 napos ingyenes próbát kínál bankkártya nélkül. Ezután a Pro $5/hó + adó, bővített hozzáféréssel és mind a 10 modellel. Nincs állandó ingyenes csomag.",
"q2": "Mit kapok a Pro-val?",
- "a2": "Bővített MI felkészülési hozzáférés mind a 10 modellel, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.",
+ "a2": "Bővített MI-felkészülési hozzáférés mind a 10 modellel, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini modellekkel, Active Grounding™ kutatással és elsőbbségi támogatással.",
"q3": "Lemondhatom az előfizetésem?",
"a3": "Igen, bármikor lemondhatod a Beállítások oldalon. A számlázási időszak végéig megtartod a hozzáférést.",
"q4": "Az MI kitalál dolgokat?",
@@ -1061,7 +1061,7 @@
},
"deepDive": {
"title": "Active Grounding™ technológia",
- "subtitle": "Hogyan tartjuk őszintén az MI válaszokat.",
+ "subtitle": "Hogyan tartjuk őszintén az MI-válaszokat.",
"step1": "MI szkennelés Exa-val",
"step1Desc": "A neurális keresés releváns, magas minőségű forrásokat talál.",
"step2": "Tudásbázis",
@@ -1400,10 +1400,10 @@
"limitReachedDesc": "Elérted a mai keretet: {{current}} / {{limit}} MI-használatot vettél igénybe. A hozzáférés éjfélkor (UTC) frissül.",
"weightedDesc": "Ez a modell {{units}} használatnak számít, és ma még {{remaining}} maradt. Válassz könnyebb modellt, vagy várj éjfélig (UTC).",
"weightedTooLargeDesc": "Ez a modell {{units}} használatnak számít, ami több, mint a napi {{limit}} kereted. Válassz könnyebb modellt.",
- "weeklyReached": "Elfogyott a heti MI keret",
- "weeklyReachedDesc": "Felhasználtad az e heti MI keretet. Hétfőn 00:00-kor (UTC) frissül. Ha többre van szükséged, írj az ügyfélszolgálatnak.",
+ "weeklyReached": "Elfogyott a heti MI-keret",
+ "weeklyReachedDesc": "Felhasználtad az e heti MI-keretet. Hétfőn 00:00-kor (UTC) frissül. Ha többre van szükséged, írj az ügyfélszolgálatnak.",
"ok": "Rendben",
- "upgradeNote": "Több MI felkészülési hozzáférés + magas szintű érvelés",
+ "upgradeNote": "Több MI-felkészülési hozzáférés + magas szintű érvelés",
"notNow": "Most nem",
"gotIt": "Rendben",
"paymentFailed": "Sikertelen fizetés",
@@ -1438,11 +1438,11 @@
"current": "Jelenlegi",
"recommended": "Ajánlott",
"proPeriod": "/hó + adó",
- "freeFeature1": "Kezdő MI felkészülési hozzáférés",
+ "freeFeature1": "Kezdő MI-felkészülési hozzáférés",
"freeFeature2": "Kezdő modellek",
"freeFeature3": "Minden vitafunkció",
"freeFeature4": "Bankkártya nélkül",
- "proFeature1": "Bővített MI felkészülési hozzáférés",
+ "proFeature1": "Bővített MI-felkészülési hozzáférés",
"proFeature2": "Az összes modell, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini",
"proFeature3": "Mélyreható kutatás Exa-val",
"proFeature4": "Elsőbbségi feldolgozás",
@@ -1471,7 +1471,7 @@
"onboarding": {
"welcome": "Üdvözlünk a DebateLabban! 🎉",
"ready": "Készen állsz! 🚀",
- "readyDesc": "Fedezd fel a többi funkciót: Érvek támadása, Edző chat és Tények kutatása.",
+ "readyDesc": "Válaszd azt a módot, amelyre a következő fordulóhoz szükséged van.",
"back": "Vissza",
"close": "Bezárás",
"getStarted": "Kezdjük!",
@@ -1487,7 +1487,7 @@
},
"timer": {
"title": "Vitaidőmérő",
- "displayLabel": "Időmérő kijelző",
+ "displayLabel": "Időmérő-kijelző",
"speechProgress": "{{speech}} előrehaladása",
"selectFormat": "Formátum választása",
"formatStandard": "Iskolai 4-6-6-3",
@@ -1752,12 +1752,12 @@
"originPlaceholder": "URL vagy hivatkozás...",
"contentPlaceholder": "Illeszd be a forrás tartalmát...",
"emptyTitle": "Még nincsenek források",
- "emptyDesc": "Adj hozzá forrásokat az MI válaszok valós bizonyítékokkal való megalapozásához.",
+ "emptyDesc": "Adj hozzá forrásokat az MI-válaszok valós bizonyítékokkal való megalapozásához.",
"manualEntry": "Kézi bevitel",
"groundingActive": "Megalapozás aktív",
"groundingDisabled": "Megalapozás letiltva",
- "groundingActiveDesc": "Az MI válaszok használják a forrásaidat",
- "groundingDisabledDesc": "Az MI válaszok nincsenek megalapozva",
+ "groundingActiveDesc": "Az MI-válaszok használják a forrásaidat",
+ "groundingDisabledDesc": "Az MI-válaszok nincsenek megalapozva",
"clearFailed": "Nem sikerült törölni a forrásokat",
"clearConfirm": "Biztosan törölni szeretnéd az összes forrást?",
"importUrlHelp": "Az automatikus importálás szerkeszthető piszkozatot hoz létre. Nézd át, mielőtt a könyvtárba mented.",
@@ -1994,7 +1994,7 @@
"sparringTimeUsed": "Felhasznált idő",
"sparringTotals": "Végeredmény",
"sparringUser": "Te",
- "sparringOpponent": "MI ellenfél",
+ "sparringOpponent": "MI-ellenfél",
"sparringWinner": "Győztes",
"sparringTie": "Döntetlen",
"sparringScore": "Pontszám",
@@ -2016,7 +2016,7 @@
"pro": "Mellette",
"con": "Ellene",
"user": "Te",
- "opponent": "MI ellenfél",
+ "opponent": "MI-ellenfél",
"format": {
"standard": "Iskolai 4-6-6-3",
"ld": "Lincoln-Douglas",
diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx
index b49147034..7d44329b7 100644
--- a/src/pages/AdminPage.tsx
+++ b/src/pages/AdminPage.tsx
@@ -467,7 +467,7 @@ export default function AdminPage() {
}
if (cost > 0) return `$${formatDecimal(cost, 4)}`;
if (unknownCostRequests > 0) return t('admin.dashboard.unknownCost');
- return '$0.0000';
+ return `$${formatDecimal(0, 4)}`;
};
const fetchAdminData = useCallback(async (action: string, params?: Record) => {
diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx
index 424435430..a1cdf5b99 100644
--- a/src/pages/HistoryPage.tsx
+++ b/src/pages/HistoryPage.tsx
@@ -239,7 +239,7 @@ export default function HistoryPage() {
{item.title}
-
{t('askChat.messageCount', { count: item.data.length })} •{formatEuropeanDateTime(item.createdAt)}
+
{t('askChat.messageCount', { count: item.data.length })} • {formatEuropeanDateTime(item.createdAt)}
handleLoadChat(item.id)}>
{t('historyPage.resume')}
diff --git a/src/pages/LibraryPage.tsx b/src/pages/LibraryPage.tsx
index 9e3b50535..8819f13da 100644
--- a/src/pages/LibraryPage.tsx
+++ b/src/pages/LibraryPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DebateRequiredError, DuplicateSourceError, isPendingSource, useSessionStore, type Source } from '@/lib/stores/session-store';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -250,7 +250,7 @@ export default function LibraryPage() {
folder: 'General',
isActive: true,
});
- const draftWordCount = sourceDraft.content.trim().split(/\s+/).filter(Boolean).length;
+ const draftWordCount = useMemo(() => sourceDraft.content.trim().split(/\s+/).filter(Boolean).length, [sourceDraft.content]);
const [importPreview, setImportPreview] = useState<{
title: string;
content: string;
From 884e31f958fea12e22b4185ca590c3235e517446 Mon Sep 17 00:00:00 2001
From: Adam
Date: Fri, 23 Oct 2026 12:10:29 +0000
Subject: [PATCH 5/5] Address review round 2 (T13)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Hyphenate the remaining HU MI- and vitapárbaj-forduló compounds, join Esetstruktúra and Esettámadás-elemzés, use Exával, say Megnyitás and időmérő in the flowchart dialog and Sparring round, cache formatDecimal formatters, and note in the glossary that the Timer spells out R1 and R2.
Co-Authored-By: Claude Opus 5.5
---
docs/standards/ui.md | 2 +-
src/lib/date-format.ts | 10 +++++++++-
src/locales/hu.json | 34 +++++++++++++++++-----------------
3 files changed, 27 insertions(+), 19 deletions(-)
diff --git a/docs/standards/ui.md b/docs/standards/ui.md
index 898f7a5b6..17001bb41 100644
--- a/docs/standards/ui.md
+++ b/docs/standards/ui.md
@@ -20,7 +20,7 @@ Part of the [coding standards](../../CODING_STANDARDS.md). [docs/architecture.md
| Case | Case | Caso | Eset (Vitaeset in titles) |
| Ruthless (difficulty, intensity) | Ruthless | Despiadado | Kíméletlen |
| School format | School 4-6-6-3 | Formato escolar 4-6-6-3 | Iskolai 4-6-6-3 |
-| School-format roles | Introductor, R1, R2, Conclusor | Introductor, R1, R2, Conclusor | Bevezető, R1, R2, Záró |
+| School-format roles (the Timer spells out R1 and R2) | Introductor, R1, R2, Conclusor | Introductor, R1, R2, Conclusor | Bevezető, R1, R2, Záró |
| Sides in a Sparring turn | Pro, Con | a favor, en contra | Pro, Kontra |
| Competition formats | Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary | same as English | same as English |
| Load (button) / Loading | Load / Loading | Cargar / Cargando | Megnyitás / Betöltés |
diff --git a/src/lib/date-format.ts b/src/lib/date-format.ts
index ed6bc7512..480ef951c 100644
--- a/src/lib/date-format.ts
+++ b/src/lib/date-format.ts
@@ -99,9 +99,17 @@ export function formatNumber(value: number, language: AppLanguage = activeLangua
return FORMATTERS[language].number.format(value);
}
+const DECIMAL_FORMATTERS = new Map();
+
/** A number with exactly `digits` decimals in the language's notation: "1.5" in English, "1,5" in Spanish and Hungarian. */
export function formatDecimal(value: number, digits: number, language: AppLanguage = activeLanguage()): string {
- return new Intl.NumberFormat(toIntlLocale(language), { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
+ const key = `${language}:${digits}`;
+ let formatter = DECIMAL_FORMATTERS.get(key);
+ if (!formatter) {
+ formatter = new Intl.NumberFormat(toIntlLocale(language), { minimumFractionDigits: digits, maximumFractionDigits: digits });
+ DECIMAL_FORMATTERS.set(key, formatter);
+ }
+ return formatter.format(value);
}
export function formatRelativeDate(
diff --git a/src/locales/hu.json b/src/locales/hu.json
index 0ae44c2d9..df2234266 100644
--- a/src/locales/hu.json
+++ b/src/locales/hu.json
@@ -1051,7 +1051,7 @@
"sub5Title": "Keresztkérdezési gyakorlás",
"sub5Desc": "Gyakorold a célzott kérdéseket és válaszokat időzített párbeszédben.",
"sub5Badge": "Új eszköz",
- "sub6Title": "Vitapárbaj fordulók",
+ "sub6Title": "Vitapárbaj-fordulók",
"sub6Desc": "Gyakorolj egy időzített vitafordulót, majd nézd át az értékelőlapot.",
"sub6Badge": "Új eszköz",
"sub1Badge": "Számlázás",
@@ -1062,12 +1062,12 @@
"deepDive": {
"title": "Active Grounding™ technológia",
"subtitle": "Hogyan tartjuk őszintén az MI-válaszokat.",
- "step1": "MI szkennelés Exa-val",
+ "step1": "MI-szkennelés Exával",
"step1Desc": "A neurális keresés releváns, magas minőségű forrásokat talál.",
"step2": "Tudásbázis",
"step2Desc": "A forrásaid vektorizálva vannak szemantikus egyeztetéshez.",
"step3": "Kontextus injektálás",
- "step3Desc": "Az ellenőrzött tények bekerülnek az MI promptba.",
+ "step3Desc": "Az ellenőrzött tények bekerülnek az MI-promptba.",
"step4": "Megalapozott kimenet",
"step4Desc": "A válaszok valós forrásokat idéznek, nem hallucinációkat.",
"preview": {
@@ -1134,7 +1134,7 @@
"exordioDesc": "A meggyőző nyitásod",
"hook": "Felvezetés",
"transition": "Átvezetés",
- "caseStructure": "Eset struktúra",
+ "caseStructure": "Esetstruktúra",
"caseStructureDesc": "Az érvelési kereted",
"thesis": "Tézis",
"framework": "Keretrendszer",
@@ -1143,7 +1143,7 @@
"noDefinitions": "Nincsenek megadott definíciók",
"arguments": "Érvek",
"structuredArguments": "strukturált érv",
- "predictedOpponent": "Várható ellenfél eset",
+ "predictedOpponent": "Az ellenfél várható esete",
"predictedOpponentDesc": "Amit valószínűleg érvelni fognak",
"likelyArguments": "Valószínű érvek",
"strategicWeaknesses": "Stratégiai gyengeségek",
@@ -1444,7 +1444,7 @@
"freeFeature4": "Bankkártya nélkül",
"proFeature1": "Bővített MI-felkészülési hozzáférés",
"proFeature2": "Az összes modell, köztük a Claude Opus és Sonnet, a GPT-6.1 Sol és a Gemini",
- "proFeature3": "Mélyreható kutatás Exa-val",
+ "proFeature3": "Mélyreható kutatás Exával",
"proFeature4": "Elsőbbségi feldolgozás",
"proFeature5": "Minden vitafunkció",
"redirecting": "Átirányítás...",
@@ -1558,7 +1558,7 @@
"saveChanges": "Változások mentése",
"saveFlowchart": "Folyamatábra mentése",
"savedFlowcharts": "Mentett folyamatábrák",
- "savedFlowchartsDesc": "Tölts be egy korábban mentett folyamatábrát",
+ "savedFlowchartsDesc": "Nyiss meg egy korábban mentett folyamatábrát",
"saveFlowchartDialog": "Folyamatábra mentése",
"saveFlowchartDialogDesc": "Adj nevet a folyamatábrádnak a mentéshez",
"editNode": "Csomópont szerkesztése",
@@ -1666,16 +1666,16 @@
"attackCase": "Eset támadása",
"brainstormIdeas": "Ötletelés",
"findEvidence": "Bizonyíték keresése",
- "sparringTitle": "Vitapárbaj fordulók",
- "emptySparring": "Még nincs vitapárbaj forduló",
+ "sparringTitle": "Vitapárbaj-fordulók",
+ "emptySparring": "Még nincs vitapárbaj-forduló",
"emptySparringDesc": "Fejezz be egy időzített vitafordulót, hogy itt átnézhesd.",
"startSparring": "Vitapárbaj indítása",
"score": "Te {{user}} - MI {{ai}}",
- "deleteSparringAria": "Vitapárbaj forduló törlése: {{motion}}",
- "deleteSparringTitle": "Törlöd a vitapárbaj fordulót?",
+ "deleteSparringAria": "Vitapárbaj-forduló törlése: {{motion}}",
+ "deleteSparringTitle": "Törlöd a vitapárbaj-fordulót?",
"deleteSparringDesc": "Ez eltávolítja az értékelőlapot és az átiratot az előzményeidből.",
- "sparringDeleted": "A vitapárbaj forduló törölve.",
- "sparringDeleteFailed": "Nem sikerült törölni a vitapárbaj fordulót.",
+ "sparringDeleted": "A vitapárbaj-forduló törölve.",
+ "sparringDeleteFailed": "Nem sikerült törölni a vitapárbaj-fordulót.",
"deleteCaseLabel": "{{title}} törlése",
"load": "Megnyitás",
"resume": "Folytatás",
@@ -1957,7 +1957,7 @@
"side": "Oldal",
"proposition": "Mellette",
"opposition": "Ellene",
- "caseAttack": "Eset támadás elemzés",
+ "caseAttack": "Esettámadás-elemzés",
"overallScore": "Összesített pontszám",
"whyWeak": "Miért gyenge",
"howOpponentAttacks": "Hogyan támadja az ellenfél",
@@ -1974,13 +1974,13 @@
"source": "Forrás",
"noFactoids": "Nem találhatók tények.",
"exordio": "Exordio",
- "caseStructure": "Eset struktúra",
+ "caseStructure": "Esetstruktúra",
"thesis": "Tézis",
"framework": "Keretrendszer",
"burdenOfProof": "Bizonyítási teher",
"keyDefinitions": "Kulcsdefiníciók",
"arguments": "Érvek",
- "predictedOpponent": "Várható ellenfél eset",
+ "predictedOpponent": "Az ellenfél várható esete",
"counters": "Előkészített ellenérvek",
"weaknesses": "Azonosított gyengeségek",
"fallacies": "Észlelt téves következtetések",
@@ -2111,7 +2111,7 @@
"preparingResult": "Eredmény előkészítése",
"judgeReading": "A bíró olvassa az átiratot.",
"scoring": "Forduló értékelése",
- "noTimer": "Értékelés közben nincs óra",
+ "noTimer": "Értékelés közben nincs időmérő",
"timeRemaining": "{{time}} van hátra",
"pauseClock": "Óra szüneteltetése",
"resumeClock": "Óra folytatása",