Cuaderno de GNUtas

Propuestas para una gestión multilingüe en Org Mode (un primer apunte)

Estado de la cuestión

A lo largo de mi trabajo en Org Mode —especialmente cuando lo uso como interfaz de alto nivel para LATEX—, una de las cosas que más vengo echando en falta es un consistente soporte multilingüe, especialmente asociado a las posibilidades de exportación. Cuando nos referimos a «soporte multilingüe» queremos decir un sistema parecido a lo que hace el paquete babel en LATEX: definir una lengua principal para el documento junto a la posibilidad de definir también una serie de lenguas «secundarias» para activarlas o desactivarlas a voluntad sobre tramos de texto determinados. Por desgracia, la situación dista mucho de ser ésa.

Los documentos de Org, en efecto, son monolingües de fábrica. Podemos indicar una lengua principal mediante la directiva #+LANGUAGE:, y según sea la lengua que hayamos escogido, Org tomará una serie de decisiones a la hora de exportar el documento. Dichas decisiones pueden concertarse perfectamente con la opción de exportación ':t, que se encargará de escoger las comillas adecuadas, de primer o segundo nivel, para la lengua en cuestión: una suerte de funcionalidad de «comillas inteligentes», aunque no tan poderosa como la que ofrece el paquete para LATEX csquotes.

Por supuesto, Org ofrece también los recursos y herramientas necesarias para salvar estas limitaciones. Una cosa que yo venía usando bastante, especialmente para los comandos del paquete babel de LATEX \foreignlanguage{lengua}{texto}, cuando se trata de texto extranjero dentro de un párrafo, eran las polivalentes macros de sustitución. Por ejemplo, podemos definir esta macro lg que evalúa un condicional en Elisp. Si la exportación es a LATEX, toma como segundo argumento (el primero es la lengua escogida) el tramo de texto a encerrar dentro del segundo argumento de la macro \foreignlanguage. Si la exportación es a otro formato, de momento, devuelve sólo el segundo argumento, es decir, el texto. Si luego queremos afinar más con otras salidas, podemos aplicar un multi-condicional del estilo cond, pero no es el caso pues aquí lo que necesitaba era la salida a LATEX:

#+MACRO: lg (eval (if (org-export-derived-backend-p org-export-current-backend 'latex) (concat "@@latex:\\foreignlanguage{@@" $1 "@@latex:}{@@" "\u200B" $2 "\u200B" "@@latex:}@@") $2))

Así, podríamos poner en nuestro documento algo como esto:

Lorem ipsum dolor sit amet, {{{lg(english,consectetuer adipiscing elit)}}}.

que nos entregaría este esperable resultado en LATEX:

Lorem ipsum dolor sit amet, \foreignlanguage{english}{consectetuer adipiscing elit}.

Bien, este sistema, aunque nos viene a hacer el apaño, adolece de ciertos inconvenientes que sólo con el uso continuado he ido más o menos solventando. A saber:

  • Obsérvese que en la definición de la macro, justo antes y después del segundo argumento, he concatenado el carácter Unicode U+200B (ZERO WIDTH SPACE), para evitar que cualquier marca de énfasis de Org se exporte literalmente (lo que no se pretende) en contacto con las llaves de inicio o cierre del argumento en LATEX1. O sea, que esto:

    {{{lg(english,/consectetuer/ adipiscing elit)}}}.
    

    entregue esto:

    \foreignlanguage{english}{\emph{consectetuer} adipiscing elit}.
    

    y no esto:

    \foreignlanguage{english}{/consectetuer/ adipiscing elit}.
    
  • Otro inconveniente, éste mucho más engorroso, es el hecho de que el propio signo de separación de macros en Org, la coma, puede interferir claramente en el segundo argumento de la macro, al ser de naturaleza textual. Si queremos incluir allí una coma literal, debemos precederla de una barra invertida como carácter de escape:

    {{{lg(english,consectetuer adipiscing elit\, y mucho texto más)}}}.
    

    o, de lo contrario, la macro se exportará mal y el tramo de texto que queda tras la coma no aparecerá. Tener que escapar un signo tan habitual como una coma es una pesadez, y no es raro que nos olvidemos más de una vez de hacerlo. Para evitar estos lapsus (que nos pueden destrozar sin querer el documento de salida), tuve que apañármelas con esta función:

    (defun arregla-comas-macro-org ()
        (interactive)
        (let
            ((x (make-marker))
             (y (make-marker)))
          (save-excursion
            (goto-char (point-min))
            (while
                (re-search-forward "{{{lg(\\b[a-z]+\\b," nil t)
              (set-marker x  (point))
              (re-search-forward ")}}}" nil t)
              (set-marker y (point))
              (save-restriction
                (narrow-to-region x y)
                (reemplaza "\\([^\\\\]\\)," "\\1\\\\,"))))))
    

    y anclarla, siquiera de manera local, a un before-save-hook.

En fin, todo este sistema de las macros, si bien funciona perfectamente cuando se van limando contratiempos, no deja de ser un remiendo, en parte porque estamos llevando el recurso de las macros de sustitución casi al límite. De modo que me puse a barajar otras posibilidades.

Acotar texto multilingüe en Org: propuestas

A mi modo de ver, las soluciones al problema multilingüe de Org deben repartirse entre dos niveles diferenciados: un nivel intra-párrafo (in line) y un nivel supra-párrafo, necesario para acotar bloques de texto individualizados dentro del flujo de texto general.

Nivel intra-párrafo

Es decir, aquí nos interesa, una vez más, acotar segmentos de texto dentro de un párrafo de tal forma que se exporten dentro de la macro de LATEX \foreignlanguage{lengua}{texto}. Lo primero que me vino a la mente fue idear algún sistema de marcas complementario y exportar todo mediante un filtro escrito ad hoc. Simple, pero más complejidad añadida innecesariamente. Es en ese momento cuando uno cae en que no hay por qué inventar la rueda, cuando ya dentro de Org tenemos un recurso tan útil como org-link-set-parameters, que nos permite definir nuevos tipos de enlaces añadiéndoles un formato de exportación arbitrario. Un ejemplo extenso de las posibilidades y potencia de ese recurso de Org lo puede encontrar el lector en esta otra Gnuta donde hablábamos de cómo generar «subfiguras» en Org para la exportación a LATEX y HTML2.

Así pues, podemos definir un primer tipo de enlace que tome la etiqueta «lang» y que se despliegue completamente. La estructura sería entonces esta:

Lorem ipsum dolor sit amet, [[lang:en][consectetuer adipiscing elit]].

Como se ve, podemos indicar la lengua mediante su correspondiente código ISO (para lo cual nos viene de perlas la lista asociativa ya definida en la biblioteca ox-latex org-latex-babel-language-alist) o de forma literal, tal y como lo entiende babel en LATEX, pero precedido de «!»:

Para llegar a este tipo de enlace, entonces, tuve que escribir el código que sigue3:

(org-link-set-parameters "lang"
                         :display 'full
                         :face 'magit-header-line-key
                         :export (lambda (ruta desc format)
                                   (cond
                                    ((eq format 'latex)
                                     (let ((langs org-latex-babel-language-alist))
                                       (concat
                                        "\\foreignlanguage{"
                                        (if (string-match-p "!" ruta)
                                            (replace-regexp-in-string "!" "" ruta)
                                          (cdr (assoc ruta langs)))
                                        "}{" desc "}")))
                                    ((or (eq format 'html)
                                         (eq format 'odt))
                                     (format "%s" desc)))))

Obsérvese que dejo abierta en el condicional las posibilidades de un distinto tratamiento para la salida a HTML u odt (véase esta línea del código precedente), pero de momento, como no es una prioridad para mí, lo que devuelve es el texto acotado sin más.

Pero aún podemos afinar más las cosas. Como casi siempre estos fragmentos de texto extranjero se ponen entrecomillados, podemos definir otro nuevo tipo de enlace (complementario al anterior) que aprovecha la macro del paquete de LATEX csquotes \hyphentextquote{lengua}{texto}, que entrecomilla un pasaje y le carga las normas de guionado correspondiente a la lengua escogida4.

(org-link-set-parameters "qlang"
                         :display 'full
                         :face 'magit-header-line-key
                         :export (lambda (ruta desc format)
                                   (cond
                                    ((eq format 'latex)
                                     (let ((langs org-latex-babel-language-alist))
                                       (concat
                                        "\\hyphentextquote{"
                                        (if (string-match-p "!" ruta)
                                            (replace-regexp-in-string "!" "" ruta)
                                          (cdr (assoc ruta langs)))
                                        "}{" desc "}")))
                                    ((or (eq format 'html)
                                         (eq format 'odt))
                                     ;; ojo, aquí habría que arreglar
                                     ;; lo de las comillas interiores
                                     (format "«%s»" desc)))))

Como sucede en el caso anterior, en este bloque también se deja abierta la posibilidad para el formato en salidas distintas de LATEX. Pero habría que añadir alguna solución ad hoc para el caso de las comillas interiores, de haberlas, ya que aquí no nos podemos beneficiar de los automatismos de csquotes (véase esta línea en el bloque precedente).

Nivel supra-párrafo

A este nivel, donde queremos bloques de texto diferenciados para ser tratados en la exportación como lenguas distintas de la principal, en la salida a LATEX nos interesan los entornos de babel otherlanguage{lengua} y otherlanguage*{lengua} (véase la documentación del paquete).

Org ya viene de fábrica con la útil funcionalidad de los bloques «especiales», que a LATEX se exportan como entornos y a HTML como DIVS. Así pues, podemos poner en nuestro documento algo como esto (con un pasaje de Balzac):

#+ATTR_LaTeX: :options {french}
#+begin_otherlanguage
Nous exagérons le malheur et le bonheur de manière égale, nous ne sommes jamais aussi
mauvais ni aussi heureux que nous le disons
#+end_otherlanguage

Pero aún podemos hacer las cosas más afinadas, como veremos.

Bloques de código Org como bloques multilingües

Hace unos meses (a fecha de publicación de este artículo), en la lista de desarrollo de Org Mode, un usuario hizo una propuesta que me pareció bastante interesante: ¿por qué no aplicarles a los bloques de código de Org un argumento de cabecera :lang? Esto tiene la evidente ventaja de que podemos escribir Org dentro de Org (por decirlo de manera gruesa), pero toda esa parte estaría tratada en la exportación como una lengua diferente. Sobre esta base me puse a ensayar algo de código. Aplicar un nuevo argumento de cabecera a los bloques Org no es algo complicado en principio. Bastaría con hacer algunas redefiniciones del código original.

Primero escribí este par de funciones. Ambas encierran el segundo argumento (body) en el entorno de LaTeX adecuado, con la salvedad de que la segunda añade además la macro de csquotes \EnableQuotes dentro del entorno:

(defun my-lang-org-backend (lang body)
  (cond
   ((org-export-derived-backend-p org-export-current-backend 'latex)
    (format "@@latex:\\begin{otherlanguage}{%s}@@\n%s\n@@latex:\\end{otherlanguage}@@" lang body))
   ((or (org-export-derived-backend-p org-export-current-backend 'html)
        (org-export-derived-backend-p org-export-current-backend 'odt))
    (format "%s" body))))

(defun my-lang-csquotes-org-backend (lang body)
  (cond
   ((org-export-derived-backend-p org-export-current-backend 'latex)
    (format "@@latex:\\begin{otherlanguage*}{%s}\n\\EnableQuotes@@\n%s\n@@latex:\\end{otherlanguage*}@@" lang body))
   ((or (org-export-derived-backend-p org-export-current-backend 'html)
        (org-export-derived-backend-p org-export-current-backend 'odt))
    (format "%s" body))))

Y a continuación, mi definición del bloque de código org, que tomará dos parámetros: lang y lang-quotes (para la versión con \EnableQuotes):

(defun org-babel-execute:org (body params)
  "Execute a block of Org code with.
This function is called by `org-babel-execute-src-block'."
  (let ((result-params (split-string (or (cdr (assq :results params)) "")))
        (lang (cdr (assq :lang params)))
        (lang-quotes (cdr (assq :lang-quotes params)))
        (body (org-babel-expand-body:org
               (replace-regexp-in-string "^," "" body) params)))
    (cond
     (lang
      (my-lang-org-backend lang body))
     (lang-quotes
      (my-lang-csquotes-org-backend lang-quotes body))
     ((member "latex" result-params)
      (org-export-string-as (concat "#+Title: \n" body) 'latex t))
     ((member "html" result-params) (org-export-string-as  body 'html t))
     ((member "ascii" result-params) (org-export-string-as body 'ascii t))
     (t body))))

Un ejemplo de uso, con un texto de Hegel:

#+begin_src org :lang german :results replace :exports results
  Eine Erklärung, wie sie einer Schrift in einer Vorrede nach der Gewohnheit
  vorausgeschickt wird ---über den Zweck, den der Verfasser sich in ihr vorgesetzt, sowie
  über die Veranlassungen und das Verhältnis, worin er sie zu andern frühern oder
  gleichzeitigen Behandlungen desselben Gegenstandes zu stehen glaubt--- scheint bei einer
  philosophischen Schrift nicht nur überflüssig, sondern um der Natur der Sache willen sogar
  unpassend und zweckwidrig zu sein.
#+end_src

Bloques `quote’ con atributos de LATEX.

Para la versión de desarrollo de Org (que se puede descargar desde su repositorio Git) me aceptaron un parche que escribí para que los bloques quote aceptaran opciones y atributos de LATEX5. Dado que muchos pasajes independientes en lengua extranjera son citas en bloque dentro de un documento, aprovechar el bloque quote nos viene bien para mantener la coherencia. Mediante mi parche podemos, pues, definir cosas como estas (aprovechándonos, una vez más, de las facilidades del paquete csquotes: en este caso del entorno foreigndisplayquote):

#+LaTeX_Header:\usepackage[german,english]{babel}
#+LaTeX_Header:\usepackage{quoting}
#+LaTeX_Header:\usepackage[babel=true,autostyle=true,german=quotes]{csquotes}
#+LaTeX_Header:\SetBlockEnvironment{quoting}

#+ATTR_LaTeX: :environment foreigndisplayquote :options {german}
#+begin_quote
Eine Erklärung, wie sie einer Schrift in einer Vorrede nach der
Gewohnheit vorausgeschickt wird ---über den Zweck, den der Verfasser
sich in ihr vorgesetzt, sowie über die Veranlassungen und das
Verhältnis, worin er sie zu andern frühern oder gleichzeitigen
Behandlungen desselben Gegenstandes zu stehen glaubt--- scheint bei
einer philosophischen Schrift nicht nur überflüssig, sondern um der
Natur der Sache willen sogar unpassend und zweckwidrig zu sein (Hegel).
#+end_quote

Publicado: 16/07/21

Última actualización: 16/08/23


Índice general

Acerca de...

Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

Notas:

1

La inclusión de un carácter de espacio cero es un recurso bien conocido en Org para evitar malos contactos de las marcas de énfasis. Para más información, puede consultarse en el manual de Org la sección «16.11 Escape Character».

2

También hice un uso intensivo de ese recurso en mi paquete para Emacs org-critical-edition, que escribí para componer ediciones críticas desde Org Mode: https://gitlab.com/maciaschain/org-critical-edition/-/tree/master

3

Si se quiere mostrar el enlace sin desplegar hay que eliminar el parémetro :display. El aspecto con que se mostrará el link lo controla el parámetro :face. Aquí uso, por comodidad, una «face» ya definida en el paquete magit: magit-header-line-key. Puede definirse cualquier otra «face» nueva ad hoc o, simplemente, si queremos que los links tengan el aspecto estándar, no incluir este parámetro.

4

Que nos cargue sólo los patrones de guionado y no todas las propiedades de una lengua es muy práctico en estos escenarios, ya que ciertas propiedades (como los espaciados en los signos de puntuación del francés) podrían producir efectos indeseados.

5

Por defecto, el bloque quote sólo produce el entorno quote en la salida a LATEX.

© Juan Manuel Macías
Creado con esmero en
GNU Emacs