domingo, 11 de noviembre de 2012

Internacionalización práctica en MVC4 con Razor y Code52

Lo qué

Luego de dar vueltas todo un fin de semana buscando una forma fácil de hacer un sitio web MVC4 multi idioma / multi cultura, hago aquí un rejunte de la parva de artículos leídos. En esta notas pongo en práctica la forma que me pareció más directa de implementarlo. No se si es la mejor (me lo dirán en sus comentarios), pero la forma más simple que encontré es utilizando Code52, incluido como paquete Nuget.
No voy a explicar los por qué de cada cosa, lo que dejo librado a la inquietud de cada uno. Así que vamos derecho a hacer un ejemplo completo, paso a paso y con muchas imágenes.
El proyecto con el código de este ejemplo está disponible aquí. (hacer clic en "Archivo > Descargar" para bajarlo completo).

Qué se necesita

Para este ejemplo voy a utilizar Visual Studio 2010 con C#. Es necesario tener instalado la extensión Nuget para Visual Studio. Se puede instalar y obtener la documentación de Nuget en http://nuget.codeplex.com/.

Luego de eso es necesario instalar el paquete MVC4.
Para esto, abrir la consola Nuget de administación de paquetes (PM) desde Tools  > Library Package Manager > Package Manager Console. En la consola escribir  PM> Install-Package AspNetMvc (y darle Enter).


Una vez descargado e instalado estará todo listo para crear sitios Web con MVC4.

Crear el sitio

Vamos a crear un sitio Web del tipo MVC4 que para este ejemplo se llamará "GlobalMVC4"


El tipo de aplicación será Internet Application con Razor



Una vez creado el sitio, vamos a verificar que funcione correctamente, para lo que presionamos F5. Si se creó correctamente, se verá la página de inicio (Home).


detenemos el sitio y pasamos a instalar el paquete internacionalización, lo que finalmente nos mostrará un menú de idiomas para seleccionar alguna de las culturas disponibles.

Code52.i18n

Este es el paquete que vamos a utilizar para manejar la internacionalización de nuestro sitio. Para instalarlo, en la consola de Nuget debemos escribir:  PM> Install-Package Code52.i18n.MVC4
Toda la información sobre el paquete está en el sitio de Code52.org. La mayor parte sobre la que se basa este artículo está en esta guía paso a paso para configurar i18n.

Como se puede ver en la imagen, luego de descargado se instalan varios archivos nuevos, que pueden verse también en las carpetas y archivos de la solución. Los paquetes instalados por Nuget quedan además registrados en el archivo packages.config



Listos para comenzar

A partir de este momento hay que trabajar en el sitio para poner todo lo instalado a funcionar. Así que vamos paso a paso:

Preparar los archivos

En el directorio raíz, crear una carpeta llamada "Resources", y dentro de ella agregar un archivo de recursos llamado "Language.resx". Este es el nombre de archivo de recursos predeterminado con que trabaja Code52.i18n. Se puede utilizar otro, pero habrá que cambiar luego las referencias dentro del código.



Nuestro directorio quedará como la imagen siguiente. Se ve el nombre de la entrada String1 en el archivo de recursos, donde luego pondremos nuestros textos para cada idioma.


Agregar los recursos de idioma para cada cultura

Cuando se instala el paquete de internacionalización se crean tres culturas a modo de ejemplo: Polaco, Inglés y Francés. Como esta nota está den español, para nuestro sitio vamos a cambiar el francés y el polaco por Español Argentina (es-AR) y Portugués Brasil (pt-BR).

Así que lo que vamos a hacer ahora es crear una entrada de texto para el idioma predeterminado (en este caso dejamos inglés) y la vamos a reproducir en español y portugués.

En el archivo de recursos, reemplazar la clave "String1" por "Home_Title", así:


Luego vamos a copiar y pegar dos veces el archivo "Language.resx", uno para español Argentina y el otro para Portugués Brasil. Renombrar cada uno como "Language.es-AR.resx" y "Language.pt-BR,resx". Cabe notar que a pesar de que "Language.resx" es el archivo predeterminado para inglés de Gran Bretaña, no tiene en realidad ninguna cultura definida (ni general ni específica), por lo que podríamos en realidad utilizarlo para cualquier idioma de forma predeterminada. Yo en general construyo toda la aplicación utilizando este archivo, y al final suelo agregar los otros idiomas o culturas.


Ahora hay que editar las entradas para cada idioma. En este caso, para la única entrada que tiene casda uno, en "Language.es-AR.resx" ponemos "Página de inicio", y en "Language.pt-BR" ponemos "Tela de inicio" (no es traducción literal, es solo a los fines de notar el cambio de idioma).



Editar páginas y clases

Ya tenemos los recursos. Ahora hay que modificar algunos archivos del sitio para poner en acción el paquete de Code52 y hacer que funcione:

Abrir la página "_Layout.cshtml" (Views/Shared/_Layout.cshtml) y al principio de todo agregar:
@using GlobalMVC4.Code52.i18n

Luego, dentro del cuerpo agregar en algún lado la siguiente línea:
@Html.Partial("LanguageSelection")

(la ubicación depende del diseño, cosa que por ahora no vamos a ver).

Nota: "GlobalMVC4" corresponde al espacio de nombres de nuestra aplicación.


4 - La primer línea de código que agregamos corresponde a una vista parcial que el paquete agregó como un control .ascx (Views/Shared/LanguageSelection.ascx) y que editaremos más adelante. Este control utiliza una hoja de estilos predeterminada que trae el mismo paquete, por lo que el siguiente paso es hacer referencia a esta hoja de estilos (Content/Code52.i18n/Code52.i18n.css) en las páginas de nuestro sitio.

La última versión de MVC4 incorpora una nueva estructura que facilita las configuraciones generales y otras funciones que se agregaban normalmente al Global.asax. Se verá por lo tanto una nueva carpeta llamada "App_Start", donde editaremos el archivo "BundleConfig.cs" (App_Start/BundleConfig.cs).
Dentro de esta clase buscamos la línea donde se configura la hoja de estilos general del sitio.
bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

Editarla de esta manera para incluir la página de estilos que utiliza el menú de idiomas (el agregado está resaltado):
bundles.Add(
new StyleBundle("~/Content/css")
.Include("~/Content/site.css"
, "~/Content/Code52.i18n/Code52.i18n.css"));

Agregar Sripts

Ahora es necesario agregar las siguientes referencias a los scripts necesarios. Al igual que la hoja de estilos, podrían agregarse también en BundleConfig.cs, pero ahora por cuestiones de simplicidad lo hacemos directamente en la vista _Layout.cshtml, así que justo antes del cierre del tag </body> agregamos lo siguiente (recomiendo copiar y pegar, presionando luego Ctrl+K+D para acomodar el indentado :))

<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/jquery.validate.min.js"></script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.globalize/globalize.js")"></script>
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery.cookie.js")"></script>
<script type="text/javascript"     src="@Url.Content(string.Format("~/Scripts/jquery.globalize/cultures/globalize.culture.{0}.js", CultureHelper.GetCurrentCulture()))"></script>
@if (CultureHelper.GetCurrentNeutralCulture() != "en")
{
<script type="text/javascript" src="@String.Format("http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/localization/messages_{0}.js", CultureHelper.GetCurrentNeutralCulture())"></script>    
}
<script type="text/javascript" src="@Url.Content("~/Scripts/Code52.i18n.js")"></script>
<script type="text/javascript" src="@Url.Content("/i18n/Code52.i18n.language.js")"></script>
<script type="text/javascript">
    Code52.Language.Init('@CultureHelper.GetCurrentCulture()');    
</script>

Debería quedar así:

Editar el menú de idiomas

Llegó el momento de editar el selector donde el usuario elige el idioma. Para esto editaremos el control .ascx al que le hicimos referencia antes en la vista general (_Layout.cshtml).

Abriendo el control encontraremos los elementos del menú de idiomas. El archivo instalado por el paquete viene con las opciones para seleccionar inglés de Gran Bretaña (en-GB), francés de Francia (fr-FR) y polaco de Polonia (pl-PL). 
Quizás para cuando leas esto ya esté corregido, pero de momento el código viene con un error, donde se repite dos veces el atributo href, por lo que hay que eliminarlos (indicados con las flechas).


Vamos a editarlos cambiando el francés y el polaco por el español y el portugués respectivamente. Nuestro menú quedará así:

Cabe aclarar que pueden utilizarse idiomas sin referencia a cultura alguna (parent culture), por ejemplo definiendo "es" en lugar de "es-AR" para español sin especificar país. 

Ahora hay que abrir el archivo "CultureHelper.cs" (Code52.i18n/CultureHelper.cs), donde encontraremos las colecciones de las culturas soportadas y las utilizadas en el sitio. Para eso vamos a modificar las que trae de manera predeterminada, como se ve en la imagen:

Cambiamos el francés (fr) por es-AR y el polaco (pl) por pt-BR. Como se puede ver, el primero de la lista es el que será el predeterminado del sitio cuando el usuario aún no haya seleccionado ninguno.


Aplicar las referencias

Queda finalmente la parte más pesada dentro de la programación de una aplicación que soportan múltiples culturas: aplicar los textos en los controladores y las vistas. Y siguiendo con este ejemplo, vamos a utilizar nuestra entrada que creamos en cada uno de los tres idiomas. Para esto, abrimos el controlador de la home page y cambiamos el texto fijo que se ve aquí...


... por nuestra referencia al recurso correspondiente. El IntelliSense nos ayuda a encontrar la entrada de nuestro archivo de recursos:


Comprobando el resultado

Teniendo ya todo junto, es hora de probarlo, así que a darle a F5 para correr el sitio. Veremos que aparece la opción de seleccionar idioma (no se por qué no me apareció la banderita de Brasil).



He aquí la selección de cada uno:


Conclusión

Sobre el tema de internacionalización de aplicaciones hay mucha tela para cortar aún. Especialmente en los sitios MVC, una cuestión a tener bien en cuenta es el tema de las validaciones de fechas y decimales, cosa que no hemos tratado aquí.

Por otra parte, y si bien esto escapa también al alcance de esta nota, una de las cosas que más me gustan de construir aplicaciones MVC con Visual Studio es el Scaffolding, para lo cual antes de construir la aplicación con soporte internacional yo suelo modificar las plantillas T4, para que cada vista y controlador creados a partir del modelo ya traigan los botones y textos haciendo referencia a los archivos de recursos.

En resumen, espero que esto les sea de utilidad. Dado que me costó encontrar una guía completa que funcionara quise ser lo más detallista posible. Espero que no haya grande cambios en los paquetes y/o clases, cosa de que este artículo sirva y tenga vigencia, pero como sabemos, eso con la tecnología y tratándose de Microsoft, es bastante impredecible.

Saludos y éxito en la tarea!


30 comentarios:

  1. Gracias, me ah sido de mucha ayuda.

    para agregar la bandera te vas al archivo Code52.i18n.css (Content/code52.i18n/Code52.i18n.css ) y creas un estilo para tu idioma
    .language_ES_MX
    {
    background-image: url(/content/code52.i18n/images/flags/es_mx.png);
    }

    y pones la bandera en la carpeta (Content/code52.i18n/images ) con el nombre que pusiste en tu estilo "es_mx.png"

    ResponderEliminar
    Respuestas
    1. Muchas gracias por los comentarios y sobre todo por el aporte.
      Saludos!

      Eliminar
  2. No me funcina con MVC3, ya que sale un error , hay manera de hacerlo con la version 3 del MVC?

    ResponderEliminar
    Respuestas
    1. No hice las pruebas, y no se de qué error se trata, pero asegurate de instalar el paquete de la librería correcta. Para MVC3 sería
      "PM> Install-Package Code52.i18n.MVC3"

      Eliminar
  3. Rápido y conciso, por estas fechas aún funcionó a la primera, muchas gracias por compartir tu trabajo

    ResponderEliminar
    Respuestas
    1. Por el contrario. Comentarios como el tuyo son el gran incentivo de saber que se puede devolver algo a la comunidad de la que uno siempre está aprendiendo.
      Muchas gracias!

      Eliminar
  4. Muchas gracias por el aporte, lo intenté en un proyecto de ejemplo, pero cuando quise implementarlo en un proyecto grande no funcionó, imagino que tiene que ver con el lugar donde coloco el: @@Html.Partial("LanguageSelection"), ¿Podrías explicarme un poco sobre eso?.... al revisar la página paso por paso con la ayuda de la consola de chrome, me doy cuenta de que la lista con las opciones aparece y desaparece al instante y si la activo, no hace nada.....

    ResponderEliminar
    Respuestas
    1. ¿Están todos los scripts y estilos bien registrados? Seguramente que puede tener que ver con el lugar dónde estás poniendo el menú de idiomas. Intenta reemplazar el @Html.Partial(...) directamente por el código que correspondiente. Verifica y compara el código del ejemplo que te funcionó con el de tu proyecto. Las causa pueden ser muchas, hasta un html mal formado...

      Eliminar
    2. Que tal, al parecer si era problema del manejo de estilos, ya lo he resuelto...

      Ahora una pregunta más, ¿Puedo utilizar más de un archivo de recursos, es decir, uno por vista?, esto porque tengo varias vistas y mantenerlas por medio de un solo archivo me parece que se volverá tedioso mientras vaya creciendo... ¿Alguna recomendación o guía para saber que hacer?

      Eliminar
    3. Los archivos de recursos son "stellite assemblies", y según esta página del msdn (http://msdn.microsoft.com/es-ar/library/ms227427.aspx), en ASP.NET varios archivos de recursos se compilan todos juntos para un mismo idioma, siendo la compilación dinámica. Así que si la performance mejora, con muchos archivos o uno solo, parecería que no habría grandes diferencias (no lo se). Usar uno o varios tiene a mi modo de ver diversas ventajas y desventajas. Un solo archivo es más fácil de manipular si hay que enviárselo a alguien que no programa para que lo traduzca, y además muchos términos pueden reutilizarse en todo el sitio (por ejemplo la palabra "Guardar"). Varios archivos ayuda a mantener más aislada cada vista, pero con ese concepto hay muchas palabras que se repiten. Lo que no me parece que haya que hacer es mezclar los conceptos, aunque también se podría mantener un recurso general en común. Según esta otra página (http://odetocode.com/blogs/scott/archive/2009/07/16/resource-files-and-asp-net-mvc-projects.aspx), hay que evitar los archivos globales porque pueden perjudicar el desarrollo basado en TDD. Lo que yo hago la mayoría de las veces es poner todo en un solo archivo (con MVC, no con ASP.NET) y nombro los recursos comenzando con el nombre de la vista (ej: "Compras_PrecioRequerido", donde Compras es el nombre de la vista). Luego los generales para usar en todas las páginas no llevan prefijo (simplemnte, "Eliminar", por ejemplo).

      Los temas de arquitectura siempre son difíciles de debatir, así que es cuestión de cada uno y de cada proyecto. Pero a tu pregunta, si, podrías usar varios archivos de recursos separados.

      Eliminar
  5. ¿Cómo haz logrado cuando la información proviene de la base de datos?
    ¿Mantienes un campo para cada idioma para cada termino en cada tabla?

    Saludos!

    ResponderEliminar
    Respuestas
    1. Hay distintas formas de hacerlo. Dos de ellas pueden ser poner un campo para cada idioma (o cultura específica), o bien un campo idioma donde se guarde la cultura y en otro campo el término, y luego se filtra por el campo idioma. Dependerá de cuán dinámica sea la incorporación de nuevo idiomas a tu sitio si se elige una u otra, ya que cada una tiene sus ventajas y desventajas. Es cuestión de criterio al momento de diseñar, y a mi modo de ver no hay una mejor que otra.
      Saludos!

      Eliminar
  6. Excelente Trabajo amigo me ha ayudado montones

    ResponderEliminar
    Respuestas
    1. Me alegra que te sirva. Gracias!

      Eliminar
    2. Una consulta podría cambiar yo el selector de idioma por un select??? y como sería


      Eliminar
    3. Tendrías que reemplazar la lista que se encuentra en LanguageSelection.asx por tu select, y luego modificar la función jQuery que hace referencia al evento click de la selección ".language a", que está en el archivo Code52.i18n.js de la carpeta Scripts.
      En el caso del select, habrá que capturar el evento change con algo así como $('#miSelectId').change()
      No queda más que codificar y probar.

      Eliminar
    4. He estado tratando pero por alguna razón no me toma el .change()

      si pudieras hacer un ejemplo me serias de gran ayuda Gracias de antemano.

      Eliminar
  7. estimado, he leido tu posto y me ha servido mucho. de hecho implemente toda la aplicación y funciono de maravillas.

    pero aún tengo una pregunta ¿Puedo internacionalixar campos de BD?
    ahora tengo un registro por lenguaje lo que me esta sirviendo pero más adelante puede ser un problema.


    por favor requiero de su respuesta.

    ResponderEliminar
  8. My work environment is win7 + visual studio 2013 RC
    Me step by step as you approach to do it, but always error ..
    In http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/jquery.validate.min.js
    Can you please send your sample to me and my EMAIL to duke.topmost @ gmail.com
    Thank you.

    ResponderEliminar
    Respuestas
    1. Should not has to do with your work environment. I think your problem has more to do with your jQuery references. Try to verify the jQuery version of the files that you have in the Script folder on the example code (did you downloaded it from the first link at the top of the article?). If still not working please try the following:

      - Remove the script reference of the jquery.validate.js and replace it for the one in the Script folder of the example code (the script reference is located in the _Layout partial view).
      - Verify that all versions of the referenced files jQuery match between each other.
      - Try debugging the page using the browser developer tools (by pressing F12 at your browser. It differs depending on the browser that you are using).
      - Try with another browser.

      I would like to be more specific, but I couldn't reproduce your error.
      Regards

      Eliminar
  9. Saludos, tengo visual studio 2012 y he puesto todo como se menciona en el tutorial y tengo un problema con esta referencia src="@Url.Content("/i18n/Code52.i18n.language.js") la cual no es encontrada cuando la pagina esta cargada, según entiendo esa es la referencia para poder traducir los mensajes puestos en javascript, me gustaría que me digas si conoces alguna forma de solucionar este problema, Gracias.

    ResponderEliminar
    Respuestas
    1. Hola ¿bajaste el código del ejemplo? ¿instalaste correctamente el paquete nuget de Code52 desde la consola del package manager?
      En todo caso intenta buscar si la referencia está bien agregada utilizando las herramientas de desarrollo de tu navegador (tecla F12).
      Busca el archivo y modifica la referencia para que apunte correctamente, desde la vista directamente o mediante la referencia a MVC bundles en tu proyecto.
      Saludos!

      Eliminar
    2. Buenas, ya lo tengo funcionando correctamente excepto por un detalle, y es que cuando cambio el idioma y hace el refresh el diccionario javascript con los mensajes localizados tarda en actualizarse al nuevo mensaje, ¿alguna idea de como yo puedo hacer que ese diccionario se actualize instantaneamente? Gracias por la ayuda. :D

      Eliminar
    3. Cuando digo mensaje, me refiero a los mensajes en el idioma seleccionado, mi aplicación está soportando básicamente 2 lenguajes, pero cuando hago el cambio se queda en el lenguaje anterior el diccionario, hasta luego de un refresh extra, y no debería funcionar así.

      Eliminar
    4. Evidentemente hay un problema, pero vas a tener que hacer debugging para ver dónde está. Con Visual Studio para el lado del servidor o con las herramientas de desarrollo del navegador para el lado del cliente.
      Saludos!

      Eliminar
    5. ¿Ese problema no te llegó a suceder a tí?

      Eliminar
    6. La verdad que no. ¿cómo te funciona la aplicación de ejemplo? ¿te aparece el mismo problema?

      Eliminar
    7. Por fin dí con la solución, sencillamente era el outputcache que tenia el controller de idioma que dejaba el contenido cacheado por alrededor de 60 segundos, entonces cuando yo cambiaba de idioma no lo hacía y se quedaba el contenido cacheado, esto debe estar pasando quizá porque no está reconociendo el VaryByCustom, o talvez porque tenga que hacerle un override explicito en el GlobalAsax. De todas formas gracias por tu ayuda. :D

      Eliminar
  10. Excelente post....lo voy aplicar en un proyecto.

    Mil gracias por tu aporte

    ResponderEliminar
  11. Recomiendo https://poeditor.com como una herramienta muy útil para la internalización. Se puede utilizar para muchos tipos de archivos: cuerdas, po, pot, resx, Excel y tiene una interfaz sencilla y API también.

    ResponderEliminar