[WPF] Localización de aplicaciones

Hola a todos!

Ayer hablando con Jorge Serrano, Lluis Franco, Javier Torrecilla y Eduard Tomàs por twitter surgía la duda de como y cual es la mejor forma de localizar aplicaciones en WPF.

Actualmente existen dos métodos ampliamente usados para localizar nuestra aplicación WPF.

El más extendido se basa en usar archivos de recursos (resx) que contengan los textos en diferentes idiomas y, usando una clase wrapper, mediante un ObjectDataProvider enlazar a estos recursos de forma que cuando el idioma cambie en la cultura de la aplicación, el ObjectDataProvider refresque los bindings de los textos hacia el idioma correcto.

El otro método se basa en el uso de ensamblados satélites. Debemos marcar los objetos que queramos traducir en XAML con el atributo Uid, luego, usando una herramienta llamada BamlTool exportaremos todos estos objetos junto a sus textos a un fichero que podemos editar y traducir para acto seguido, usando la herramienta BamlTool de nuevo generar un ensamblado con nuestros textos traducidos. Repetimos estos pasos por tantos idiomas como deseemos controlar y automáticamente cuando la aplicación detectará el idioma actual y usará el ensamblado apropiado.

Oficialmente, a partir de .NET 3.5 Microsoft recomienda usar el segundo método, los ensamblados satélites, aunque ambos métodos presentan sus más y sus menos.

Mucha gente (la mayoría de hecho) siguen usando el método de archivos de recursos resx, aunque Microsoft se empeña en dejar bien claro que los archivos resx están obsoletos para estos menesteres.

El problema principalmente lo encontramos en que, cada vez que añadamos un control nuevo a nuestra aplicación, con la solución de los archivos de recursos, solo debemos añadir el texto de este control a cada archivo resx en su correspondiente idioma, mientras que con la técnica de bamltool debemos regenerar los ensamblados satélites una y otra vez, lo cual nos obliga a ir a la linea de comandos, ejecutar la herramienta,… además, la herramienta bamltool está actualmente en estado beta, por lo que, aunque Microsoft nos recomienda usarla, la usamos “as is” sin ninguna garantía… lo cual hace que a muchos les tiemble la mano al tener que usarla en entornos de producción, algo totalmente comprensible.

Bueno, aun así, como estos son los dos métodos mayoritariamente aceptados como válidos (supongo que habrá más formas de localizar una app, los caminos del código son infinitos) vamos a ver un ejemplo de cada uno de ellos y que el lector se encargue de elegir por sí mismo el camino más adecuado para su situación.

Usando recursos

Bueno, para empezar, vamos a crear una aplicación WPF nueva con una interface muy sencilla, unos botones (3) y un textblock, algo así:

image

El código XAML necesario es realmente sencillo:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="30"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <TextBlock Grid.ColumnSpan="3" Grid.Row="0"
                HorizontalAlignment="Center"
                VerticalAlignment="Top"
                Text="Prueba de cambio dinámico de idiomas"></TextBlock>
        
    <Button Content="Español" Grid.Row="1" Grid.Column="0" Margin="5"></Button>
    <Button Content="Ingles" Grid.Row="1" Grid.Column="1" Margin="5"></Button>
    <Button Content="Italiano" Grid.Row="1" Grid.Column="2" Margin="5"></Button>
</Grid>

Una vez creado nuestro interface de usuario, el cual tiene los textos Hard coded en el propio XAML algo que no es demasiado elegante, vamos a crear unos archivos de recursos para poder tener nuestra aplicación en español, ingles e italiano, para mantener algo de orden en la solución los agruparemos en una carpeta idiomas y los llamaremos idiomas.resx, idiomas.en.resx e idiomas.it.resx, uno para cada idioma:

image

Una vez creados, es muy importante que entremos en cada archivo de recursos y establezcamos su modificador de acceso (Access Modifier) a public, para poder acceder a las cadenas de texto de los recursos usando nombres fuertemente tipados:

image

Una vez hecho todo esto, vamos a crear en cada archivo unos recursos para el textblock y otro para cada botón. Recuerda, querido lector, que para que todo funcione correctamente en todos los archivos de recursos una cadena debe tener el mismo nombre, aunque cambiemos su valor dependiendo del idioma:

image

Ahora ya tenemos los textos de nuestra aplicación en un archivo de recursos, vamos a crear una clase Wrapper que nos permita acceder a ellos desde un ObjectDataProvider y enlazarnos a el mediante XAML:

public class WrapperIdiomas
{
    private static ObjectDataProvider m_provider;
        
    public WrapperIdiomas()
    {
    }

    //devuelve una instancia nueva de nuestros recursos.
    public idiomas GetResourceInstance()
    {
        return new idiomas();
    }

    //Esta propiedad devuelve el ObjectDataProvider en uso.
    public static ObjectDataProvider ResourceProvider
    {
        get
        {
            if (m_provider == null)
                m_provider = (ObjectDataProvider)App.Current.FindResource("IdiomasRes");
            return m_provider;
        }
    }

    //Este método cambia la cultura aplicada a los recursos y refresca la propiedad ResourceProvider.
    public static void ChangeCulture(CultureInfo culture)
    {
        Properties.Resources.Culture = culture;
        ResourceProvider.Refresh();
    }
}

Esta clase es muy sencilla, se compone de varios métodos que se encargan de refrescar el idioma (cultura) aplicado en la aplicación y devolver los recursos apropiados para la cultura establecida.

Una vez hecho esto, vamos a crear nuestro ObjectDataProvider en el archivo App.Xaml:

Primero debemos referenciar el namespace donde se encuentra nuestro wrapper:

xmlns:idiomas="clr-namespace:Resources.idiomas"

Ahora crearemos el ObjectDataProvider:

<ObjectDataProvider x:Key="IdiomasRes" 
                    ObjectType="{x:Type idiomas:WrapperIdiomas}" 
                    MethodName="GetResourceInstance">
</ObjectDataProvider

Ahora ya podremos enlazar nuestros controles en XAML, usando Databinding, directamente al recurso que deseemos. Es recomendable recompilar el proyecto antes de enlazar en XAML:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
        <ColumnDefinition Width=".3*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="30"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <TextBlock Grid.ColumnSpan="3" Grid.Row="0"
        HorizontalAlignment="Center"
        VerticalAlignment="Top"
        Text="{Binding Texto, Source={StaticResource IdiomasRes}}"></TextBlock>

    <Button Content="{Binding Boton_español, Source={StaticResource IdiomasRes}}" 
            Grid.Row="1" Grid.Column="0" Margin="5" Click="Español_Click"></Button>
    <Button Content="{Binding Boton_ingles, Source={StaticResource IdiomasRes}}" 
            Grid.Row="1" Grid.Column="1" Margin="5" Click="Ingles_Click"></Button>
    <Button Content="{Binding Boton_italiano, Source={StaticResource IdiomasRes}}" 
            Grid.Row="1" Grid.Column="2" Margin="5" Click="Italiano_Click"></Button>
</Grid>

Como podemos ver, ahora ya tenemos una solución mucho más decente, que nos permite reutilizar cadenas y no tener textos escritos por nuestro XAML. Si todo ha ido bien, si abres la vista de diseño, verás los textos en el idioma del sistema operativo (siempre que esté en español, ingles o italiano) ya queda muy poco para terminar, vamos a indicarle a cada botón en el evento click, que cambie la cultura de la aplicación a la que le corresponda y que llame al método ChangeCulture de nuestro wrapper:

private void Español_Click(object sender, RoutedEventArgs e)
{
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
    idiomas.WrapperIdiomas.ChangeCulture(Thread.CurrentThread.CurrentUICulture);
}

private void Ingles_Click(object sender, RoutedEventArgs e)
{
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
    idiomas.WrapperIdiomas.ChangeCulture(Thread.CurrentThread.CurrentUICulture);
}

private void Italiano_Click(object sender, RoutedEventArgs e)
{
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("it-IT");
    idiomas.WrapperIdiomas.ChangeCulture(Thread.CurrentThread.CurrentUICulture);
}

Y con esto, si ejecutamos nuestra aplicación, veremos que por defecto usa el idioma de nuestro  S.O. y podemos cambiar el idioma pulsando cada uno de los botones.

Conclusión

Como os comente al inicio, cada uno de los métodos que podemos usar tiene sus inconvenientes, el gran inconveniente que tiene a mi entender esta aproximación mediante recursos es que no es interoperable entre WP7, Silverlight y WPF, puesto que Silverlight, tanto en su versión web como móvil, carecen del objeto ObjectDataProvider, lo que evita que podamos compartir de forma incondicional este método.

En un próximo artículo veremos como generar ensamblados satélites y traducir nuestras aplicaciones, también veremos como localizar aplicaciones silverlight y Wp7.

Os dejo una demo con código, espero que os guste tanto leer este artículo como a mi escribirlo.

Un gran saludo y Happy Coding!

4 comentarios sobre “[WPF] Localización de aplicaciones”

  1. Hola Josué,

    Nosotros hemos utilizado una aproximación que a priori parece más sencilla en un proyecto bastante grande: Hemos creado extensiones Xaml. (XamlExtension).

    Lo interesante es que al final el código XAML para usarlo se simplifica bastante. Basta con hacer algo del estilo a lo siguiente:

    Como puedes ver, la sintaxis se reduce bastante, y no es necesaria la definición previa del ObjectDataProvider.

  2. Muy interesante el artículo, desde hace tiempo siempre he reusado el utilizar archivos de recursos para las aplicaciones, son difíciles de mantener y manejar, en nuestro caso desarrollamos un sistema que almacena las traducciones en una tabla de la base de datos, a partir de la cultura, el sistema carga un diccionario con las palabras claves y sus traducciones al inicio de la aplicación y las cachea. En tiempo de ejecución al abrir los formulario realizamos la traducción de cada formulario, esto nos aporta varias ventajas, los propios usuarios pueden introducir o cambiar las traducciones que no sean correctas, por otra parte al tener almacenados todos los datos en una tabla su mantenimiento y la posibilidad de añadir más idiomas nos resulta mucho más fácil frente a los sistemas tradicionales.

    Un saludo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *