(cc) Foto por woodleywonderworks

Aprovechando la reciente traducción de este sitio web al inglés, voy a dedicar este post a explicar cómo gestionar la traducción de contenidos en ASP.NET MVC multi idioma, cuando hablamos de contenido con cierto tamaño y formato, es decir, más allá de los archivos de recursos, útiles sobre todo para palabras o frases cortas.

Traducción mediante archivos de recursos

En mi caso, al iniciar la traducción, empecé utilizando archivos de Recursos (.resx), que permiten disponer de una librería de recursos organizados por clave-valor, en este caso, cadenas, donde es el propio servidor el que gestiona qué idioma mostrar.

Para ello, se dispone de un archivo «Content.resx» donde se definen todos los textos que vamos a utilizar, con su clave correspondiente:

resources1.png

A continuación, creamos un archivo con el mismo nombre, pero añadiéndole el sufijo del código ISO del idioma al que queremos traducir, que en el caso del inglés es «en». Por tanto, el archivo será «Content.en.resx», y duplicamos las claves del archivo original, pero traduciendo el contenido en este caso:

resources2.png

La aplicación utiliza los valores de:

Thread.CurrentThread.CurrentUICulture
Thread.CurrentThread.CurrentCulture

para determinar el idioma a utilizar, y para ello utilizará el sufijo que hemos añadido al archivo de recursos. En caso de no encontrar ningún archivo con el sufijo correcto («de», por ejemplo), utilizará el archivo sin sufijo como fallback. Por eso este archivo sin sufijo siempre representa el idioma por defecto de nuestro contenido.

Las ventajas de este sistema es que al estar los recursos dentro del ensamblado, el acceso a los mismos es inmediato y es el sistema el que se encarga de decidir qué archivo de recursos utilizar en función del idioma, liberándonos de dicha tarea.

En nuestro ejemplo, ya podemos utilizar en la vista el siguiente código (si Content.resx está en el raíz de la aplicación):

<h4>@Content.Academic</h4>

que mostrará el subtítulo «Formación académica» en la tercera pestaña de la página «/es/about», mientras que mostrará «Academic» en la página «/en/about». En este caso, utilizo un parámetro en cada ruta «{culture}/about» que indica el idioma a seleccionar.

El código de ejemplo que gestiona el cambio de idioma sería:

String routeCulture = requestContext.RouteData.Values["culture"].ToString();

CultureInfo cultureInfo = new CultureInfo(routeCulture);
Thread.CurrentThread.CurrentUICulture = cultureInfo;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);

Que implica que hemos definido en nuestra tabla de rutas que éstas tienen un parámetro {culture} en cada una de ellas. La gestión de rutas multi idioma la dejaremos para otra publicación.

Pero utilizar archivos de recursos tiene dos problemas fundamentales:

  • Al ser archivos compilados, es necesario volver a compilar y publicar las DLLs del site para cualquier modificación, lo que le resta versatilidad y mantenimiento.
  • Cuando necesitamos traducir grandes bloques de contenido, que a su vez incorporan formato HTML, se convierte en un formato poco manejable y que mezcla formato con contenido. Como ejemplo, si accedemos a una página con descripción de proyecto, como /es/Projects/web-personal, observamos que las secciones de «Descripción del proyecto» o «Responsabilidades y tareas» contienen gran cantidad de texto y formato.

Markdown al rescate

Para solucionarlo he separado el contenido en archivos de Markdown para editar el contenido. La sintaxis de Markdown es muy sencilla, y se hizo muy popular para la edición de Wikis, por ejemplo. Existen diversos editores para modificar archivos markdown, si no queremos tener que aprender la sintaxis. Yo estoy utilizando Markdown Pad 2.

Para diferenciar los idiomas, sigo utilizando la misma filosofía de sufijos ISO que con los archivos de recursos, añadiendo «.en» a los archivos en inglés. De esta manera, por ejemplo, para traducir el contenido de la sección «Responsabilidades y tareas» del ejemplo anterior, /es/Projects/web-personal, tenemos 2 archivos: Project12-tasks.md y Project12-tasks.en.md. De esta manera, la gestión del contenido se hace con un editor especializado, que me permite modificar el formato de manera sencilla, y los cambios son visibles con tan sólo subir el archivo afectado, sin tener que recompilar el site.

La edición de un archivo de markup es tan sencilla como abrir el archivo con el editor y realizar las modificaciones al contenido:

project12_tasks.png

Como se puede observar, el propio archivo de markdown permite incluir códigos HTML, aunque luego no los muestre en la vista previa. Esto es importante porque nos permite total flexibilidad con nuestro contenido y luego veremos como este código HTML insertado sí que es tenido en cuenta a la hora de generar el HTML asociado.

A modo de ejemplo, así es como ha quedado organizado el contenido asociado con el proyecto 12 completo, /es/Projects/web-personal:

project_md.png

Donde observamos un archivo de recursos para los textos sencillos, y dos archivos de markdown para las dos secciones complejas, y en cada caso, la correspondiente réplica en inglés.

De markdown a HTML

Obviamante, el formato markdown no se puede mostrar directamente en el navegador como tal, sino que tenemos que transformarlo al HTML correspondiente y enviarlo a la vista.

Para ello, utilizo la librería de código abierto MarkdowDeep, que nos permite transformar el contenido markdown en HTML con muy poco esfuerzo. También implementa una librería de cliente que nos permitiría editar el markdown desde el navegador, mediante javascript.

Para instalar la librería, utilizaremos Nuget. Existen dos versiones de la librería: una que sólo incluye la parte de .NET y otra que incluye además el código de cliente. En mi caso, sólo he necesitado la versión de .NET, por lo que procedemos a su instalación, desde la Consola de Nuget:

PM> Install-Package MarkdownDeep.NET

O a través del gestor de paquetes de Nuget:

markdowndeep.png

Una vez instalada, la manera más sencilla de utilizar es creándonos una extensión de HtmlHelper que gestione esta tarea:

    /// <summary>
    /// Lectura de archivos de contenido en markdown
    /// </summary>
    public static class MarkdownExtension
    {
        #region "Métodos"

        public static IHtmlString RenderMarkdown(this HtmlHelper helper, string filename)
        {
                var selectedLanguage = helper.ViewContext.Controller.ControllerContext.GetSelectedLanguage();
                var pathSelected = helper.ViewContext.HttpContext.Server.MapPath(filename + "." + selectedLanguage + ".md");
                var pathDefault = helper.ViewContext.HttpContext.Server.MapPath(filename + ".md");
                if (!File.Exists(pathSelected))
                    if (!File.Exists(pathDefault))
                        return helper.Raw("File " + pathDefault + " not found");
                    else
                        pathSelected = pathDefault;

                // Load source text
                var text = File.ReadAllText(pathSelected);

                // Setup processor
                var md = new MarkdownDeep.Markdown
                {
                    SafeMode = false,
                    ExtraMode = true,
                    AutoHeadingIDs = true,
                    MarkdownInHtml = true,
                    NewWindowForExternalLinks = true
                };

                // Write it
                return helper.Raw(md.Transform(text));
            }

        #endregion

    }

Notas a destacar:

  • La función recibe como parámetro el nombre base del archivo a generar, por ejemplo «/Texts/Projects/Project12/Project12-desc», y ella ya se encarga de añadirle el sufijo de idioma correspondiente y la extensión «.md»
  • Si no encuentra el archivo con la extensión del idioma seleccionado, utiliza como fallback el archivo por defecto sin idioma.
  • La lectura del archivo se puede almacenar en Caché, si queremos aligerar la carga de proceso, aunque según mis pruebas, y al ser archivos relativamente pequeños, la mejora es muy poca, del orden de 2ms por archivo.
  • Al instanciar la librería de Markdown con la propiedad MarkdownInHtml = true, permite que el HTML incluido en el archivo se incorpore también a la salida.
  • Finalmente, devolvemos el resultado de la generación con Raw() para que el HTML no se interprete como texto, sino como salida tal cual.
  • La línea de código:
var selectedLanguage = helper.ViewContext.Controller.ControllerContext.GetSelectedLanguage();

obtiene el idioma seleccionado, en mi caso, de la ruta. Esta línea dependerá de la implementación del idioma de cada uno.

Una vez tenemos la extensión implementada, tan sólo nos queda utilizarla en nuestra vista:

@Html.RenderMarkdown("/Texts/Projects/Project12/Project12-desc")

Como nota adicional, he tenido problemas si la ruta del archivo no es un literal de texto, sino una cadena dinámica. En este caso, en lugar de utilizar la extensión, he tenido que utilizar directamente la clase estática para acceder al método:

@MarkdownExtension.RenderMarkdown(Html, "/Texts/Projects/Project" + Model.Id.ToString() + "/Project" + Model.Id.ToString() + "-desc")

Conclusiones

La combinación de archivos de recursos para textos cortos y sencillos, con archivos de texto para contenido más elaborados y con formato (en este caso mediante Markdown) nos permiten realizar una gestión sencilla de aplicaciones multi idioma, fácilmente ampliables a nuevos idiomas en un futuro y que facilitan la actualización de los contenidos en un futuro.

Seguramente hay otras soluciones y cada uno tiene sus trucos, pero esta es la que he utilizado yo en mi propia web. ¿Y tú, cómo gestionas grandes bloques de contenido multi idioma? ¡Se aceptan sugerencias!