From 3524b13614e8671854ac0db2e53920717cc2cc58 Mon Sep 17 00:00:00 2001 From: dev-Flyblue <1077373674@qq.com> Date: Sun, 15 Mar 2026 13:12:49 +0800 Subject: [PATCH 1/6] feat(i18n): add zh-CN translations and populate en/es locale files Extract all user-visible hardcoded strings into 7 namespace locale files (common, launch, editor, timeline, settings, dialogs, shortcuts) for all three languages: English (source of truth), Spanish, and Simplified Chinese. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/locales/en/common.json | 23 +++++-- src/i18n/locales/en/dialogs.json | 62 +++++++++++++++++- src/i18n/locales/en/editor.json | 92 ++++++++++++++++++++++++++- src/i18n/locales/en/launch.json | 37 ++++++++++- src/i18n/locales/en/settings.json | 61 +++++++++++++++++- src/i18n/locales/en/shortcuts.json | 17 ++++- src/i18n/locales/en/timeline.json | 39 +++++++++++- src/i18n/locales/es/common.json | 23 +++++-- src/i18n/locales/es/dialogs.json | 62 +++++++++++++++++- src/i18n/locales/es/editor.json | 92 ++++++++++++++++++++++++++- src/i18n/locales/es/launch.json | 37 ++++++++++- src/i18n/locales/es/settings.json | 61 +++++++++++++++++- src/i18n/locales/es/shortcuts.json | 17 ++++- src/i18n/locales/es/timeline.json | 39 +++++++++++- src/i18n/locales/zh-CN/common.json | 19 +++++- src/i18n/locales/zh-CN/dialogs.json | 62 +++++++++++++++++- src/i18n/locales/zh-CN/editor.json | 92 ++++++++++++++++++++++++++- src/i18n/locales/zh-CN/launch.json | 37 ++++++++++- src/i18n/locales/zh-CN/settings.json | 61 +++++++++++++++++- src/i18n/locales/zh-CN/shortcuts.json | 17 ++++- src/i18n/locales/zh-CN/timeline.json | 39 +++++++++++- 21 files changed, 958 insertions(+), 31 deletions(-) diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 42341541..c661e479 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -1,7 +1,18 @@ { - "app": { - "name": "Recordly", - "editorTitle": "Recordly Editor", - "subtitle": "Screen recording and editing" - } -} \ No newline at end of file + "app": { + "name": "Recordly", + "editorTitle": "Recordly Editor", + "subtitle": "Screen recording and editing" + }, + "actions": { + "cancel": "Cancel", + "save": "Save", + "delete": "Delete", + "done": "Done" + }, + "errors": { + "invalidFileType": "Invalid file type", + "failedToUploadImage": "Failed to upload image", + "fileReadError": "There was an error reading the file." + } +} diff --git a/src/i18n/locales/en/dialogs.json b/src/i18n/locales/en/dialogs.json index 9e26dfee..0fa405e0 100644 --- a/src/i18n/locales/en/dialogs.json +++ b/src/i18n/locales/en/dialogs.json @@ -1 +1,61 @@ -{} \ No newline at end of file +{ + "export": { + "pleaseTryAgain": "Please try again", + "compilingGifProgress": "Compiling GIF... {{progress}}%", + "compilingGifWait": "Compiling GIF... This may take a while", + "takeMoment": "This may take a moment...", + "exportFailed": "Export Failed", + "compilingGifTitle": "Compiling GIF", + "exportingFormat": "Exporting {{format}}", + "exportComplete": "Export Complete", + "formatReady": "Your {{format}} is ready", + "showInFolder": "Show in Folder", + "compiling": "Compiling", + "renderingFrames": "Rendering Frames", + "processing": "Processing...", + "status": "Status", + "format": "Format", + "compilingStatus": "Compiling...", + "frames": "Frames", + "cancelExport": "Cancel Export", + "savedSuccess": "{{format}} saved successfully!" + }, + "addFont": { + "title": "Add Google Font", + "heading": "Add Google Font", + "description": "Add a custom font from Google Fonts to use in your annotations.", + "urlLabel": "Google Fonts Import URL", + "urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap", + "urlHelp": "Get this from Google Fonts: Select a font → Click \"Get font\" → Copy the @import URL", + "nameLabel": "Display Name", + "namePlaceholder": "My Custom Font", + "nameHelp": "This is how the font will appear in the font selector", + "adding": "Adding...", + "addFont": "Add Font", + "enterUrl": "Please enter a Google Fonts import URL", + "invalidUrl": "Please enter a valid Google Fonts URL", + "enterName": "Please enter a font name", + "extractFailed": "Could not extract font family from URL", + "addSuccess": "Font \"{{name}}\" added successfully", + "addFailed": "Failed to add font", + "loadTimeout": "Font took too long to load. Please check the URL and try again.", + "loadFailed": "The font could not be loaded. Please verify the Google Fonts URL is correct." + }, + "shortcutsConfig": { + "title": "Keyboard Shortcuts", + "configurable": "Configurable", + "fixed": "Fixed", + "pressEscToCancel": "Press Esc to cancel", + "clickToChange": "Click to change", + "pressAKey": "Press a key…", + "alreadyUsedBy": "Already used by {{action}}", + "swap": "Swap", + "reserved": "This shortcut is reserved for \"{{label}}\" and cannot be reassigned.", + "saved": "Keyboard shortcuts saved", + "resetNotice": "Reset to default shortcuts — click Save to apply", + "instructions": "Click a shortcut then press the new key combination. Press Esc to cancel.", + "resetToDefaults": "Reset to defaults", + "cancel": "Cancel", + "save": "Save" + } +} diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index 9e26dfee..4cb31ab3 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -1 +1,91 @@ -{} \ No newline at end of file +{ + "playback": { + "play": "Play", + "pause": "Pause" + }, + "annotations": { + "settings": "Annotation Settings", + "active": "Active", + "text": "Text", + "image": "Image", + "arrow": "Arrow", + "textContent": "Text Content", + "textPlaceholder": "Enter your text...", + "fontStyle": "Font Style", + "selectStyle": "Select style", + "size": "Size", + "toggleBold": "Toggle bold", + "toggleItalic": "Toggle italic", + "toggleUnderline": "Toggle underline", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "textColor": "Text Color", + "background": "Background", + "none": "None", + "clearBackground": "Clear Background", + "uploadImage": "Upload Image", + "supportedFormats": "Supported formats: JPG, PNG, GIF, WebP", + "arrowDirection": "Arrow Direction", + "strokeWidth": "Stroke Width: {{width}}px", + "arrowColor": "Arrow Color", + "deleteAnnotation": "Delete Annotation", + "shortcutsAndTips": "Shortcuts & Tips", + "tipSelectAnnotation": "Move playhead to overlapping annotation section and select an item.", + "tipCycleForward": "Use Tab to cycle through overlapping items.", + "tipCycleBackward": "Use Shift+Tab to cycle backwards.", + "imageUploadSuccess": "Image uploaded successfully!", + "imageUploadError": "Please upload a JPG, PNG, GIF, or WebP image file." + }, + "fontStyles": { + "classic": "Classic", + "editor": "Editor", + "strong": "Strong", + "typewriter": "Typewriter", + "deco": "Deco", + "simple": "Simple", + "modern": "Modern", + "clean": "Clean" + }, + "format": { + "mp4Video": "MP4 Video", + "mp4Description": "High quality video file", + "gifAnimation": "GIF Animation", + "gifDescription": "Animated image for sharing" + }, + "gifOptions": { + "frameRate": "Frame Rate", + "outputSize": "Output Size", + "outputDimensions": "Output: {{width}} × {{height}}px", + "loopAnimation": "Loop Animation", + "loopDescription": "GIF will play continuously" + }, + "tutorial": { + "howTrimmingWorks": "How trimming works", + "title": "How Trimming Works", + "understanding": "Understanding how to cut out unwanted parts of your video.", + "descriptionP1": "The Trim tool works by defining the segments you want to", + "descriptionRemove": "remove", + "descriptionP2": "from your video.", + "descriptionP3": "Any part of the timeline that is covered by a red trim segment will be cut out when you export.", + "visualExample": "Visual Example", + "removed": "REMOVED", + "kept": "Kept", + "finalVideo": "Final Video", + "part": "Part {{number}}", + "addTrimStep": "1. Add Trim", + "addTrimDesc": "Press T or click the scissors icon to mark a section for removal.", + "adjustStep": "2. Adjust", + "adjustDesc": "Drag the edges of the red region to cover exactly what you want to cut out." + }, + "keyboardShortcuts": { + "title": "Keyboard Shortcuts", + "customizeTooltip": "Customize shortcuts", + "customize": "Customize", + "panTimeline": "Pan Timeline", + "zoomTimeline": "Zoom Timeline", + "cycleAnnotations": "Cycle Annotations", + "tab": "Tab" + }, + "openRecordingsFolder": "Open recordings folder" +} diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json index 9e26dfee..4829693f 100644 --- a/src/i18n/locales/en/launch.json +++ b/src/i18n/locales/en/launch.json @@ -1 +1,36 @@ -{} \ No newline at end of file +{ + "recording": { + "disableSystemAudio": "Disable system audio", + "enableSystemAudio": "Enable system audio", + "disableMicrophone": "Disable microphone", + "enableMicrophone": "Enable microphone", + "record": "Record", + "recordingFolder": "Recording folder: {{path}}", + "chooseRecordingsFolder": "Choose recordings folder", + "folderPath": "Path: /{{name}}/", + "openVideoFile": "Open video file", + "openProject": "Open project", + "hideHud": "Hide HUD", + "closeApp": "Close App" + }, + "sourceSelector": { + "loadingSources": "Loading sources...", + "screens": "Screens", + "windows": "Windows", + "windowsNote": "Only visible (non-minimized) windows can be recorded.", + "windowPlaceholder": "Window", + "cancel": "Cancel", + "share": "Share" + }, + "permissions": { + "screenRecordingNeeded": "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly.", + "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.", + "accessibilityNeeded": "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly.", + "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.", + "selectSource": "Please select a source to record", + "systemAudioUnavailable": "System audio is not available for this source. Recording will continue without system audio.", + "microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.", + "failedToStart": "Failed to start recording: {{error}}", + "failedToStartGeneric": "Failed to start recording" + } +} diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 9e26dfee..2405ce5f 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -1 +1,60 @@ -{} \ No newline at end of file +{ + "zoom": { + "level": "Zoom Level", + "selectRegion": "Select a zoom region to adjust", + "deleteZoom": "Delete Zoom" + }, + "trim": { + "deleteRegion": "Delete Trim Region" + }, + "speed": { + "playbackSpeed": "Playback Speed", + "selectRegion": "Select a speed region to adjust", + "deleteRegion": "Delete Speed Region" + }, + "effects": { + "title": "Video Effects", + "showCursor": "Show Cursor", + "loopCursor": "Loop cursor", + "backgroundBlur": "Background Blur", + "zoomMotionBlur": "Zoom Motion Blur", + "connectZooms": "Connect Zooms", + "cursorSize": "Cursor Size", + "cursorSmoothing": "Cursor Smoothing", + "off": "Off", + "cursorMotionBlur": "Cursor Motion Blur", + "cursorClickBounce": "Cursor Click Bounce", + "shadow": "Shadow", + "roundness": "Roundness", + "padding": "Padding" + }, + "crop": { + "title": "Crop Video", + "instruction": "Drag on each side to adjust the crop area" + }, + "background": { + "title": "Background", + "image": "Image", + "color": "Color", + "gradient": "Gradient", + "uploadCustom": "Upload Custom", + "uploadSuccess": "Custom image uploaded successfully!", + "uploadError": "Please upload a JPG or JPEG image file." + }, + "export": { + "mp4": "MP4", + "gif": "GIF", + "quality": { + "low": "Low", + "medium": "Medium", + "high": "High" + }, + "loop": "Loop", + "outputDimensions": "Output: {{dimensions}}px", + "loadProject": "Load Project", + "saveProject": "Save Project", + "exportVideo": "Export {{format}}", + "reportBug": "Report Bug", + "starOnGithub": "Star on GitHub" + } +} diff --git a/src/i18n/locales/en/shortcuts.json b/src/i18n/locales/en/shortcuts.json index 9e26dfee..dd789d98 100644 --- a/src/i18n/locales/en/shortcuts.json +++ b/src/i18n/locales/en/shortcuts.json @@ -1 +1,16 @@ -{} \ No newline at end of file +{ + "actions": { + "addZoom": "Add Zoom", + "addTrim": "Add Trim", + "addSpeed": "Add Speed", + "addAnnotation": "Add Annotation", + "addKeyframe": "Add Keyframe", + "deleteSelected": "Delete Selected", + "playPause": "Play / Pause", + "cycleForward": "Cycle Annotations Forward", + "cycleBackward": "Cycle Annotations Backward", + "deleteSelectedAlt": "Delete Selected (alt)", + "panTimeline": "Pan Timeline", + "zoomTimeline": "Zoom Timeline" + } +} diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json index 9e26dfee..23be3abb 100644 --- a/src/i18n/locales/en/timeline.json +++ b/src/i18n/locales/en/timeline.json @@ -1 +1,38 @@ -{} \ No newline at end of file +{ + "zoom": { + "cannotPlace": "Cannot place zoom here", + "existsOrNoSpace": "Zoom already exists at this location or not enough space available.", + "suggestHandlerUnavailable": "Zoom suggestion handler unavailable", + "noTelemetry": "No cursor telemetry available", + "recordFirst": "Record a screencast first to generate cursor-based suggestions.", + "noUsableTelemetry": "No usable cursor telemetry", + "notEnoughMovement": "The recording does not include enough cursor movement data.", + "noInteractionMoments": "No clear interaction moments found", + "tryRecording": "Try a recording with pauses or clicks around important actions.", + "noAutoZoomSlots": "No auto-zoom slots available", + "dwellPointsOverlap": "Detected dwell points overlap existing zoom regions.", + "addedSuggestions": "Added {{count}} interaction-based zoom suggestion(s)", + "label": "Zoom {{index}}", + "addZoom": "Add Zoom (Z)", + "suggestZooms": "Suggest Zooms from Cursor" + }, + "trim": { + "cannotPlace": "Cannot place trim here", + "existsOrNoSpace": "Trim already exists at this location or not enough space available.", + "label": "Trim {{index}}", + "addTrim": "Add Trim (T)" + }, + "speed": { + "cannotPlace": "Cannot place speed here", + "existsOrNoSpace": "Speed region already exists at this location or not enough space available.", + "label": "Speed" + }, + "annotation": { + "label": "Annotation", + "image": "Image", + "addAnnotation": "Add Annotation (A)" + }, + "addSpeed": "Add Speed (S)", + "resizeLeft": "Resize left", + "resizeRight": "Resize right" +} diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index f5f8d728..92020782 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -1,7 +1,18 @@ { - "app": { - "name": "Recordly", - "editorTitle": "Editor de Recordly", - "subtitle": "Grabacion de pantalla y edicion" - } -} \ No newline at end of file + "app": { + "name": "Recordly", + "editorTitle": "Editor de Recordly", + "subtitle": "Grabación de pantalla y edición" + }, + "actions": { + "cancel": "Cancelar", + "save": "Guardar", + "delete": "Eliminar", + "done": "Hecho" + }, + "errors": { + "invalidFileType": "Tipo de archivo inválido", + "failedToUploadImage": "Error al subir la imagen", + "fileReadError": "Hubo un error al leer el archivo." + } +} diff --git a/src/i18n/locales/es/dialogs.json b/src/i18n/locales/es/dialogs.json index 9e26dfee..283682b5 100644 --- a/src/i18n/locales/es/dialogs.json +++ b/src/i18n/locales/es/dialogs.json @@ -1 +1,61 @@ -{} \ No newline at end of file +{ + "export": { + "pleaseTryAgain": "Por favor inténtalo de nuevo", + "compilingGifProgress": "Compilando GIF... {{progress}}%", + "compilingGifWait": "Compilando GIF... Esto puede tardar un momento", + "takeMoment": "Esto puede tardar un momento...", + "exportFailed": "Error en la exportación", + "compilingGifTitle": "Compilando GIF", + "exportingFormat": "Exportando {{format}}", + "exportComplete": "Exportación completada", + "formatReady": "Tu {{format}} está listo", + "showInFolder": "Mostrar en carpeta", + "compiling": "Compilando", + "renderingFrames": "Renderizando cuadros", + "processing": "Procesando...", + "status": "Estado", + "format": "Formato", + "compilingStatus": "Compilando...", + "frames": "Cuadros", + "cancelExport": "Cancelar exportación", + "savedSuccess": "¡{{format}} guardado exitosamente!" + }, + "addFont": { + "title": "Agregar fuente de Google", + "heading": "Agregar fuente de Google", + "description": "Agrega una fuente personalizada de Google Fonts para usar en tus anotaciones.", + "urlLabel": "URL de importación de Google Fonts", + "urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap", + "urlHelp": "Obtén esto de Google Fonts: Selecciona una fuente → Haz clic en \"Obtener fuente\" → Copia la URL de @import", + "nameLabel": "Nombre para mostrar", + "namePlaceholder": "Mi fuente personalizada", + "nameHelp": "Así aparecerá la fuente en el selector de fuentes", + "adding": "Agregando...", + "addFont": "Agregar fuente", + "enterUrl": "Por favor ingresa una URL de importación de Google Fonts", + "invalidUrl": "Por favor ingresa una URL válida de Google Fonts", + "enterName": "Por favor ingresa un nombre de fuente", + "extractFailed": "No se pudo extraer el nombre de la familia de fuentes de la URL", + "addSuccess": "Fuente \"{{name}}\" agregada exitosamente", + "addFailed": "Error al agregar la fuente", + "loadTimeout": "La fuente tardó demasiado en cargar. Por favor verifica la URL e inténtalo de nuevo.", + "loadFailed": "No se pudo cargar la fuente. Por favor verifica que la URL de Google Fonts sea correcta." + }, + "shortcutsConfig": { + "title": "Atajos de teclado", + "configurable": "Configurable", + "fixed": "Fijo", + "pressEscToCancel": "Presiona Esc para cancelar", + "clickToChange": "Haz clic para cambiar", + "pressAKey": "Presiona una tecla…", + "alreadyUsedBy": "Ya está en uso por {{action}}", + "swap": "Intercambiar", + "reserved": "Este atajo está reservado para \"{{label}}\" y no se puede reasignar.", + "saved": "Atajos de teclado guardados", + "resetNotice": "Restablecer a atajos predeterminados — haz clic en Guardar para aplicar", + "instructions": "Haz clic en un atajo y luego presiona la nueva combinación de teclas. Presiona Esc para cancelar.", + "resetToDefaults": "Restablecer valores predeterminados", + "cancel": "Cancelar", + "save": "Guardar" + } +} diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index 9e26dfee..32d71924 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -1 +1,91 @@ -{} \ No newline at end of file +{ + "playback": { + "play": "Reproducir", + "pause": "Pausar" + }, + "annotations": { + "settings": "Configuración de anotaciones", + "active": "Activo", + "text": "Texto", + "image": "Imagen", + "arrow": "Flecha", + "textContent": "Contenido de texto", + "textPlaceholder": "Ingresa tu texto...", + "fontStyle": "Estilo de fuente", + "selectStyle": "Seleccionar estilo", + "size": "Tamaño", + "toggleBold": "Alternar negrita", + "toggleItalic": "Alternar cursiva", + "toggleUnderline": "Alternar subrayado", + "alignLeft": "Alinear a la izquierda", + "alignCenter": "Alinear al centro", + "alignRight": "Alinear a la derecha", + "textColor": "Color de texto", + "background": "Fondo", + "none": "Ninguno", + "clearBackground": "Borrar fondo", + "uploadImage": "Subir imagen", + "supportedFormats": "Formatos compatibles: JPG, PNG, GIF, WebP", + "arrowDirection": "Dirección de flecha", + "strokeWidth": "Ancho de trazo: {{width}}px", + "arrowColor": "Color de flecha", + "deleteAnnotation": "Eliminar anotación", + "shortcutsAndTips": "Atajos y consejos", + "tipSelectAnnotation": "Mueve el cabezal de reproducción a la sección de anotación superpuesta y selecciona un elemento.", + "tipCycleForward": "Usa Tab para recorrer los elementos superpuestos.", + "tipCycleBackward": "Usa Shift+Tab para recorrer hacia atrás.", + "imageUploadSuccess": "¡Imagen subida exitosamente!", + "imageUploadError": "Por favor sube un archivo de imagen JPG, PNG, GIF o WebP." + }, + "fontStyles": { + "classic": "Clásico", + "editor": "Editor", + "strong": "Fuerte", + "typewriter": "Máquina de escribir", + "deco": "Deco", + "simple": "Simple", + "modern": "Moderno", + "clean": "Limpio" + }, + "format": { + "mp4Video": "Video MP4", + "mp4Description": "Archivo de video de alta calidad", + "gifAnimation": "Animación GIF", + "gifDescription": "Imagen animada para compartir" + }, + "gifOptions": { + "frameRate": "Velocidad de cuadros", + "outputSize": "Tamaño de salida", + "outputDimensions": "Salida: {{width}} × {{height}}px", + "loopAnimation": "Animación en bucle", + "loopDescription": "El GIF se reproducirá continuamente" + }, + "tutorial": { + "howTrimmingWorks": "Cómo funciona el recorte", + "title": "Cómo Funciona el Recorte", + "understanding": "Comprende cómo eliminar las partes no deseadas de tu video.", + "descriptionP1": "La herramienta de recorte funciona definiendo los segmentos que deseas", + "descriptionRemove": "eliminar", + "descriptionP2": "de tu video.", + "descriptionP3": "Cualquier parte de la línea de tiempo cubierta por un segmento de recorte rojo será eliminada al exportar.", + "visualExample": "Ejemplo visual", + "removed": "ELIMINADO", + "kept": "Conservado", + "finalVideo": "Video final", + "part": "Parte {{number}}", + "addTrimStep": "1. Agregar recorte", + "addTrimDesc": "Presiona T o haz clic en el ícono de tijeras para marcar una sección para eliminar.", + "adjustStep": "2. Ajustar", + "adjustDesc": "Arrastra los bordes de la región roja para cubrir exactamente lo que deseas eliminar." + }, + "keyboardShortcuts": { + "title": "Atajos de teclado", + "customizeTooltip": "Personalizar atajos", + "customize": "Personalizar", + "panTimeline": "Desplazar línea de tiempo", + "zoomTimeline": "Zoom en línea de tiempo", + "cycleAnnotations": "Recorrer anotaciones", + "tab": "Tab" + }, + "openRecordingsFolder": "Abrir carpeta de grabaciones" +} diff --git a/src/i18n/locales/es/launch.json b/src/i18n/locales/es/launch.json index 9e26dfee..5fd0e6db 100644 --- a/src/i18n/locales/es/launch.json +++ b/src/i18n/locales/es/launch.json @@ -1 +1,36 @@ -{} \ No newline at end of file +{ + "recording": { + "disableSystemAudio": "Desactivar audio del sistema", + "enableSystemAudio": "Activar audio del sistema", + "disableMicrophone": "Desactivar micrófono", + "enableMicrophone": "Activar micrófono", + "record": "Grabar", + "recordingFolder": "Carpeta de grabaciones: {{path}}", + "chooseRecordingsFolder": "Elegir carpeta de grabaciones", + "folderPath": "Ruta: /{{name}}/", + "openVideoFile": "Abrir archivo de video", + "openProject": "Abrir proyecto", + "hideHud": "Ocultar HUD", + "closeApp": "Cerrar aplicación" + }, + "sourceSelector": { + "loadingSources": "Cargando fuentes...", + "screens": "Pantallas", + "windows": "Ventanas", + "windowsNote": "Solo se pueden grabar las ventanas visibles (no minimizadas).", + "windowPlaceholder": "Ventana", + "cancel": "Cancelar", + "share": "Compartir" + }, + "permissions": { + "screenRecordingNeeded": "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly.", + "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.", + "accessibilityNeeded": "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly.", + "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.", + "selectSource": "Por favor selecciona una fuente para grabar", + "systemAudioUnavailable": "El audio del sistema no está disponible para esta fuente. La grabación continuará sin audio del sistema.", + "microphoneDenied": "Se denegó el acceso al micrófono. La grabación continuará sin audio del micrófono.", + "failedToStart": "Error al iniciar la grabación: {{error}}", + "failedToStartGeneric": "Error al iniciar la grabación" + } +} diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 9e26dfee..7b3a1379 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -1 +1,60 @@ -{} \ No newline at end of file +{ + "zoom": { + "level": "Nivel de zoom", + "selectRegion": "Selecciona una región de zoom para ajustar", + "deleteZoom": "Eliminar zoom" + }, + "trim": { + "deleteRegion": "Eliminar región de recorte" + }, + "speed": { + "playbackSpeed": "Velocidad de reproducción", + "selectRegion": "Selecciona una región de velocidad para ajustar", + "deleteRegion": "Eliminar región de velocidad" + }, + "effects": { + "title": "Efectos de video", + "showCursor": "Mostrar cursor", + "loopCursor": "Cursor en bucle", + "backgroundBlur": "Desenfoque de fondo", + "zoomMotionBlur": "Desenfoque de movimiento del zoom", + "connectZooms": "Conectar zooms", + "cursorSize": "Tamaño del cursor", + "cursorSmoothing": "Suavizado del cursor", + "off": "Desactivado", + "cursorMotionBlur": "Desenfoque de movimiento del cursor", + "cursorClickBounce": "Rebote de clic del cursor", + "shadow": "Sombra", + "roundness": "Redondez", + "padding": "Relleno" + }, + "crop": { + "title": "Recortar video", + "instruction": "Arrastra cada lado para ajustar el área de recorte" + }, + "background": { + "title": "Fondo", + "image": "Imagen", + "color": "Color", + "gradient": "Degradado", + "uploadCustom": "Subir personalizado", + "uploadSuccess": "¡Imagen personalizada subida exitosamente!", + "uploadError": "Por favor sube un archivo de imagen JPG o JPEG." + }, + "export": { + "mp4": "MP4", + "gif": "GIF", + "quality": { + "low": "Baja", + "medium": "Media", + "high": "Alta" + }, + "loop": "Bucle", + "outputDimensions": "Salida: {{dimensions}}px", + "loadProject": "Cargar proyecto", + "saveProject": "Guardar proyecto", + "exportVideo": "Exportar {{format}}", + "reportBug": "Reportar error", + "starOnGithub": "Estrella en GitHub" + } +} diff --git a/src/i18n/locales/es/shortcuts.json b/src/i18n/locales/es/shortcuts.json index 9e26dfee..28b42cbc 100644 --- a/src/i18n/locales/es/shortcuts.json +++ b/src/i18n/locales/es/shortcuts.json @@ -1 +1,16 @@ -{} \ No newline at end of file +{ + "actions": { + "addZoom": "Agregar Zoom", + "addTrim": "Agregar Recorte", + "addSpeed": "Agregar Velocidad", + "addAnnotation": "Agregar Anotación", + "addKeyframe": "Agregar Fotograma clave", + "deleteSelected": "Eliminar seleccionado", + "playPause": "Reproducir / Pausar", + "cycleForward": "Recorrer anotaciones hacia adelante", + "cycleBackward": "Recorrer anotaciones hacia atrás", + "deleteSelectedAlt": "Eliminar seleccionado (alt)", + "panTimeline": "Desplazar línea de tiempo", + "zoomTimeline": "Zoom en línea de tiempo" + } +} diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json index 9e26dfee..ed948860 100644 --- a/src/i18n/locales/es/timeline.json +++ b/src/i18n/locales/es/timeline.json @@ -1 +1,38 @@ -{} \ No newline at end of file +{ + "zoom": { + "cannotPlace": "No se puede colocar zoom aquí", + "existsOrNoSpace": "Ya existe un zoom en esta ubicación o no hay suficiente espacio.", + "suggestHandlerUnavailable": "Manejador de sugerencias de zoom no disponible", + "noTelemetry": "No hay telemetría de cursor disponible", + "recordFirst": "Graba una captura de pantalla primero para generar sugerencias basadas en el cursor.", + "noUsableTelemetry": "No hay telemetría de cursor utilizable", + "notEnoughMovement": "La grabación no incluye suficientes datos de movimiento del cursor.", + "noInteractionMoments": "No se encontraron momentos de interacción claros", + "tryRecording": "Intenta una grabación con pausas o clics alrededor de acciones importantes.", + "noAutoZoomSlots": "No hay espacios de auto-zoom disponibles", + "dwellPointsOverlap": "Los puntos de permanencia detectados se superponen con regiones de zoom existentes.", + "addedSuggestions": "Se agregaron {{count}} sugerencia(s) de zoom basadas en interacción", + "label": "Zoom {{index}}", + "addZoom": "Agregar Zoom (Z)", + "suggestZooms": "Sugerir zooms desde cursor" + }, + "trim": { + "cannotPlace": "No se puede colocar recorte aquí", + "existsOrNoSpace": "Ya existe un recorte en esta ubicación o no hay suficiente espacio.", + "label": "Recorte {{index}}", + "addTrim": "Agregar Recorte (T)" + }, + "speed": { + "cannotPlace": "No se puede colocar velocidad aquí", + "existsOrNoSpace": "Ya existe una región de velocidad en esta ubicación o no hay suficiente espacio.", + "label": "Velocidad" + }, + "annotation": { + "label": "Anotación", + "image": "Imagen", + "addAnnotation": "Agregar Anotación (A)" + }, + "addSpeed": "Agregar Velocidad (S)", + "resizeLeft": "Redimensionar izquierda", + "resizeRight": "Redimensionar derecha" +} diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 0967ef42..bee0c88b 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -1 +1,18 @@ -{} +{ + "app": { + "name": "Recordly", + "editorTitle": "Recordly 编辑器", + "subtitle": "屏幕录制与编辑" + }, + "actions": { + "cancel": "取消", + "save": "保存", + "delete": "删除", + "done": "完成" + }, + "errors": { + "invalidFileType": "无效的文件类型", + "failedToUploadImage": "上传图片失败", + "fileReadError": "读取文件时出错。" + } +} diff --git a/src/i18n/locales/zh-CN/dialogs.json b/src/i18n/locales/zh-CN/dialogs.json index 0967ef42..a1749127 100644 --- a/src/i18n/locales/zh-CN/dialogs.json +++ b/src/i18n/locales/zh-CN/dialogs.json @@ -1 +1,61 @@ -{} +{ + "export": { + "pleaseTryAgain": "请重试", + "compilingGifProgress": "正在编译 GIF... {{progress}}%", + "compilingGifWait": "正在编译 GIF... 这可能需要一些时间", + "takeMoment": "这可能需要一点时间...", + "exportFailed": "导出失败", + "compilingGifTitle": "正在编译 GIF", + "exportingFormat": "正在导出 {{format}}", + "exportComplete": "导出完成", + "formatReady": "您的 {{format}} 已准备就绪", + "showInFolder": "在文件夹中显示", + "compiling": "正在编译", + "renderingFrames": "正在渲染帧", + "processing": "正在处理...", + "status": "状态", + "format": "格式", + "compilingStatus": "正在编译...", + "frames": "帧", + "cancelExport": "取消导出", + "savedSuccess": "{{format}} 保存成功!" + }, + "addFont": { + "title": "添加 Google 字体", + "heading": "添加 Google 字体", + "description": "从 Google Fonts 添加自定义字体用于注释。", + "urlLabel": "Google Fonts 导入 URL", + "urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap", + "urlHelp": "从 Google Fonts 获取:选择字体 → 点击「获取字体」→ 复制 @import URL", + "nameLabel": "显示名称", + "namePlaceholder": "我的自定义字体", + "nameHelp": "此名称将显示在字体选择器中", + "adding": "正在添加...", + "addFont": "添加字体", + "enterUrl": "请输入 Google Fonts 导入 URL", + "invalidUrl": "请输入有效的 Google Fonts URL", + "enterName": "请输入字体名称", + "extractFailed": "无法从 URL 提取字体系列名称", + "addSuccess": "字体 \"{{name}}\" 添加成功", + "addFailed": "添加字体失败", + "loadTimeout": "字体加载超时。请检查 URL 后重试。", + "loadFailed": "无法加载字体。请确认 Google Fonts URL 是否正确。" + }, + "shortcutsConfig": { + "title": "键盘快捷键", + "configurable": "可配置", + "fixed": "固定", + "pressEscToCancel": "按 Esc 取消", + "clickToChange": "点击以更改", + "pressAKey": "按下按键…", + "alreadyUsedBy": "已被 {{action}} 使用", + "swap": "交换", + "reserved": "此快捷键已保留给 \"{{label}}\",无法重新分配。", + "saved": "键盘快捷键已保存", + "resetNotice": "已重置为默认快捷键 - 点击保存以应用", + "instructions": "点击快捷键后按下新的组合键。按 Esc 取消。", + "resetToDefaults": "重置为默认", + "cancel": "取消", + "save": "保存" + } +} diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index 0967ef42..97358490 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -1 +1,91 @@ -{} +{ + "playback": { + "play": "播放", + "pause": "暂停" + }, + "annotations": { + "settings": "注释设置", + "active": "活动", + "text": "文本", + "image": "图片", + "arrow": "箭头", + "textContent": "文本内容", + "textPlaceholder": "输入文本...", + "fontStyle": "字体样式", + "selectStyle": "选择样式", + "size": "大小", + "toggleBold": "切换粗体", + "toggleItalic": "切换斜体", + "toggleUnderline": "切换下划线", + "alignLeft": "左对齐", + "alignCenter": "居中对齐", + "alignRight": "右对齐", + "textColor": "文本颜色", + "background": "背景", + "none": "无", + "clearBackground": "清除背景", + "uploadImage": "上传图片", + "supportedFormats": "支持格式:JPG、PNG、GIF、WebP", + "arrowDirection": "箭头方向", + "strokeWidth": "描边宽度:{{width}}px", + "arrowColor": "箭头颜色", + "deleteAnnotation": "删除注释", + "shortcutsAndTips": "快捷键与提示", + "tipSelectAnnotation": "将播放头移动到注释重叠区域并选择项目。", + "tipCycleForward": "使用 Tab 循环切换重叠项目。", + "tipCycleBackward": "使用 Shift+Tab 反向循环切换。", + "imageUploadSuccess": "图片上传成功!", + "imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。" + }, + "fontStyles": { + "classic": "经典", + "editor": "编辑器", + "strong": "加粗", + "typewriter": "打字机", + "deco": "装饰", + "simple": "简约", + "modern": "现代", + "clean": "清爽" + }, + "format": { + "mp4Video": "MP4 视频", + "mp4Description": "高质量视频文件", + "gifAnimation": "GIF 动画", + "gifDescription": "可分享的动态图片" + }, + "gifOptions": { + "frameRate": "帧率", + "outputSize": "输出尺寸", + "outputDimensions": "输出:{{width}} × {{height}}px", + "loopAnimation": "循环动画", + "loopDescription": "GIF 将持续播放" + }, + "tutorial": { + "howTrimmingWorks": "修剪的工作方式", + "title": "修剪的工作方式", + "understanding": "了解如何裁剪视频中不需要的部分。", + "descriptionP1": "修剪工具通过定义要", + "descriptionRemove": "移除", + "descriptionP2": "的片段来工作。", + "descriptionP3": "时间线上被红色修剪区域覆盖的部分将在导出时被剪掉。", + "visualExample": "视觉示例", + "removed": "已移除", + "kept": "保留", + "finalVideo": "最终视频", + "part": "第 {{number}} 部分", + "addTrimStep": "1. 添加修剪", + "addTrimDesc": "按 T 或点击剪刀图标标记要移除的部分。", + "adjustStep": "2. 调整", + "adjustDesc": "拖动红色区域的边缘,精确覆盖要剪掉的部分。" + }, + "keyboardShortcuts": { + "title": "键盘快捷键", + "customizeTooltip": "自定义快捷键", + "customize": "自定义", + "panTimeline": "平移时间线", + "zoomTimeline": "缩放时间线", + "cycleAnnotations": "循环切换注释", + "tab": "Tab" + }, + "openRecordingsFolder": "打开录制文件夹" +} diff --git a/src/i18n/locales/zh-CN/launch.json b/src/i18n/locales/zh-CN/launch.json index 0967ef42..9ac43f3a 100644 --- a/src/i18n/locales/zh-CN/launch.json +++ b/src/i18n/locales/zh-CN/launch.json @@ -1 +1,36 @@ -{} +{ + "recording": { + "disableSystemAudio": "禁用系统音频", + "enableSystemAudio": "启用系统音频", + "disableMicrophone": "禁用麦克风", + "enableMicrophone": "启用麦克风", + "record": "录制", + "recordingFolder": "录制文件夹:{{path}}", + "chooseRecordingsFolder": "选择录制文件夹", + "folderPath": "路径:/{{name}}/", + "openVideoFile": "打开视频文件", + "openProject": "打开项目", + "hideHud": "隐藏 HUD", + "closeApp": "关闭应用" + }, + "sourceSelector": { + "loadingSources": "正在加载源...", + "screens": "屏幕", + "windows": "窗口", + "windowsNote": "仅可录制可见(非最小化)窗口。", + "windowPlaceholder": "窗口", + "cancel": "取消", + "share": "共享" + }, + "permissions": { + "screenRecordingNeeded": "Recordly 需要屏幕录制权限才能开始。系统设置已打开。启用后请退出并重新打开 Recordly。", + "screenRecordingMissing": "屏幕录制权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 Recordly。", + "accessibilityNeeded": "Recordly 还需要辅助功能权限以跟踪光标。系统设置已打开。启用后请退出并重新打开 Recordly。", + "accessibilityMissing": "辅助功能权限仍然缺失。系统设置已再次打开。请启用权限,然后退出并重新打开 Recordly。", + "selectSource": "请选择要录制的源", + "systemAudioUnavailable": "此源不支持系统音频。将继续录制但不包含系统音频。", + "microphoneDenied": "麦克风访问被拒绝。将继续录制但不包含麦克风音频。", + "failedToStart": "录制启动失败:{{error}}", + "failedToStartGeneric": "录制启动失败" + } +} diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 0967ef42..46106e60 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -1 +1,60 @@ -{} +{ + "zoom": { + "level": "缩放级别", + "selectRegion": "选择缩放区域以调整", + "deleteZoom": "删除缩放" + }, + "trim": { + "deleteRegion": "删除修剪区域" + }, + "speed": { + "playbackSpeed": "播放速度", + "selectRegion": "选择变速区域以调整", + "deleteRegion": "删除变速区域" + }, + "effects": { + "title": "视频效果", + "showCursor": "显示光标", + "loopCursor": "循环光标", + "backgroundBlur": "背景模糊", + "zoomMotionBlur": "缩放运动模糊", + "connectZooms": "连接缩放", + "cursorSize": "光标大小", + "cursorSmoothing": "光标平滑", + "off": "关", + "cursorMotionBlur": "光标运动模糊", + "cursorClickBounce": "光标点击弹跳", + "shadow": "阴影", + "roundness": "圆角", + "padding": "内边距" + }, + "crop": { + "title": "裁剪视频", + "instruction": "拖动各边以调整裁剪区域" + }, + "background": { + "title": "背景", + "image": "图片", + "color": "颜色", + "gradient": "渐变", + "uploadCustom": "上传自定义", + "uploadSuccess": "自定义图片上传成功!", + "uploadError": "请上传 JPG 或 JPEG 图片文件。" + }, + "export": { + "mp4": "MP4", + "gif": "GIF", + "quality": { + "low": "低", + "medium": "中", + "high": "高" + }, + "loop": "循环", + "outputDimensions": "输出:{{dimensions}}px", + "loadProject": "加载项目", + "saveProject": "保存项目", + "exportVideo": "导出{{format}}", + "reportBug": "报告问题", + "starOnGithub": "在 GitHub 上加星" + } +} diff --git a/src/i18n/locales/zh-CN/shortcuts.json b/src/i18n/locales/zh-CN/shortcuts.json index 0967ef42..9e3f599b 100644 --- a/src/i18n/locales/zh-CN/shortcuts.json +++ b/src/i18n/locales/zh-CN/shortcuts.json @@ -1 +1,16 @@ -{} +{ + "actions": { + "addZoom": "添加缩放", + "addTrim": "添加修剪", + "addSpeed": "添加变速", + "addAnnotation": "添加注释", + "addKeyframe": "添加关键帧", + "deleteSelected": "删除选中", + "playPause": "播放 / 暂停", + "cycleForward": "向前循环切换注释", + "cycleBackward": "向后循环切换注释", + "deleteSelectedAlt": "删除选中(替代)", + "panTimeline": "平移时间线", + "zoomTimeline": "缩放时间线" + } +} diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json index 0967ef42..53e1054d 100644 --- a/src/i18n/locales/zh-CN/timeline.json +++ b/src/i18n/locales/zh-CN/timeline.json @@ -1 +1,38 @@ -{} +{ + "zoom": { + "cannotPlace": "无法在此处放置缩放", + "existsOrNoSpace": "此位置已存在缩放或空间不足。", + "suggestHandlerUnavailable": "缩放建议处理不可用", + "noTelemetry": "无光标遥测数据", + "recordFirst": "请先录制屏幕以生成基于光标的建议。", + "noUsableTelemetry": "无可用的光标遥测数据", + "notEnoughMovement": "录制中未包含足够的光标移动数据。", + "noInteractionMoments": "未找到明显的交互时刻", + "tryRecording": "尝试在关键操作附近有停顿或点击的录制。", + "noAutoZoomSlots": "没有可用的自动缩放位置", + "dwellPointsOverlap": "检测到的停留点与现有缩放区域重叠。", + "addedSuggestions": "已添加 {{count}} 个基于交互的缩放建议", + "label": "缩放 {{index}}", + "addZoom": "添加缩放 (Z)", + "suggestZooms": "从光标建议缩放" + }, + "trim": { + "cannotPlace": "无法在此处放置修剪", + "existsOrNoSpace": "此位置已存在修剪或空间不足。", + "label": "修剪 {{index}}", + "addTrim": "添加修剪 (T)" + }, + "speed": { + "cannotPlace": "无法在此处放置变速", + "existsOrNoSpace": "此位置已存在变速区域或空间不足。", + "label": "变速" + }, + "annotation": { + "label": "注释", + "image": "图片", + "addAnnotation": "添加注释 (A)" + }, + "addSpeed": "添加变速 (S)", + "resizeLeft": "向左调整", + "resizeRight": "向右调整" +} From e7476881a3f89d55eb16a996500bf6dd46959fde Mon Sep 17 00:00:00 2001 From: dev-Flyblue <1077373674@qq.com> Date: Sun, 15 Mar 2026 13:13:14 +0800 Subject: [PATCH 2/6] feat(i18n): replace hardcoded UI strings with t() calls across all components Migrate all user-visible hardcoded English strings in launch, editor, timeline, settings, dialogs, and shortcuts components to use the i18n t()/useScopedT() functions, enabling runtime language switching. Modified components: - launch: LaunchWindow, SourceSelector - editor: VideoEditor, PlaybackControls, AnnotationSettingsPanel, SettingsPanel, ExportDialog, FormatSelector, GifOptionsPanel, TutorialHelp, KeyboardShortcutsHelp, AddCustomFontDialog, ShortcutsConfigDialog - timeline: TimelineEditor, Item Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/launch/LaunchWindow.tsx | 20 +- src/components/launch/SourceSelector.tsx | 16 +- .../video-editor/AddCustomFontDialog.tsx | 42 +- .../video-editor/AnnotationSettingsPanel.tsx | 101 +- src/components/video-editor/ExportDialog.tsx | 36 +- .../video-editor/FormatSelector.tsx | 138 +-- .../video-editor/GifOptionsPanel.tsx | 207 ++-- .../video-editor/KeyboardShortcutsHelp.tsx | 122 +- .../video-editor/PlaybackControls.tsx | 162 ++- src/components/video-editor/SettingsPanel.tsx | 96 +- .../video-editor/ShortcutsConfigDialog.tsx | 39 +- src/components/video-editor/TutorialHelp.tsx | 271 +++-- src/components/video-editor/VideoEditor.tsx | 143 +-- src/components/video-editor/timeline/Item.tsx | 286 ++--- .../video-editor/timeline/TimelineEditor.tsx | 1019 +++++++++-------- 15 files changed, 1396 insertions(+), 1302 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 0aa8ff5e..20d121b0 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -9,6 +9,7 @@ import { RxDragHandleDots2 } from "react-icons/rx"; import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter"; import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; import { useScreenRecorder } from "../../hooks/useScreenRecorder"; +import { useScopedT } from "../../contexts/I18nContext"; import { Button } from "../ui/button"; import { AudioLevelMeter } from "../ui/audio-level-meter"; import { ContentClamp } from "../ui/content-clamp"; @@ -25,6 +26,7 @@ import styles from "./LaunchWindow.module.css"; export function LaunchWindow() { const { locale, setLocale } = useI18n(); + const t = useScopedT('launch'); const LOCALE_LABELS: Record = { en: "EN", es: "ES", "zh-CN": "中文" }; const { @@ -224,7 +226,7 @@ export function LaunchWindow() { size="icon" onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)} disabled={recording} - title={systemAudioEnabled ? "Disable system audio" : "Enable system audio"} + title={systemAudioEnabled ? t('recording.disableSystemAudio') : t('recording.enableSystemAudio')} className="text-white/80 hover:bg-transparent" > {systemAudioEnabled ? : } @@ -234,7 +236,7 @@ export function LaunchWindow() { size="icon" onClick={toggleMicrophone} disabled={recording} - title={microphoneEnabled ? "Disable microphone" : "Enable microphone"} + title={microphoneEnabled ? t('recording.disableMicrophone') : t('recording.enableMicrophone')} className="text-white/80 hover:bg-transparent" > {microphoneEnabled ? : } @@ -258,7 +260,7 @@ export function LaunchWindow() { ) : ( <> - Record + {t('recording.record')} )} @@ -268,10 +270,10 @@ export function LaunchWindow() { size="sm" onClick={chooseRecordingsDirectory} disabled={recording} - title={recordingsDirectory ? `Recording folder: ${recordingsDirectory}` : "Choose recordings folder"} + title={recordingsDirectory ? t('recording.recordingFolder', undefined, { path: recordingsDirectory }) : t('recording.chooseRecordingsFolder')} className={`text-white/75 hover:bg-transparent px-1 text-[11px] underline decoration-white/45 underline-offset-2 ${styles.electronNoDrag}`} > - {`Path: /${recordingsDirectoryName}/`} + {t('recording.folderPath', undefined, { name: recordingsDirectoryName })}
@@ -281,7 +283,7 @@ export function LaunchWindow() { size="icon" onClick={openVideoFile} disabled={recording} - title="Open video file" + title={t('recording.openVideoFile')} className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`} > @@ -291,7 +293,7 @@ export function LaunchWindow() { size="icon" onClick={openProjectFile} disabled={recording} - title="Open project" + title={t('recording.openProject')} className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`} > @@ -330,7 +332,7 @@ export function LaunchWindow() { variant="link" size="icon" onClick={sendHudOverlayHide} - title="Hide HUD" + title={t('recording.hideHud')} className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`} > @@ -339,7 +341,7 @@ export function LaunchWindow() { variant="link" size="icon" onClick={sendHudOverlayClose} - title="Close App" + title={t('recording.closeApp')} className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`} > diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 0de404bb..9b98e329 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -3,6 +3,7 @@ import { Button } from "../ui/button"; import { MdCheck } from "react-icons/md"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Card } from "../ui/card"; +import { useScopedT } from "../../contexts/I18nContext"; import styles from "./SourceSelector.module.css"; interface DesktopSource { @@ -50,6 +51,7 @@ function parseSourceMetadata(source: ProcessedDesktopSource) { } export function SourceSelector() { + const t = useScopedT('launch'); const [sources, setSources] = useState([]); const [selectedSource, setSelectedSource] = useState(null); const [activeTab, setActiveTab] = useState<'screens' | 'windows'>('screens'); @@ -118,7 +120,7 @@ export function SourceSelector() {
-

Loading sources...

+

{t('sourceSelector.loadingSources')}

); @@ -130,10 +132,10 @@ export function SourceSelector() { setActiveTab(value as 'screens' | 'windows')}> - Screens ({screenSources.length}) + {t('sourceSelector.screens')} ({screenSources.length}) - Windows ({windowSources.length}) + {t('sourceSelector.windows')} ({windowSources.length})
@@ -171,7 +173,7 @@ export function SourceSelector() {
-

Only visible (non-minimized) windows can be recorded.

+

{t('sourceSelector.windowsNote')}

{windowSources.length === 0 && (
No windows available
@@ -202,7 +204,7 @@ export function SourceSelector() { ) : (
)} -
Window
+
{t('sourceSelector.windowPlaceholder')}
)} {selectedSource?.id === source.id && ( @@ -233,8 +235,8 @@ export function SourceSelector() {
- - + +
diff --git a/src/components/video-editor/AddCustomFontDialog.tsx b/src/components/video-editor/AddCustomFontDialog.tsx index a60644fa..f0054a7f 100644 --- a/src/components/video-editor/AddCustomFontDialog.tsx +++ b/src/components/video-editor/AddCustomFontDialog.tsx @@ -19,12 +19,14 @@ import { isValidGoogleFontsUrl, type CustomFont, } from '@/lib/customFonts'; +import { useScopedT } from '../../contexts/I18nContext'; interface AddCustomFontDialogProps { onFontAdded?: (font: CustomFont) => void; } export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { + const t = useScopedT('dialogs'); const [open, setOpen] = useState(false); const [importUrl, setImportUrl] = useState(''); const [fontName, setFontName] = useState(''); @@ -45,17 +47,17 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { const handleAdd = async () => { // Validate inputs if (!importUrl.trim()) { - toast.error('Please enter a Google Fonts import URL'); + toast.error(t('addFont.enterUrl')); return; } if (!isValidGoogleFontsUrl(importUrl)) { - toast.error('Please enter a valid Google Fonts URL'); + toast.error(t('addFont.invalidUrl')); return; } if (!fontName.trim()) { - toast.error('Please enter a font name'); + toast.error(t('addFont.enterName')); return; } @@ -65,7 +67,7 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { // Extract font family from URL const fontFamily = parseFontFamilyFromImport(importUrl); if (!fontFamily) { - toast.error('Could not extract font family from URL'); + toast.error(t('addFont.extractFailed')); setLoading(false); return; } @@ -86,7 +88,7 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { onFontAdded(newFont); } - toast.success(`Font "${fontName}" added successfully`); + toast.success(t('addFont.addSuccess', undefined, { name: fontName })); // Reset and close setImportUrl(''); @@ -95,10 +97,10 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { } catch (error) { console.error('Failed to add custom font:', error); const errorMessage = error instanceof Error ? error.message : 'Failed to load font'; - toast.error('Failed to add font', { + toast.error(t('addFont.addFailed'), { description: errorMessage.includes('timeout') - ? 'Font took too long to load. Please check the URL and try again.' - : 'The font could not be loaded. Please verify the Google Fonts URL is correct.', + ? t('addFont.loadTimeout') + : t('addFont.loadFailed'), }); } finally { setLoading(false); @@ -114,47 +116,47 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { className="w-full bg-white/5 border-white/10 text-slate-200 hover:bg-white/10 h-9 text-xs" > - Add Google Font + {t('addFont.title')} - Add Google Font + {t('addFont.heading')} - Add a custom font from Google Fonts to use in your annotations. + {t('addFont.description')}
handleImportUrlChange(e.target.value)} className="bg-white/5 border-white/10 text-slate-200" />

- Get this from Google Fonts: Select a font → Click "Get font" → Copy the @import URL + {t('addFont.urlHelp')}

setFontName(e.target.value)} className="bg-white/5 border-white/10 text-slate-200" />

- This is how the font will appear in the font selector + {t('addFont.nameHelp')}

@@ -164,14 +166,14 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { onClick={() => setOpen(false)} className="bg-white/5 border-white/10 text-slate-200 hover:bg-white/10" > - Cancel + {t('addFont.cancel')}
@@ -179,4 +181,4 @@ export function AddCustomFontDialog({ onFontAdded }: AddCustomFontDialogProps) { ); } - + diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 4590b421..fd8d9a82 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, useMemo } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info } from "lucide-react"; @@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"; import { getArrowComponent } from "./ArrowSvgs"; import { AddCustomFontDialog } from "./AddCustomFontDialog"; import { getCustomFonts, type CustomFont } from "@/lib/customFonts"; +import { useScopedT } from "../../contexts/I18nContext"; interface AnnotationSettingsPanelProps { annotation: AnnotationRegion; @@ -23,15 +24,15 @@ interface AnnotationSettingsPanelProps { onDelete: () => void; } -const FONT_FAMILIES = [ - { value: 'system-ui, -apple-system, sans-serif', label: 'Classic' }, - { value: 'Georgia, serif', label: 'Editor' }, - { value: 'Impact, Arial Black, sans-serif', label: 'Strong' }, - { value: 'Courier New, monospace', label: 'Typewriter' }, - { value: 'Brush Script MT, cursive', label: 'Deco' }, - { value: 'Arial, sans-serif', label: 'Simple' }, - { value: 'Verdana, sans-serif', label: 'Modern' }, - { value: 'Trebuchet MS, sans-serif', label: 'Clean' }, +const FONT_FAMILY_VALUES = [ + { value: 'system-ui, -apple-system, sans-serif', labelKey: 'fontStyles.classic' }, + { value: 'Georgia, serif', labelKey: 'fontStyles.editor' }, + { value: 'Impact, Arial Black, sans-serif', labelKey: 'fontStyles.strong' }, + { value: 'Courier New, monospace', labelKey: 'fontStyles.typewriter' }, + { value: 'Brush Script MT, cursive', labelKey: 'fontStyles.deco' }, + { value: 'Arial, sans-serif', labelKey: 'fontStyles.simple' }, + { value: 'Verdana, sans-serif', labelKey: 'fontStyles.modern' }, + { value: 'Trebuchet MS, sans-serif', labelKey: 'fontStyles.clean' }, ]; const FONT_SIZES = [12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 96, 128]; @@ -44,9 +45,15 @@ export function AnnotationSettingsPanel({ onFigureDataChange, onDelete, }: AnnotationSettingsPanelProps) { + const t = useScopedT('editor'); const fileInputRef = useRef(null); const [customFonts, setCustomFonts] = useState([]); + const fontFamilies = useMemo(() => + FONT_FAMILY_VALUES.map((f) => ({ value: f.value, label: t(f.labelKey) })), + [t], + ); + // Load custom fonts on mount useEffect(() => { setCustomFonts(getCustomFonts()); @@ -82,8 +89,8 @@ export function AnnotationSettingsPanel({ // Validate file type const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!validTypes.includes(file.type)) { - toast.error('Invalid file type', { - description: 'Please upload a JPG, PNG, GIF, or WebP image file.', + toast.error(t('annotations.imageUploadError'), { + description: t('annotations.imageUploadErrorDescription'), }); event.target.value = ''; return; @@ -95,13 +102,13 @@ export function AnnotationSettingsPanel({ const dataUrl = e.target?.result as string; if (dataUrl) { onContentChange(dataUrl); - toast.success('Image uploaded successfully!'); + toast.success(t('annotations.imageUploadSuccess')); } }; reader.onerror = () => { - toast.error('Failed to upload image', { - description: 'There was an error reading the file.', + toast.error(t('annotations.imageUploadFailed'), { + description: t('annotations.imageUploadFailedDescription'), }); }; @@ -113,9 +120,9 @@ export function AnnotationSettingsPanel({
- Annotation Settings + {t('annotations.settings')} - Active + {t('annotations.active')}
@@ -124,28 +131,28 @@ export function AnnotationSettingsPanel({ - Text + {t('annotations.text')} - Image + {t('annotations.image')} - Arrow + {t('annotations.arrow')} {/* Text Content */}
- +