Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dos idiomas: traducciones, y letras o solfeo

Archivos: crates/lineup-core/src/i18n.rs, crates/lineup-core/src/i18n/es.rs, crates/lineup-app/src/i18n.rs, crates/lineup-core/src/pitch.rs, crates/lineup-core/src/harmony/symbol.rs, crates/lineup-core/src/chord_sheet.rs, crates/lineup-core/src/file.rs y crates/lineup-app/src/app/settings.rs

Dos ajustes cambian cómo te habla Ferrous Lineup:

  • Edición > Ajustes… > Apariencia > Idioma (con la app en inglés, Edit > Settings… > Appearance > Language) escribe los menús, las ventanas y los ajustes en English o en Español.
  • Edición > Ajustes… > Armonía > Nomenclatura de notas (en inglés, Harmony > Note naming) nombra las notas, las tonalidades y los acordes con letras (C, F#, Bb major, Am7) o en solfeo (Do, Fa#, Sib mayor, Lam7).

Son independientes: puedes tener los menús en español con letras, o los menús en inglés con solfeo. Pero mientras no elijas una nomenclatura, sigue al idioma: con los menús en español las notas se nombran en solfeo, como en las escuelas de habla hispana, y con los menús en inglés, con letras. Cuando eliges una, se queda, cambies después al idioma que cambies. Los dos ajustes cambian solo lo que ves. Una canción guardada con una elección se abre exactamente igual para quien tenga la otra.

Con el español elegido, todo lo que escribe la app está en español: los menús, las ventanas, las ayudas, los avisos y los nombres de las demos, los estilos, los instrumentos y los canales. En inglés solo quedan los mensajes que llegan del sistema operativo o de una biblioteca (por ejemplo, por qué no se pudo abrir un dispositivo de audio) y los nombres de géneros que el español usa tal cual, como swing o funk. Este libro nombra los menús, las ventanas y los botones como los escribe la app en español. La mayoría de las capturas de este libro se hicieron con la app en inglés.

Las notas Do, Re, Mi

En inglés y en alemán, las notas se nombran con letras. En español, italiano, francés y portugués, y en los países que los hablan, se nombran con sílabas:

letras    C    D    E    F    G    A    B
solfeo    Do   Re   Mi   Fa   Sol  La   Si

Los sostenidos y los bemoles se escriben detrás de la sílaba igual que detrás de la letra: Fa sostenido es Fa# y Si bemol es Sib. Una tonalidad se nombra igual, así que Si♭ mayor es Sib mayor (o Sib major con los menús en inglés), y el modo dórico sobre Re es Re dórico.

Esto es el Do fijo: Do es siempre la misma nota, sea cual sea la tonalidad. Algunos métodos de entrenamiento auditivo usan un do móvil, en el que do es la tónica de la tonalidad (en Sol mayor, do es Sol). La app usa siempre el Do fijo, como las escuelas de España y América Latina, porque el nombre de una nota no debe cambiar con la tonalidad.

Los números de octava no cambian: el Do central es C4 con letras y Do4 en solfeo, y cada octava va de Do a Si. Algunos libros franceses y españoles numeran las octavas una más abajo y llaman Do3 al Do central. La app mantiene una sola numeración, así que una nota conserva su número la nombres como la nombres.

Los acordes en solfeo

Un cifrado es una fundamental, una cualidad y, a veces, una nota en el bajo. En solfeo solo cambian los nombres de las notas:

letras    C    Am7     G7/B     Bdim     F#m7b5    Eb+
solfeo    Do   Lam7    Sol7/Si  Sidim    Fa#m7b5   Mib+

A la primera manera se le llama cifrado americano y a la segunda cifrado latino. Las partituras de jazz y de pop suelen ir en cifrado americano, también en español, y por eso este libro escribe así los cifrados; muchos profesores y cancioneros usan el latino. Con el solfeo elegido, la hoja de acordes, las teclas de armonía, el piano roll, los cambios de tonalidad de la lista de reproducción y los ejemplos de las ayudas escriben las notas, las tonalidades y los acordes de la segunda manera.

Escribir un acorde de las dos maneras

Puedes escribir un acorde de cualquiera de las dos maneras, esté elegida la que esté: Am7 y Lam7 son el mismo acorde. Al pulsar Intro, el compás lo muestra como el ajuste nombra las notas.

Unos pocos nombres se pueden leer de las dos maneras. o es una forma de escribir un acorde disminuido (Bo es B°), así que Do podría ser Do mayor (solfeo) o Re disminuido (letras), y Do7 podría ser C7 o D°7. La app lee esos nombres como el ajuste nombra las notas: con el solfeo elegido, Do es Do mayor. Los nombres que solo tienen sentido de una manera se leen de esa manera: Faug es Fa aumentado aunque esté elegido el solfeo, porque «Fa» seguido de «ug» no es un acorde, y Fam es Fa menor aunque estén elegidas las letras.

La cualidad se escribe igual en los dos: Fa aumentado es Faaug en solfeo. Se lee bien, pero Fa+ se ve mejor, y la app también lo lee.

En la hoja de acordes, escribir sobre un compás seleccionado empieza a escribir en él, pero L es la tecla que cambia entre patrón y canción. Con el solfeo elegido, los acordes de La empiezan por L, así que ahí L empieza a escribir. Con las letras elegidas, ningún acorde empieza por L, y L cambia de modo como siempre.

Qué se guarda

Los proyectos, los estilos guardados, el texto de la hoja de acordes que copia Ctrl+C y las trazas de reproducción se escriben siempre con letras, y en inglés. Los ajustes no llegan nunca a ellos. Así que una canción que guardaste viendo Lam7 contiene Am7, y se abre con Am7 o con Lam7 según los ajustes de quien la abra.

En el código

Dos ajustes, guardados como el de los números romanos

Language y NoteNaming (los dos en lineup-core) se guardan como el ajuste de los números romanos en menor: un enum con un #[default], un Cell dentro de thread_local! que toda función puede leer, y un with que lo fija un rato y lo devuelve con un guard en un Drop, incluso tras un panic.

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum NoteNaming {
    #[default]
    Letters,
    Solfege,
}

thread_local! {
    static NOTE_NAMING: Cell<NoteNaming> = const { Cell::new(NoteNaming::Letters) };
}
}

Una nota se escribe en cientos de lugares, a través de su implementación de Display ({} en format!). Un valor por hilo permite que todos sigan el ajuste sin pasarlo hacia abajo por cada función:

#![allow(unused)]
fn main() {
impl fmt::Display for NoteName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.format(NoteNaming::current()))
    }
}
}

La ventana fija los dos valores a partir de los ajustes guardados antes de dibujar cada fotograma:

#![allow(unused)]
fn main() {
pub fn apply_settings(ctx: &egui::Context) {
    language(ctx).set();
    note_naming(ctx).set();
}
}

Una exportación de video dibuja sus vistas en un hilo propio, que empieza con los valores por defecto, así que se lleva el idioma y la nomenclatura consigo, como ya se llevaba el ajuste de los números romanos.

Solfeo hasta que elijas

egui guarda las preferencias de la ventana como valores bajo un Id, y get_persisted da None para un Id bajo el que no se guardó nada. Ese None es lo que distingue «nunca elegido» de una elección, así que la nomenclatura recurre al idioma solo entonces:

#![allow(unused)]
fn main() {
pub fn note_naming(ctx: &egui::Context) -> NoteNaming {
    ctx.data_mut(|data| data.get_persisted(note_naming_id()))
        .unwrap_or_else(|| language_naming(language(ctx)))
}

/// How notes are named where `language` is spoken.
fn language_naming(language: Language) -> NoteNaming {
    match language {
        Language::English => NoteNaming::Letters,
        Language::Spanish => NoteNaming::Solfege,
    }
}
}

Leer nunca guarda nada; solo lo hace set_note_naming, que se llama cuando haces clic en una nomenclatura en los Ajustes. Así que hasta ese clic cada fotograma vuelve a preguntar al idioma, y pasar al español pasa las notas al solfeo. El match no tiene un brazo _ a propósito: un tercer idioma no compilaría hasta que alguien diga cómo nombra las notas.

Traducir: la clave es el texto en inglés

Cada texto que muestra la interfaz está escrito en inglés en el código y pasa por tr. Vive en lineup-core, no en la app, para que también se traduzcan los textos que escribe el núcleo, los nombres de los estilos, los instrumentos, los acordes y las escalas:

#![allow(unused)]
fn main() {
pub fn tr(text: &'static str) -> &'static str {
    match Language::current() {
        Language::English => text,
        Language::Spanish => in_spanish(text).unwrap_or_else(|| missed(text)),
    }
}
}

El catálogo en español está en i18n/es.rs: un gran match para los menús, las ventanas principales y los ajustes, y, en los archivos de i18n/es/, tablas de pares por área (la app, la teoría, los instrumentos, la biblioteca, las canciones), reunidas una sola vez en un HashMap:

#![allow(unused)]
fn main() {
static TEXTS: LazyLock<HashMap<&'static str, &'static str>> =
    LazyLock::new(|| TABLES.into_iter().flatten().copied().collect());

/// `text` in Spanish, or `None` for a text with no translation yet.
pub fn translate(text: &str) -> Option<&'static str> {
    main_texts(text).or_else(|| TEXTS.get(text).copied())
}
}

LazyLock construye el mapa la primera vez que se usa, en el hilo que lo pida primero, y a partir de ahí todos los hilos lo comparten. flatten convierte las cinco tablas en una sola serie de pares, y collect construye el mapa con ellos.

Usar el propio texto en inglés como clave tiene dos ventajas. El código se sigue leyendo con naturalidad (tr("Settings"), no tr(SETTINGS_TITLE)), y un texto que aún no tiene traducción se queda simplemente en inglés, gracias a unwrap_or_else. El precio es que cambiar un texto en inglés obliga a cambiar también su clave en el catálogo, o se pierde el español; una prueba lo detecta (ver más abajo).

Tanto el argumento como el resultado son &'static str: texto escrito en el propio programa, que vive tanto como el programa. Así tr puede devolver cualquiera de los dos sin copiar nada.

Unos cuantos parientes de tr cubren los demás casos:

  • trf("{} steps a bar", &[&12]) pone sus argumentos donde el texto traducido tiene {}, así que el español puede poner el número entre otras palabras: «12 pasos por compás».
  • trc("clef", "Bass") es para una palabra cuya traducción depende de dónde se muestra: la clave de fa es «Fa», el instrumento «Bajo». El catálogo la tiene como "clef|Bass".
  • tr_name traduce los nombres que la app da a las cosas, un canal, una pista o un patrón: «Bass» es «Bajo», «Pattern 12» es «Patrón 12» y «A drums» es «Batería de A». Un nombre que escribiste tú se queda como lo escribiste.
  • Un texto del catálogo puede nombrar notas entre llaves, como en "{Bb} mayor"; se nombran como dice el ajuste (ver Ejemplos que siguen el ajuste). Un texto así se construye una vez para cada nomenclatura y se conserva, con Box::leak, que convierte un String en un &'static str que nunca se libera. Hay un número limitado de textos y dos nomenclaturas, así que la memoria conservada tiene un límite.

Nombres en pantalla, letras en los archivos

Como Display sigue el ajuste, todo lo que convierta una nota en texto y lo guarde guardaría solfeo. Los lugares por donde los datos salen del programa trabajan con letras, a través de with:

#![allow(unused)]
fn main() {
pub fn to_text(project: &Project) -> Result<String, FileError> {
    // …
    let file = Language::English.with(|| NoteNaming::Letters.with(|| ProjectFile::from(project)));
    let mut text = NoteNaming::Letters.with(|| ron::ser::to_string_pretty(&file, style))?;
    // …
}
}

Un marcador de acorde se guarda como su texto, mediante los atributos de serde into = "String" y try_from = "String" (ver Series dodecafónicas). Sus conversiones también trabajan con letras, así que un marcador se guarda como Am7 lo guarde quien lo guarde:

#![allow(unused)]
fn main() {
impl From<HarmonyName> for String {
    fn from(harmony: HarmonyName) -> Self {
        NoteNaming::Letters.with(|| harmony.to_string())
    }
}
}

Los estilos guardados, el texto de la hoja de acordes y la traza de reproducción hacen lo mismo. Los cifrados de la propia biblioteca están escritos con letras en el código fuente, así que también se leen con letras.

Leer un nombre de las dos maneras

NoteName::read_as lee una nota al principio de un texto con una nomenclatura, y devuelve el nombre y el resto del texto. Para el solfeo prueba cada sílaba:

#![allow(unused)]
fn main() {
NoteNaming::Solfege => Letter::ALL.into_iter().find_map(|letter| {
    text.strip_prefix(letter.solfege())
        .map(|rest| (letter, rest))
})?,
}

strip_prefix devuelve el texto que sigue al prefijo si el texto empieza por él, y None si no. find_map devuelve el primer Some. «Sol» y «Si» empiezan los dos por S, pero ninguno es el principio del otro, así que solo uno puede coincidir.

read_either prueba las dos nomenclaturas, primero la del ajuste, y se queda con la primera lectura cuyo resto encaja (fits):

#![allow(unused)]
fn main() {
pub fn read_either<'a>(
    text: &'a str,
    fits: impl Fn(&'a str) -> bool,
) -> Option<(NoteName, &'a str)> {
    NoteNaming::current()
        .reading_order()
        .into_iter()
        .filter_map(|naming| NoteName::read_as(text, naming))
        .find(|&(_, rest)| fits(rest))
}
}

Qué significa «encajar» depende de quién llama, así que es un closure. Un cifrado encaja cuando el resto es una cualidad y quizá un bajo; un nombre de nota completo encaja cuando no queda nada:

#![allow(unused)]
fn main() {
pub fn parse(text: &str) -> Option<NoteName> {
    NoteName::read_either(text, str::is_empty).map(|(name, _)| name)
}
}

str::is_empty es una función, y una función puede pasarse allí donde se espera un closure con los mismos argumentos.

El lifetime 'a dice que el resto que recibe fits, y que se devuelve, es un trozo de text, no una copia. Así funciona «Faug» con el solfeo elegido: «Fa» deja «ug», que fits rechaza, así que se prueba la lectura con letras, F con «aug», y se queda esa.

Ejemplos que siguen el ajuste

Las ayudas de la hoja de acordes y de los ajustes dan acordes de ejemplo. Se escriben con las notas entre llaves, {A}m7 {D}7, y with_notes nombra cada una como dice el ajuste, así que con el solfeo elegido la ayuda dice «Lam7 Re7». Las llaves alrededor de algo que no es un nombre de nota, como el conjunto {0,1,4} de la misma ayuda, se dejan como están.

L de La

ChordSheet::takes_l dice si una L escrita ahora le corresponde a la hoja: la hoja tiene el teclado, hay un compás seleccionado, todavía no se está escribiendo en él, y las notas se nombran en solfeo. El atajo L de la app lo mira primero:

#![allow(unused)]
fn main() {
let l_types = self.chord_sheet.takes_l() && !self.live.typing();
if !typing && !controls_active && !l_types && pressed(egui::Key::L) {
}

!self.live.typing() está ahí porque, con el teclado para tocar activado, las letras tocan notas, y L cambia entre patrón y canción como siempre.

Fuera de la app

El archivo de escritorio de Linux tiene líneas en español junto a las inglesas, por ejemplo GenericName[es]=Banda de acompañamiento, que un escritorio en español muestra en sus menús. El archivo AppStream que leen las tiendas de software da a cada párrafo de la descripción un gemelo en español, <p xml:lang="es">, y appstreamcli validate lo comprueba.

Las pruebas

every_window_writes_in_spanish abre seis demostraciones en español con todas las ventanas abiertas, y después los atajos, Acerca de, la exportación de video, el editor de estilos, cada página de los Ajustes y cada menú. Falla si algo le pidió a tr un texto que no tiene español en el catálogo, o si algún texto dibujado en pantalla es un texto en inglés que el catálogo sí tiene en español, lo que detecta el texto dibujado sin pasar por tr. En el núcleo, every_name_and_description_the_app_shows_has_spanish hace lo mismo con cada nombre de instrumento, estilo, acorde, escala y demostración que el núcleo le da a la app.

solfege_changes_how_names_look_not_what_is_saved construye cada canción de demostración dos veces, una con letras y otra con solfeo, y comprueba que el archivo guardado y el texto de la hoja de acordes son idénticos, y que cada acorde, escrito como lo muestra la hoja en solfeo, se lee de vuelta como el mismo acorde.

names_are_read_in_letters_or_solfege lee Sib7, Fa#m, Sol y Reb, y comprueba que Do7 empieza por Re con las letras elegidas y por Do con el solfeo. solfege_is_a_way_of_showing_names comprueba que Am7 se muestra como Lam7, G/B como Sol/Si y Si♭ mayor como Sib mayor, y que Lam7 y Am7 son el mismo acorde.

in_solfege_l_types_la_chords_and_in_letters_it_switches_mode selecciona un compás de la hoja de acordes, pulsa L con el solfeo elegido y escribe Lam7, y comprueba que el modo no cambió y que el compás tiene La menor séptima; con las letras, L cambia el modo y deja el compás como estaba.

spanish_names_notes_in_solfege_until_a_naming_is_chosen cambia el idioma de un lado a otro sin nomenclatura elegida, y la nomenclatura lo sigue; luego elige una nomenclatura y vuelve a cambiar el idioma, y la nomenclatura se queda. spanish_names_notes_in_solfege_and_keeps_letters_once_chosen lo hace como lo harías tú: menús en español, Armonía, Letras (C, D, E…) y un reinicio de la app, tras el cual las notas siguen en letras.