i18n: Spanish interface language

Add Español next to English and 日本語 (Edit ▸ Language ▸ Español, also in the
native menu bar). SPANISH covers every menu, submenu and menu item label
(347 entries, Latin American Spanish); names, formats and percentages stay
as they are. A new test checks that every entry is a current menu label, so
a renamed command cannot leave a dead entry behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
carlitosteso3
2026-10-06 22:06:02 -05:00
co-authored by Claude Opus 5.5
parent 771b614ad4
commit ed535180cb
3 changed files with 393 additions and 10 deletions
+4 -1
View File
@@ -92,8 +92,11 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver<String>, Sho
}
});
let update: ShortcutUpdater = Box::new(move |items: &[Item]| {
let language = if items.iter().any(|it| it.id == "app.language.japanese" && it.checked == Some(true)) {
let checked = |id: &str| items.iter().any(|it| it.id == id && it.checked == Some(true));
let language = if checked("app.language.japanese") {
filmcraft_ui_egui::i18n::Language::Ja
} else if checked("app.language.spanish") {
filmcraft_ui_egui::i18n::Language::Es
} else {
filmcraft_ui_egui::i18n::Language::En
};
+381 -7
View File
@@ -14,15 +14,17 @@ pub enum Language {
#[default]
En,
Ja,
Es,
}
impl Language {
pub const ALL: [Self; 2] = [Self::En, Self::Ja];
pub const ALL: [Self; 3] = [Self::En, Self::Ja, Self::Es];
pub fn name(self) -> &'static str {
match self {
Self::En => "English",
Self::Ja => "日本語",
Self::Es => "Español",
}
}
@@ -30,17 +32,18 @@ impl Language {
match code {
"en" => Some(Self::En),
"ja" => Some(Self::Ja),
"es" => Some(Self::Es),
_ => None,
}
}
pub fn tr(self, text: &str) -> &str {
if self == Self::Ja
&& let Some((_, japanese)) = JAPANESE.iter().find(|(english, _)| *english == text)
{
return japanese;
}
text
let table = match self {
Self::En => return text,
Self::Ja => JAPANESE,
Self::Es => SPANISH,
};
table.iter().find(|(english, _)| *english == text).map_or(text, |(_, translated)| translated)
}
}
@@ -120,6 +123,356 @@ pub fn install_japanese_font(ctx: &egui::Context) -> bool {
true
}
const SPANISH: &[(&str, &str)] = &[
("About FilmCraft", "Acerca de FilmCraft"),
("Add Audio Submix Track", "Añadir pista de submezcla de audio"),
("Add Caption at Playhead", "Añadir subtítulo en el cabezal"),
("Add Chapter Marker…", "Añadir marcador de capítulo…"),
("Add Edit", "Añadir edición"),
("Add Edit to All Tracks", "Añadir edición a todas las pistas"),
("Add Flash Cue Marker…", "Añadir marcador de referencia Flash…"),
("Add Frame Hold", "Añadir fotograma congelado"),
("Add Guide…", "Añadir guía…"),
("Add Marker", "Añadir marcador"),
("Add New Caption Track…", "Añadir nueva pista de subtítulos…"),
("Add Range Marker", "Añadir marcador de rango"),
("Add Range Marker to In and Out", "Añadir marcador de rango de entrada a salida"),
("Add Tracks…", "Añadir pistas…"),
("Adjustment Layer…", "Capa de ajuste…"),
("Align to Selection", "Alinear con la selección"),
("Align to Video Frame", "Alinear con el fotograma de video"),
("Align to Video Frame as Group", "Alinear con el fotograma de video como grupo"),
("All Panels", "Todos los paneles"),
("Alpha", "Alfa"),
("Appearance", "Apariencia"),
("Appearance…", "Apariencia…"),
("Apply Audio Transition", "Aplicar transición de audio"),
("Apply Default Transitions to Selection", "Aplicar transiciones predeterminadas a la selección"),
("Apply Video Transition", "Aplicar transición de video"),
("Arrange", "Organizar"),
("ArtCraft Website", "Sitio web de ArtCraft"),
("Assembly", "Ensamblaje"),
("Attach Proxies…", "Adjuntar proxies…"),
("Audio Channels…", "Canales de audio…"),
("Audio Clip Mixer", "Mezclador de clips de audio"),
("Audio Gain…", "Ganancia de audio…"),
("Audio Hardware…", "Hardware de audio…"),
("Audio In", "Entrada de audio"),
("Audio Meters", "Medidores de audio"),
("Audio Options", "Opciones de audio"),
("Audio Out", "Salida de audio"),
("Audio Track Mixer", "Mezclador de pistas de audio"),
("Audio Waveform", "Forma de onda de audio"),
("Auto Save…", "Guardado automático…"),
("Automate to Sequence…", "Automatizar a secuencia…"),
("Bars and Tone…", "Barras y tono…"),
("Bin", "Bandeja"),
("Bin From Selection", "Bandeja a partir de la selección"),
("Black Video…", "Video negro…"),
("Blue", "Azul"),
("Bottom", "Abajo"),
("Breakout to Mono", "Separar en mono"),
("Bring Forward", "Traer adelante"),
("Bring to Front", "Traer al frente"),
("Brown", "Marrón"),
("Captions", "Subtítulos"),
("Captions and Graphics", "Subtítulos y gráficos"),
("Captions…", "Subtítulos…"),
("Caribbean", "Caribe"),
("Center Horizontally", "Centrar horizontalmente"),
("Center Vertically", "Centrar verticalmente"),
("Cerulean", "Cerúleo"),
("Clear", "Borrar"),
("Clear Guides", "Borrar guías"),
("Clear In", "Borrar entrada"),
("Clear In and Out", "Borrar entrada y salida"),
("Clear Markers", "Borrar marcadores"),
("Clear Out", "Borrar salida"),
("Clear Selected Marker", "Borrar marcador seleccionado"),
("Close", "Cerrar"),
("Close All Other Projects", "Cerrar todos los demás proyectos"),
("Close All Projects", "Cerrar todos los proyectos"),
("Close Gap", "Cerrar hueco"),
("Close Project", "Cerrar proyecto"),
("Color Management…", "Gestión del color…"),
("Color Matte…", "Mate de color…"),
("Comparison View", "Vista de comparación"),
("Composite Video", "Video compuesto"),
("Consolidate Duplicates", "Consolidar duplicados"),
("Copy", "Copiar"),
("Copy Paste Includes Sequence Markers", "Copiar y pegar incluye marcadores de secuencia"),
("Create Captions from Transcript…", "Crear subtítulos a partir de la transcripción…"),
("Create Multi-Camera Source Sequence…", "Crear secuencia de origen multicámara…"),
("Create Proxies…", "Crear proxies…"),
("Cut", "Cortar"),
("Darkest", "Más oscuro"),
("Delete Render Files", "Eliminar archivos de procesamiento"),
("Delete Render Files In to Out", "Eliminar archivos de procesamiento de entrada a salida"),
("Delete Tracks…", "Eliminar pistas…"),
("Delete Transcript", "Eliminar transcripción"),
("Demo Footage", "Material de demostración"),
("Demo Project", "Proyecto de demostración"),
("Deselect All", "Deseleccionar todo"),
("Detach Proxies", "Separar proxies"),
("Display Mode", "Modo de visualización"),
("Distribute", "Distribuir"),
("Distribute Horizontally", "Distribuir horizontalmente"),
("Distribute Space Horizontally", "Distribuir espacio horizontalmente"),
("Distribute Space Vertically", "Distribuir espacio verticalmente"),
("Distribute Vertically", "Distribuir verticalmente"),
("Duplicate", "Duplicar"),
("Dynamic Audio Waveforms", "Formas de onda de audio dinámicas"),
("Edit", "Edición"),
("Edit Offline…", "Editar sin conexión…"),
("Edit Original", "Editar original"),
("Edit Subclip…", "Editar subclip…"),
("Editing", "Edición"),
("Effect Controls", "Controles de efectos"),
("Effects", "Efectos"),
("Ellipse", "Elipse"),
("Enable", "Activar"),
("Enable Remix", "Activar remezcla"),
("Essential Graphics", "Gráficos esenciales"),
("Essential Sound", "Sonido esencial"),
("Events", "Eventos"),
("Export", "Exportar"),
("Export As Motion Graphics Template…", "Exportar como plantilla de animación…"),
("Extract", "Extraer"),
("Extract Audio", "Extraer audio"),
("Field Options…", "Opciones de campo…"),
("File", "Archivo"),
("File…", "Archivo…"),
("Fill frame", "Rellenar fotograma"),
("FilmCraft Help…", "Ayuda de FilmCraft…"),
("FilmCraft on GitHub", "FilmCraft en GitHub"),
("FilmCraft on getartcraft.com", "FilmCraft en getartcraft.com"),
("Find Next", "Buscar siguiente"),
("Find…", "Buscar…"),
("Fit", "Ajustar"),
("Fit to frame", "Ajustar al fotograma"),
("Flatten", "Acoplar"),
("Forest", "Bosque"),
("Frame Blending", "Fusión de fotogramas"),
("Frame Hold Options…", "Opciones de fotograma congelado…"),
("Frame Sampling", "Muestreo de fotogramas"),
("From Bin", "Desde la bandeja"),
("From Source Monitor", "Desde el monitor de origen"),
("From Source Monitor, Match Frame", "Desde el monitor de origen, fotograma coincidente"),
("From file…", "Desde archivo…"),
("Full", "Completa"),
("Generate Audio Waveform", "Generar forma de onda de audio"),
("Get Media File Properties for", "Obtener propiedades del archivo multimedia de"),
("Go to Gap", "Ir al hueco"),
("Go to In", "Ir a la entrada"),
("Go to Next Caption Segment", "Ir al siguiente segmento de subtítulo"),
("Go to Next Marker", "Ir al siguiente marcador"),
("Go to Out", "Ir a la salida"),
("Go to Previous Caption Segment", "Ir al segmento de subtítulo anterior"),
("Go to Previous Marker", "Ir al marcador anterior"),
("Go to Split", "Ir a la división"),
("Graphics and Titles", "Gráficos y títulos"),
("Graphics…", "Gráficos…"),
("Green", "Verde"),
("Group", "Agrupar"),
("Guide Templates", "Plantillas de guías"),
("Help", "Ayuda"),
("Hide All Caption Tracks", "Ocultar todas las pistas de subtítulos"),
("High Quality Playback", "Reproducción de alta calidad"),
("History", "Historial"),
("Import From", "Importar desde"),
("Import Image Sequence…", "Importar secuencia de imágenes…"),
("Import from Media Browser", "Importar desde el navegador de medios"),
("Import…", "Importar…"),
("Info", "Información"),
("Ingest Settings…", "Ajustes de ingesta…"),
("Insert", "Insertar"),
("Insert Frame Hold Segment", "Insertar segmento de fotograma congelado"),
("Install Motion Graphics Template…", "Instalar plantilla de animación…"),
("Interpret Footage…", "Interpretar material…"),
("Join the ArtCraft Discord…", "Únete al Discord de ArtCraft…"),
("Keyboard Shortcuts…", "Atajos de teclado…"),
("Label", "Etiqueta"),
("Labels…", "Etiquetas…"),
("Language", "Idioma"),
("Lavender", "Lavanda"),
("Learning", "Aprendizaje"),
("Left", "Izquierda"),
("Libraries", "Bibliotecas"),
("Lift", "Levantar"),
("Light", "Claro"),
("Link", "Vincular"),
("Link Media…", "Vincular medios…"),
("Linked Selection", "Selección vinculada"),
("Lock Guides", "Bloquear guías"),
("Lumetri Color", "Color Lumetri"),
("Lumetri Scopes", "Ámbitos Lumetri"),
("Magnification", "Ampliación"),
("Make Offline…", "Desconectar…"),
("Make Subclip…", "Crear subclip…"),
("Make Subsequence", "Crear subsecuencia"),
("Manage Guides…", "Gestionar guías…"),
("Mark Clip", "Marcar clip"),
("Mark In", "Marcar entrada"),
("Mark Out", "Marcar salida"),
("Mark Selection", "Marcar selección"),
("Mark Split", "Marcar división"),
("Markers", "Marcadores"),
("Match Frame", "Fotograma coincidente"),
("Media Analysis & Transcription…", "Análisis y transcripción de medios…"),
("Media Browser", "Navegador de medios"),
("Media Cache…", "Caché de medios…"),
("Media…", "Medios…"),
("Medium", "Medio"),
("Memory…", "Memoria…"),
("Merge Clips…", "Combinar clips…"),
("Metadata", "Metadatos"),
("Modify", "Modificar"),
("Motion Graphics Template…", "Plantilla de animación…"),
("Multi-Camera", "Multicámara"),
("Multi-Camera View", "Vista multicámara"),
("Nest…", "Anidar…"),
("New", "Nuevo"),
("New Layer", "Nueva capa"),
("New Project Panel", "Nuevo panel de proyecto"),
("Next in Sequence", "Siguiente en la secuencia"),
("Next in Track", "Siguiente en la pista"),
("Normalize Mix Track…", "Normalizar pista de mezcla…"),
("Offline File…", "Archivo sin conexión…"),
("Open Project…", "Abrir proyecto…"),
("Optical Flow", "Flujo óptico"),
("Overwrite", "Sobrescribir"),
("Paste", "Pegar"),
("Paste Attributes…", "Pegar atributos…"),
("Paste Insert", "Pegar e insertar"),
("Paused Resolution", "Resolución en pausa"),
("Playback Resolution", "Resolución de reproducción"),
("Playback…", "Reproducción…"),
("Plugins…", "Complementos…"),
("Polygon", "Polígono"),
("Preferences", "Preferencias"),
("Previous in Sequence", "Anterior en la secuencia"),
("Previous in Track", "Anterior en la pista"),
("Program", "Programa"),
("Progress", "Progreso"),
("Project", "Proyecto"),
("Project Manager…", "Administrador de proyectos…"),
("Project Settings", "Ajustes del proyecto"),
("Project…", "Proyecto…"),
("Properties", "Propiedades"),
("Purple", "Púrpura"),
("Reconnect Full Resolution Media…", "Reconectar medios de resolución completa…"),
("Recover Unsaved Changes…", "Recuperar cambios no guardados…"),
("Rectangle", "Rectángulo"),
("Red", "Rojo"),
("Redo", "Rehacer"),
("Reference Monitor", "Monitor de referencia"),
("Remix", "Remezcla"),
("Remix Properties…", "Propiedades de remezcla…"),
("Remove Attributes…", "Quitar atributos…"),
("Remove Filler Words", "Quitar muletillas"),
("Remove Pauses", "Quitar pausas"),
("Remove Unused", "Quitar no utilizados"),
("Rename…", "Cambiar nombre…"),
("Render Audio", "Procesar audio"),
("Render Effects In to Out", "Procesar efectos de entrada a salida"),
("Render In to Out", "Procesar de entrada a salida"),
("Render Selection", "Procesar selección"),
("Replace Fonts in Projects…", "Reemplazar fuentes en proyectos…"),
("Replace With Clip", "Reemplazar con clip"),
("Report an Issue…", "Informar de un problema…"),
("Reset All Parameters", "Restablecer todos los parámetros"),
("Reset Duration", "Restablecer duración"),
("Reset to Saved Layout", "Restablecer al diseño guardado"),
("Restore Captions from Source Clip", "Restaurar subtítulos del clip de origen"),
("Reveal Log Files…", "Mostrar archivos de registro…"),
("Reverse Match Frame", "Fotograma coincidente inverso"),
("Revert", "Revertir"),
("Revert Remix", "Revertir remezcla"),
("Review", "Revisión"),
("Right", "Derecha"),
("Ripple Delete", "Eliminar y cerrar hueco"),
("Ripple Sequence Markers", "Desplazar marcadores de secuencia"),
("Rose", "Rosa"),
("Safe Margins", "Márgenes seguros"),
("Save", "Guardar"),
("Save All", "Guardar todo"),
("Save As…", "Guardar como…"),
("Save Guides as Template…", "Guardar guías como plantilla…"),
("Save a Copy…", "Guardar una copia…"),
("Save as Template…", "Guardar como plantilla…"),
("Scale to Frame Size", "Escalar al tamaño del fotograma"),
("Scene Edit Detection…", "Detección de cambios de escena…"),
("Scratch Disks…", "Discos de memoria virtual…"),
("Search Bin", "Bandeja de búsqueda"),
("Select", "Seleccionar"),
("Select All", "Seleccionar todo"),
("Select All Matching", "Seleccionar todos los coincidentes"),
("Select Label Group", "Seleccionar grupo de etiquetas"),
("Select Next Graphic", "Seleccionar siguiente gráfico"),
("Select Next Layer", "Seleccionar siguiente capa"),
("Select Previous Graphic", "Seleccionar gráfico anterior"),
("Select Previous Layer", "Seleccionar capa anterior"),
("Selection Follows Playhead", "La selección sigue al cabezal"),
("Selection as FilmCraft Project…", "Selección como proyecto de FilmCraft…"),
("Selection…", "Selección…"),
("Send Backward", "Enviar atrás"),
("Send to Back", "Enviar al fondo"),
("Sequence", "Secuencia"),
("Sequence From Clip", "Secuencia a partir de clip"),
("Sequence Settings…", "Ajustes de secuencia…"),
("Sequence…", "Secuencia…"),
("Show Active Caption Tracks Only", "Mostrar solo pistas de subtítulos activas"),
("Show All Caption Tracks", "Mostrar todas las pistas de subtítulos"),
("Show All Marker Colors", "Mostrar todos los colores de marcador"),
("Show Guides", "Mostrar guías"),
("Show Rulers", "Mostrar reglas"),
("Show Through Edits", "Mostrar ediciones pasantes"),
("Simplify Sequence…", "Simplificar secuencia…"),
("Snap in Program Monitor", "Ajustar en el monitor de programa"),
("Snap in Timeline", "Ajustar en la línea de tiempo"),
("Source", "Origen"),
("Source Settings…", "Ajustes de origen…"),
("Speed/Duration…", "Velocidad/duración…"),
("Synchronize…", "Sincronizar…"),
("System Compatibility Report…", "Informe de compatibilidad del sistema…"),
("Tan", "Canela"),
("Teal", "Verde azulado"),
("Text", "Texto"),
("Time Interpolation", "Interpolación de tiempo"),
("Timecode", "Código de tiempo"),
("Timecode…", "Código de tiempo…"),
("Timeline", "Línea de tiempo"),
("Timeline…", "Línea de tiempo…"),
("Toggle Proxies", "Alternar proxies"),
("Tools", "Herramientas"),
("Top", "Arriba"),
("Transcribe Sequence…", "Transcribir secuencia…"),
("Transcribe…", "Transcribir…"),
("Transcript", "Transcripción"),
("Transparent Video…", "Video transparente…"),
("Trim Edit", "Recortar edición"),
("Trim…", "Recorte…"),
("Undo", "Deshacer"),
("Ungroup", "Desagrupar"),
("Universal Counting Leader…", "Cuenta atrás universal…"),
("Update Metadata…", "Actualizar metadatos…"),
("Upgrade Caption to Graphic", "Convertir subtítulo en gráfico"),
("Upgrade to Source Graphic", "Convertir en gráfico de origen"),
("Vertical Text", "Texto vertical"),
("Video In", "Entrada de video"),
("Video Options", "Opciones de video"),
("Video Out", "Salida de video"),
("Video and Audio Waveform Split", "Video y forma de onda de audio divididos"),
("View", "Ver"),
("Violet", "Violeta"),
("Window", "Ventana"),
("Workspaces", "Espacios de trabajo"),
("Yellow", "Amarillo"),
("Zoom In", "Acercar"),
("Zoom Out", "Alejar"),
("Zoom to Sequence", "Ajustar zoom a la secuencia"),
];
const JAPANESE: &[(&str, &str)] = &[
("Clip", "クリップ"),
("Sequence", "シーケンス"),
@@ -222,10 +575,28 @@ mod tests {
assert_eq!(Language::En.tr(en), *en);
}
assert_eq!(Language::Ja.tr("File"), "ファイル");
for (i, (en, es)) in SPANISH.iter().enumerate() {
assert!(!es.is_empty());
assert!(SPANISH.iter().take(i).all(|(other, _)| en != other));
}
assert_eq!(Language::Es.tr("File"), "Archivo");
assert_eq!(Language::Es.tr("mi video.mp4"), "mi video.mp4");
assert_eq!(Language::parse("es"), Some(Language::Es));
assert_eq!(Language::Ja.tr("日本語の文書.pdf"), "日本語の文書.pdf");
assert_eq!(Language::parse("xx"), None);
}
#[test]
fn spanish_entries_are_menu_labels() {
// a renamed command would otherwise leave a dead entry and an untranslated menu item
let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
let items = crate::menus::menu_items(&app);
let known = |text: &str| crate::menus::MENUS.contains(&text) || items.iter().any(|it| it.label == text || it.path.iter().any(|p| p == text));
for (en, _) in SPANISH {
assert!(known(en), "not a menu label: {en}");
}
}
#[test]
fn language_commands_switch_and_persist_without_a_document() {
let mut app = crate::FilmcraftApp::new(filmcraft_engine::Session::default());
@@ -235,6 +606,9 @@ mod tests {
let saved = serde_json::to_string(&app.ui).unwrap();
let restored: crate::state::UiState = serde_json::from_str(&saved).unwrap();
assert_eq!(restored.language, Language::Ja);
crate::menus::invoke(&mut app, &ctx, "app.language.spanish", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Language::Es);
assert!(crate::menus::menu_items(&app).iter().any(|it| it.id == "app.language.spanish" && it.checked == Some(true)));
crate::menus::invoke(&mut app, &ctx, "app.language.english", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Language::En);
}
+8 -2
View File
@@ -24,6 +24,7 @@ macro_rules! uic {
pub const UI_COMMANDS: &[UiCommand] = &[
uic!("app.language.english", "English", ["Edit", "Language"], None),
uic!("app.language.japanese", "日本語", ["Edit", "Language"], None),
uic!("app.language.spanish", "Español", ["Edit", "Language"], None),
uic!("playback.toggle", "Play/Stop", [], Some("Space")),
uic!("playback.forward", "Shuttle Right", [], Some("L")),
uic!("playback.stop", "Shuttle Stop", [], Some("K")),
@@ -146,12 +147,16 @@ pub fn panel_command_id(p: PanelKind) -> String {
/// Execute a UI or engine command by id.
pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result<Value, String> {
if matches!(id, "app.language.english" | "app.language.japanese") {
if matches!(id, "app.language.english" | "app.language.japanese" | "app.language.spanish") {
// Japanese needs the craft-fonts (built with CRAFT_FONTS_DIR) or a font installed on the system
if id == "app.language.japanese" && !crate::i18n::install_japanese_font(ctx) {
return Err("no Japanese font is installed on this system (for example Noto Sans CJK JP); the interface stays in English".into());
}
app.ui.language = if id == "app.language.japanese" { crate::i18n::Language::Ja } else { crate::i18n::Language::En };
app.ui.language = match id {
"app.language.japanese" => crate::i18n::Language::Ja,
"app.language.spanish" => crate::i18n::Language::Es,
_ => crate::i18n::Language::En,
};
let items = menu_items(app);
if let Some(hook) = app.hooks.shortcuts_changed.as_mut() {
hook(&items);
@@ -401,6 +406,7 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
match it.id.as_str() {
"app.language.english" => it.checked = Some(app.ui.language == crate::i18n::Language::En),
"app.language.japanese" => it.checked = Some(app.ui.language == crate::i18n::Language::Ja),
"app.language.spanish" => it.checked = Some(app.ui.language == crate::i18n::Language::Es),
_ => {}
}
if it.id.starts_with("view.") {