lunes, 3 de enero de 2011

Traducir aplicaciones ASP.NET

 Como decía en un anterior post http://panicoenlaxbox.blogspot.com/2010/12/localizacion-y-globalizacion-en.html, estoy en proceso de internacionalizar una aplicación importante de mi empresa.

Habiendo resuelto la localización de ficheros javascript, ahora queda traducir la interfaz de usuario, esto es las páginas .aspx.

Para traducir nuestra aplicación tenemos que utilizar ficheros de recursos (.resx) que podrán ser de 2 tipos: locales (sólo para una página) o globales (visibles para todas las páginas de nuestra aplicación). Estos ficheros de recursos tendrán todos los literales utilizados en nuestra aplicación, así en vez de escribir literalmente en una página el texto “Bienvenido”, lo que haremos será acceder al fichero de recursos apropiado y recuperar el valor asociado a la clave “Bienvenido”, que en Español nos devolverá “Bienvenido” y en Inglés nos devolverá “Welcome”.

Por supuesto, podremos tener tantas versiones del mismo fichero .resx como lenguajes distintos soporte nuestra aplicación y además también podremos detectar el lenguaje preferido del usuario (según las preferencias de su navegador), así como forzar un lenguaje concreto desde código (porque por ejemplo le ofrecemos un combo para seleccionar el idioma en la página de login).

Como hemos dicho antes, existen 2 tipos de recursos: locales y globales. Ambos recursos se guardan en carpetas especiales de ASP.NET. Los recursos locales se guardan en carpetas con el nombre App_LocalResources mientras que todos los recursos globales se guardan en una única carpeta llama App_GlobalResources. Además de esta diferencia, también el nombre que los ficheros .resx tienen diferencias en función de si son locales o globales.

Recursos locales

El nombre de un fichero de recursos local, será NombrePagina.aspx[.idioma[-cultura]].resx y la carpeta App_LocalResources que lo contiene tiene que estar al mismo nivel que la página .aspx para la que se va a utilizar.

En NombrePagina.aspx[.idioma[-cultura]].resx, vemos que tanto idioma como cultura son opcionales. Esto es porque en caso de no aparecer ninguno de estos valores, lo que estamos haciendo es un fichero de recursos “neutral” que se utilizar cuando no se encuentre ningún fichero de recursos para el lenguaje solicitado por el usuario. De hecho, es práctica casi obligada suministrar un fichero de recursos neutral (la traducción base por llamarlo de alguna forma) y después tantos ficheros de recursos con lenguaje explícito en función de los lenguajes que nuestra aplicación deba soportar (además del lenguaje base).

Por ejemplo, para una página llamada Categorias.aspx podríamos tener:

  • Categorias.aspx.resx
  • Categorias.aspx.en.resx
  • Categorias.aspx.en-gb.resx

Categorias.aspx.resx es el lenguaje neutral (en nuestro caso Español), Categorias.aspx.en.resx es el lenguaje Ingles y Categorias.aspx.en-gb.resx es Ingles pero de Gran Bretaña (porque por ejemplo nuestros jefes son británicos y quieren ver escrito centre en vez de center, http://crofsblogs.typepad.com/english/2005/04/center_or_centr.html). De este modo, cualquier usuario que acceda a la web y que no seleccione el idioma “Ingles” o “Ingles británico”, navegará con el idioma “Neutral (Español)”.

Un recurso local se puede generar automáticamente desde Visual Studio si tenemos activa la vista diseño de la página .aspx y pulsamos la opción Herramientas\Generar recurso local. Esta acción genera un fichero NombrePagina.aspx.resx en una carpeta del mismo directorio donde esté alojada la página (si no existe la crea) llamada App_LocalResources. Además, los cambios que ha realizado en la página son los siguientes:

  • Agregar la propiedad culture=”auto”, uiculture=”auto” y meta:resourcekey=”PageResource1” a la directiva Page.
  • Agregar para todos los controles de la página la propiedad meta:resourcekey=”NombreControlResource1” e iterar por todas sus propiedades de texto para generar el fichero de recursos local.

Por ejemplo, antes de generar el recurso local, nuestra página tenía el siguiente código

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title></title>

</head>

<body>

    <form id="form1" runat="server">

    <asp:GridView ID="GridView1" runat="server">   

    </asp:GridView>

    <asp:Literal ID="Literal1" runat="server" Text="Hola mundo!!" ></asp:Literal>

    </form>

</body>

</html>

 

Después de generar el recurso local, tiene este aspecto

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default"

    Culture="auto" meta:resourcekey="PageResource1" UICulture="auto" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title></title>

</head>

<body>

    <form id="form1" runat="server">

    <asp:GridView ID="GridView1" runat="server" EnableModelValidation="True"

        meta:resourcekey="GridView1Resource1">   

    </asp:GridView>

    <asp:Literal ID="Literal1" runat="server" Text="Hola mundo!!"

        meta:resourcekey="Literal1Resource2"></asp:Literal>

    </form>

</body>

</html>

 

Además ha generado el fichero de recursos Default.aspx.resx.

clip_image001

Como vemos la clave está en las propiedades meta:resourcekey que enlazan los controles con claves del fichero de recursos. A este tipo de sintaxis, ASP.NET la llama “sintaxis implícita”, frente a la “sintaxis explícita” que veremos más adelante.

Cabe mencionar que aunque de forma automática, VS agrega las propiedades Culture y UICulture a la directiva Page, pero en realidad sólo son necesarios si queremos hacer uso de las mismas. Igualmente, el atributo meta:resourcekey de la directiva Page es para el título de la página, por lo que si no es necesario tampoco es obligado.

Aunque el nombre de las entradas en el fichero de recurso sea IDControlResource1.Propiedad, realmente lo único imprescindible es que se respeta el formato CualquierNombre.Propiedad, por lo que podríamos cambiar Literal1Resource1.Text a MiLiteral.Text, siempre y cuando cambiemos también el valor de la propiedad meta:resourcekey en el marcado de la página.

Este proceso podemos llevarlo a cabo tantas veces como queramos para la página actual con las siguientes consideraciones:

  • Si eliminamos un control de la página, no se eliminan las entradas asociadas en el fichero de recursos, así que podríamos guardar entradas innecesarias en el fichero de recursos.
  • Nuevos controles se agregan al fichero de recursos si se vuelve a generar.
  • Nuevas propiedades se agregan igualmente al fichero de recursos al volver a generarlo.

Antes hablamos de “sintaxis implícita” y “sintaxis explícita”, pues bien, la “sintaxis explícita” responde al siguiente patrón: Propiedad = “<%$ Resources:EntradaRecurso%>”, esto es que por el fichero de recursos local no sólo tiene que contener entradas del estilo Control.Propiedad (esto es sólo para la sintaxis implícita y meta:resourcekey), también podemos agregar nuestras propias entradas y enlazarlas con la sintaxis explícita. Por ejemplo:

clip_image002

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" culture="auto" meta:resourcekey="PageResource1" uiculture="auto" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title></title>

</head>

<body>

    <form id="form1" runat="server">

    <asp:GridView ID="GridView1" runat="server" EnableModelValidation="True" meta:resourcekey="GridView1Resource1">

    </asp:GridView>

    <asp:Literal ID="Literal1" runat="server" Text="Hola mundo!!" meta:resourcekey="MiLiteral"></asp:Literal>

    <asp:Literal ID="Literal2" runat="server" Text="<%$ Resources:MiEntrada %>"></asp:Literal>

    </form>

</body>

</html>

 

El problema de utilizar “sintaxis explícita” junto a autogenerar el fichero de recursos (las veces que sea necesario) es que al volver a generar, el fichero de recursos no “escaneará” Literal2 (hasta aquí todo bien y tiene sentido) pero a cambio borrara el valor “Sintaxis explícita” de la clave “MiEntrada” en el fichero de recursos. Yo no lo entiendo, pero…

En Internet hay amplios debates sobre la conveniencia de utilizar recursos locales o globales. El argumento que se esgrime siempre a favor de los recursos locales es que es sencillo generarlo (de hecho vemos que es automático) y que sólo carga en memoria para la página un fichero de recursos con una entradas limitadas (la que necesita la página), aunque esto último es sólo una percepción y no está basado en pruebas reales y monitorización de la carga de memoria. Por el contrario y según mi experiencia, una gran cantidad de páginas generarán una gran cantidad de recursos (muchos de ellos duplicados en gran medida) y eso en algún momento puede comenzar a resultar inmanejable. Además está el problema de que borre los valores para las entradas personalizadas en el fichero de recursos. En conclusión, para una aplicación pequeña y pasando siempre por el aro de meta:resourcekey podría ser una solución viable, pero para cualquier otro caso yo recomiendo recursos globales.

Recursos globales

Los recursos globales se almacenan (todos ellos) en una sola carpeta llamada App_GlobalResources que tiene que colgar de la raíz de nuestro sitio web y está disponible automáticamente para todas las páginas de nuestra aplicación.

No hay ninguna herramienta para generarlos automáticamente y el nombre no es relevante en el sentido de que no es el nombre de una página en concreto sino el nombre que nosotros queramos. En lo relativo a la convención de lenguaje y cultura sigue las mismas consideraciones que los recursos locales.

Con recursos globales siempre utilizamos “sintaxis explícita” pero además indicamos el fichero de recursos donde se encuentra la clave utilizada. Así, la forma de utilizar un recurso global es por ejemplo:

<asp:Literal ID="Literal2" runat="server"
Text="<%$ Resources:MiFicheroSinExtension,MiEntrada %>"
>
</
asp:Literal>
   

 

Recursos en tiempo de ejecución

Hasta ahora todo el enlace con los recursos ha sido de forma declarativa, pero puede suceder (y sucederá) que necesitemos recuperar el valor de alguna clave del fichero de recursos en tiempo de ejecución. Para ello tenemos disponibles las siguientes funciones:

·        GetGlobalResourceObject(nombreFicheroSinExtension, claveRecurso)

·        GetLocalResourceObject(claveRecurso)

 

Además, ASP.NET crea automáticamente clases autogeneradas dentro del espacio de nombres Resources que habilitan el acceso a ficheros de recursos globales de forma muy sencilla. Por ejemplo, si creamos un fichero de recursos global llamado “Comun” y agregamos la entrada “MiEntrada” podremos escribir lo siguiente en el código de nuestra aplicación:

Resources.Comun.MiEntrada

 

Acceder desde un proyecto externo a los recursos del sitio web

La verdad es que podría suceder (aunque yo soy partidario de que cada proyecto tenga sus propios recursos) pero si llegase el caso habría que agregar una referencia al ensamblado System.Web y acceder a los recursos a través del método GetGlobalResourceObject y a través del objeto HttpContext. Por ejemplo:

        Dim holaMundo As String = System.Web.HttpContext.GetGlobalResourceObject("Comun", "HolaMundo")       

 

Ficheros de recursos en una librería de clases

El crear el fichero de recursos no tiene ningún misterio. El cómo acceder quizás sí. Podemos optar por el espacio de nombres My (que aunque no me guste en este caso podría hacer la vista gorda) o acceder a pelo.

        Dim holaLibreria As String = ClassLibrary1.My.Resources.ComunEnLibreria.HolaLibreria

 

        Dim rm As New System.Resources.ResourceManager( _

            "ClassLibrary1.ComunEnLibreria", _

            System.Reflection.Assembly.GetExecutingAssembly())

        Dim holaLibreria2 As String = rm.GetString("HolaLibreria")

 

De hecho, si utilizamos la segunda forma de acceder, también podríamos acceder desde una biblioteca de clases a los recursos almacenados en otra biblioteca de clases.

Cultura automática

En anteriores puntos vimos que se puede incluir las propiedades Culture y UICulture en la directiva Page. Lo que hacen estas propiedades es definir la cultura y cultura de interfaz de la página .aspx. El valor más habitual es auto, que implica que ASP.NET automáticamente investigue la cabecera Accept-Languages que envía el navegador en cada petición, y asigne la cultura al primer lenguaje disponible. Además de auto, estas propiedades también podrían tener un valor de cultura fijo (por ejemplo es o en), pero no es lo habitual.

La cabecera Accept-Languages se puede configurar normalmente desde el navegador, por ejemplo en IE es la pantalla siguiente:

clip_image003

Esta aproximación es válida cuando queremos que el usuario no tenga que seleccionar un idioma, sino que automáticamente y en función de los lenguajes aceptados por su navegador, se seleccione el primero automáticamente.

Lo último a tener en cuenta en lo relativo a la cultura automática es que si queremos, podemos establecer estas propiedades para todas las páginas de nuestra aplicación a través del fichero web.config y la sección globalization.

<globalization culture="auto" uiCulture="auto" />

 

Cultura forzada

Es muy habitual en aplicaciones de negocio, permitir al usuario seleccionar en que idioma quiere acceder a la aplicación en vez de seleccionar automáticamente el primer idioma disponible en el navegador. De este modo, hay una serie de pasos para “forzar” al proceso de  ASP.NET a ejecutarse bajo el contexto de una cultura determinada hay que sobreescribir el método InitializeCulture de la clase System.Web.UI.Page. Lo que se hace normalmente es crear una nueva clase que herede de System.Web.UI.Page, sobreescribir este método y después hacer que todas nuestras páginas .aspx hereden de esta nueva clase.

Protected Overrides Sub InitializeCulture()

        ' Establecer cultura.

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("es-ES")

        Thread.CurrentThread.CurrentUICulture = New CultureInfo("es-ES")

        ' Guardar valores actuales para poder restaurar en futuras peticiones.

        Dim cookie As New HttpCookie("Culture", "es-ES")

        HttpContext.Current.Response.Cookies.Add(cookie)

End Sub

 

El código anterior fuera la cultura a es-ES, pero en un ejemplo real sería un parámetro que se recibiría por ejemplo desde la selección de un combo por parte del usuario.

Otro sitio donde colocar el anterior código y así tener que evitar tener que sobreescribir un método podría ser el evento Application_BeginRequest del fichero Global.asax.

    Protected Sub Application_BeginRequest(ByVal sender As Object, ByVal e As System.EventArgs)

        System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-GB")

        System.Threading.Thread.CurrentThread.CurrentUICulture = New System.Globalization.CultureInfo("en-GB")

    End Sub

 

La única consideración a tener en cuenta es que las propiedades Culture y UICulture prevalecen sobre Application_BeginRequest, así que si tenemos la sección globalization o estas propiedades establecidas en la directiva Page de la página .aspx, es más seguro (y sobre todo pasa después y funcionará correctamente) el método InitializeCulture.

Bueno, pues después de todo esto ya puedo comenzar con la traducción de mi aplicación!!.

Un saludo!

5 comentarios:

  1. Muy bueno y de mucha utilidad, gracias.

    ResponderEliminar
  2. Muy interesante Sergio. Yo estoy utilizando archivos de recursos externos, es decir haciendo referencia a una librería de recursos. Pero lo que no he conseguido aun es poder acceder a ella desde el aspx utilizando <%$ Resources:MiFicheroSinExtension,MiEntrada %>

    Un saludo.

    ResponderEliminar
  3. Uff que buena informacion. felicitaciones ahora voy a implementarla en mi aplicacion.

    Saludos

    ResponderEliminar
  4. Gracias Jhon por comentar, la verdad es que esto de la traducción con Web Forms, una vez instaurada ya es coser y cantar ;-)

    ResponderEliminar
  5. He estado trabajando con esta herramienta de traducción: https://poeditor.com/ y lo que realmente hace un gran trabajo. Soporta un gran número de traductores en el mismo proyecto, trabajando en diferentes idiomas. Hay también un montón de características que facilitan el trabajo. Se lo recomiendo.

    ResponderEliminar