Logged in as Registrarse/Entrar :
Views:
Herramientas:

Internacionalizando aplicaciones en Mono

De Mono Hispano

Autor: Mario Carrión mario.carrion@gmail.com
MonoUML Team

Contents

Introducción

En nuestros días las aplicaciones de software llegan a distintos lugares del planeta, y como tal, personas que hablan distintos idiomas las utilizan, aunque utilicemos el idioma estándar de la actualidad para nuestro software, inglés, es muy probable que gente no nativa a ese lenguaje llegue a utilizar nuestra aplicación. Podría plantearse la idea de realizar distintas versiones de la misma aplicación con cada traducción, pero eso traería utilizar tiempo en ir traduciendo la misma aplicación varias ocasiones y retardaría algunas cosas un poco más necesarias como resolver fallos de la aplicación o agregar nuevas características entre otras cosas. Como es bien sabido en entornos Unix existen las "locales" que configuradas hacen que automáticamente se muestren las aplicaciones en distintos idiomas, con distinto tipo de interpretación para los textos impresos en pantalla, etc, sin la necesidad de instalar la aplicación en algún lenguaje en particular (tal como ocurre con GNOME y muchas de sus aplicaciones, que se traduce de acuerdo al locale del sistema) ahí es donde entra gettext, en la cual se basa este tutorial.

Este tutorial intenta ser la base para poder realizar aplicaciones 100% internacionales que respondan automáticamente a la locale que se tenga para así cargar al vuelo una traducción correcta, no intenta ser la guía definitiva en este momento. Es recomendado leer la página oficial de gettext para obtener más de información.

Hay que tener claro que GetText# trabaja internamente tal cual las otras versiones de gettext y la unica diferencia entre las otras es la forma que debe usarse con C#. Si llegase a haber algún error dentro del tutorial es recomendado que se notifique al autor. Sin más que agregar por el momento comencemos con el tutorial.

Requerimientos

Este tutorial supone que se cuenta con una instalación correcta y funcional de lo siguiente:

  • mono (>= 1.0) para traducciones de aplicaciones de consola o basadas únicamente en texto.
  • gtk# (>= 1.0) para traducciones de aplicaciones gráficas.
  • Glade2 para poder realizar interfaces gráficas.

Todos los ejemplos fueron realizados y probados en un equipo Pentium 4, 2.4GHZ, 630MB RAM, Debian Sarge, kernel 2.6.8-2-386 y utilizando los paquetes oficiales de mono.

¿Cómo funciona gettext?

El uso de gettext es sencillo, podemos resumir todos los pasos de esta forma:

  • Reemplazar todas las cadenas que se deseen traducir con la sentencia alternativa de gettext.
  • Traducir las cadenas que previamente fueron seleccionadas al idioma que se desee.
  • Hacer una versión binaria del archivo traducido, ya sea para un fuente sencillo o un archivo de Glade2.
  • Si la aplicación utiliza libGlade o en nuestro caso Glade# copiar el archivo binario generado al directorio de la locale traducida dentro del sistema. Esto se explica más adelante.

Después de realizar estos pasos gettext automáticamente cargara la versión traducida que corresponda a la locale actual.

Utilizando Mono.Unix

Introducción

A pesar de que existe Gettext# para internacionalizar aplicaciones es recomendado utilizar el ensamblado Mono.Unix.

Instalación

Mono.Unix en aplicaciones de modo texto

Mono.Unix en aplicaciones de modo gráfico con Gtk# y Glade#

Utilizando Gettext#

Introducción

GetText# es la versión C# de gettext, que automáticamente carga una versión traducida de las cadenas definidas y previamente traducidas de la aplicación en base a la locale actual. Su utilización es relativamente sencilla, si deseas más información puedes consultar la página oficial de gettext.

Instalación

La versión utilizada en este tutorial es la 0.14.5 la cual se puede descargar desde su sitio oficial. Las demás versiones anteriores se pueden obtener desde el mismo sitio. Hay que hacer mención que desde la versión 0.14 existe el soporte para C#, asi que versiones anteriores no funcionan.

Después de haber obtenido el paquete gettext, es necesario descomprimirlo:

tar xvfz gettext-0.14.5.tar.gz

hacer un ./configure && make dentro del directorio recién creado, por defecto el script de configuración "configure" genera la versión de C#, así que no hay que usar algún parámetro con el script:

./configure && make

y finalmente instalar la librería siendo usuario root:

make install

después de haber realizado todo lo anterior ya tendremos nuestra librería de GetText#.

GetText# en aplicaciones de modo texto

Para utilizar GetText# hay que añadir el espacio de nombres "GNU.Gettext", a nuestro código, declarar una variable que permite a GetText# definir que cadenas son traducibles y comenzar a reemplazar todas las cadenas que consideremos necesarias para la internacionalización con una nueva sentencia propia de GetText#.

Por ejemplo si tuviéramos el código de un programa en un archivo llamado Ejemplo.cs cuyo contenido fuera el siguiente:

public class Ejemplo
{
	public static void Main (string[] args)
	{
		System.Console.WriteLine ("Hola mundo");
	}
}   

deberíamos modificarlo de esta forma:

using GNU.Gettext;	//Namespace requerido para la internacionalización (i18n)

public class Ejemplo
{
	//Esta variable nos permitirá establecer que cadenas son traducibles
	//"mi_clase" es un identificador del catálogo, utilizado más adelante
	private static GettextResourceManager catalogo = new GettextResourceManager ("mi_clase");  
	
	public static void Main (string[] args)
	{
		//La cadena anterior ahora es modificada como agregando el método GetString() de la variable catalogo.
		System.Console.WriteLine (catalogo.GetString ("Hola mundo"));
	}
} 

Como se puede observar la única cadena traducible fue "Hola mundo" la cuál se reemplazo con

catalogo.GetString ("Hola mundo")

de esta forma indicamos que esa cadena es traducible, es obvio que si hubieran mas cadenas que se pudieran y quisieran traducir deberíamos de seguir reemplazando con la misma sentencia.

Si compiláramos el código anterior:

mcs -r:GNU.Gettext Ejemplo.cs

y lo ejecutáramos:

mono Ejemplo.exe

La salida del programa seguiría siendo

Hola mundo

¿Qué hemos hecho mal? ¿Por qué aun no sale traducido nuestra cadena? Pues sencillamente porque aun no la hemos traducido. Después de haber realizado el paso anterior, debemos "sacar" las cadenas que marcamos como traducibles mediante la siguiente sentencia:

xgettext --from-code=UTF-8 Ejemplo.cs -o en.po

Donde el argumento --from-code=UTF-8 puede omitirse si el archivo fuente no esta en formato UTF-8, y en.po será el archivo que contendrá las cadenas que previamente marcamos como traducibles. En este caso traduciremos a ingles el contenido de nuestro pequeño programa, por eso se uso como nombre de archivo en.po, si hubiera sido francés hubiéramos puesto fr.po, y así para cualquier idioma, no es obligatorio seguir esta norma, pero suele ser de utilidad con múltiples traducciones.

La instrucción anterior xgettext genero un archivo llamado en.po cuyo contenido es

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2004-08-13 04:08-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: Ejemplo.cs:11
msgid "Hola mundo"
msgstr "" 

Como se puede ver, este archivo contiene las cadenas que fueron marcadas como traducibles, las cuales inician con msgid y establece un lugar donde debe ir la versión traducida de esa cadena en la linea siguiente, tal línea comienza con msgstr. De esta forma para cada msgid existe un msgstr. También podemos observar que es posible utilizar múltiples líneas para una misma cadena como el caso de

msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2004-08-13 04:08-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n" 

donde la cadena de msgstr esta definida en múltiples líneas.

Después de haber modificado el archivo en.po con la versión en ingles quedaría de la siguiente manera:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2004-08-13 04:08-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: Ejemplo.cs:11
msgid "Hola mundo"
msgstr "Hello world"

Como se puede notar, la línea que contenía previamente

"Content-Type: text/plain; charset=CHARSET\n"

ahora ha sido modificada a

"Content-Type: text/plain; charset=UTF-8\n"

pues el archivo en.po fue salvado en formato UTF-8.

Después de haber realizado este proceso de traducción de las cadenas previamente marcadas, debemos de convertir el archivo en.po para que pueda ser interpretado por gettext, de esta forma:

msgfmt --csharp --resource=mi_clase --locale=en en.po -d .

La instrucción anterior genera una librería para C#, debido al parámetro --csharp, en el directorio actual, debido al parámetro -d ., que se llama automáticamente cuando la locale es en, debido al parámetro -l en, como se puede ver el parámetro -r mi_clase hace llamado al identificador del catálogo que previamente se definió en el código fuente. Dicha librería se encuentra en ./en/ y se llama mi_clase.resources.dll siendo su ruta completa ./en/mi_clase.resources.dll.

Si ahora ejecutáramos

env LC_ALL=en mono Ejemplo.exe

la salida del programa sería la siguiente:

Hello world

Pues GetText# se basa en la locale actual, en este caso: en y busca una librería llamada igual que el identificador definido dentro del programa con terminación .resources.dll, en este caso mi_clase, dentro del directorio llamado igual que la locale actual, es decir busca ./en/mi_clase.resources.dll.

En dado caso que moviéramos o borráramos dicha librería ./en/mi_clase.resources.dll el programa imprimiría la versión por defecto contenida del código:

Hola mundo

Nota. Si quisiéramos que nuestra traducción funcionara cuando la locale fuera otra, por ejemplo en francés usaríamos --locale=fr, para otros idiomas puede mirar su archivo locale.alias, en Debian localizado en /etc/locale.alias. Note que las locales son de la forma idioma_UBICACIÓN, por ejemplo es_MX, para la locale de idioma en Español (es) con localización México (MX).

GetText# en aplicaciones de modo gráfico con Gtk# y Glade#

Ahora que ya sabemos las cosas básicas para poder utilizar GetText# en modo texto, la forma de utilizarlo en modo gráfico, no debe ser gran problema. Partamos del ejemplo de la aplicación de servicios web con Gtk# para traducirla. En este caso únicamente se utilizará la aplicación gráfica escrita en Gtk# nada de Servicios Web serán utilizados por el momento. Por lo tanto la siguiente interface gráfica será internacionalizada:

 Aplicación en Gtk con Glade

Donde el archivo fuente es GtkApp.cs:

using Glade;
using Gtk;
using System;

namespace GtkWebservices
{
	public class GtkApp
	{	         
			public GtkApp (string[] args) 
			{
				_olderResult = 0;
				Application.Init();
				_xml = new XML ("gui.glade", "_window", null);
				_xml.Autoconnect (this);
				Application.Run();
			}
        
			public void Show ()
			{
				_window.Show ();
			}

			private void OnDeleteEvent (object o, DeleteEventArgs args) 
			{
				Application.Quit ();
			}
        
			private void OnCloseEvent (object o, EventArgs args)
			{
				_window.Hide ();
				Application.Quit ();
			}
        
			private void OnAddEvent (object o, EventArgs args)
			{
				System.Console.WriteLine ("Resultado anterior: {0}",_olderResult);
				try
				{
					int firstNumber = System.Convert.ToInt32 (_firstNumber.Text);
					int secondNumber = System.Convert.ToInt32 (_secondNumber.Text);
					_olderResult = (firstNumber + secondNumber);
					_resultNumber.Text = _olderResult.ToString ();
				}
				catch (Exception ex)
				{
					_resultNumber.Text = "No es posible convertir el texto en número.";
				}
			}

			public static void Main (string []args)
			{
				GtkApp app = new GtkApp (args);
				app.Show ();
			}
		
			private XML _xml;
			private int _olderResult;
			[Widget] private Window _window;
			[Widget] private Entry _firstNumber;
			[Widget] private Entry _secondNumber;
			[Widget] private Entry _resultNumber;
	}
}

Como se puede apreciar, es un ejemplo muy sencillo de una interfaz gráfica con unos simples eventos. Ahora que ya tenemos un poco de experiencia con GetText# ya sabemos que hay que hacer.

Lo primero es reemplazar cada cadena que deseamos que sea traducida por su homólogo de GetText, por ejemplo, la sentencia original

_resultNumber.Text = "No es posible convertir el texto en número.";

deberá ser reemplazada por

_resultNumber.Text = catalogo.GetString ("No es posible convertir el texto en número.");

hay que tener siempre claro que el primer paso es exactamente igual al que se realiza en aplicaciones modo texto, y por lo tanto también debe declararse una variable del tipo GettextResourceManager, que en nuestro caso le llamamos catalogo para indicar que cadenas deberán ser traducidas.

Además no debemos olvidar utilizar el espacio de nombres GNU.Gettext

using GNU.Gettext;

La sentencia

System.Console.WriteLine ("Resultado anterior: {0}", _olderResult);

la cual imprime una cadena con un entero, tiene un trato especial

System.Console.WriteLine (String.Format (catalogo.GetString ("Resultado anterior: {0}"), _olderResult));

de esta forma GetText# podrá entender correctamente lo que se desea hacer con entero y representarla como tal.

De esta forma, el código fuente anterior quedará de la siguiente forma:

using Glade;
using Gtk;
using System;
using GNU.Gettext;

namespace GtkWebservices
{
	public class GtkApp
	{	         
		public GtkApp (string[] args) 
		{
			_olderResult = 0;
			Application.Init();
			_xml = new XML (null, "gui.glade", "_window", "VentanaInternacional");
			_xml.Autoconnect (this);
			Application.Run();
		}
    
		public void Show ()
		{
			_window.Show ();
		}

		private void OnDeleteEvent (object o, DeleteEventArgs args) 
		{
			Application.Quit ();
		}
    
		private void OnCloseEvent (object o, EventArgs args)
		{
			_window.Hide ();
			Application.Quit ();
		}
    
		private void OnAddEvent (object o, EventArgs args)
		{
			System.Console.WriteLine (String.Format (catalogo.GetString ("Resultado anterior: {0}"), _olderResult));
			try
			{
				int firstNumber = System.Convert.ToInt32 (_firstNumber.Text);
				int secondNumber = System.Convert.ToInt32 (_secondNumber.Text);
				_olderResult = (firstNumber + secondNumber);
				_resultNumber.Text = _olderResult.ToString ();
			}
			catch (Exception ex)
			{
				_resultNumber.Text = catalogo.GetString ("No es posible convertir el texto en número.");
			}
		}

		public static void Main (string []args)
		{
			GtkApp app = new GtkApp (args);
			app.Show ();
		}
		
		private GettextResourceManager catalogo = new GettextResourceManager ("mi_clase");
		private XML _xml;
		private int _olderResult;
		[Widget] private Window _window;
		[Widget] private Entry _firstNumber;
		[Widget] private Entry _secondNumber;
		[Widget] private Entry _resultNumber;
	}
}

Hay que prestar mucha atención a esta sentencia

_xml = new XML (null, "gui.glade", "_window", "VentanaInternacional");

En la cual hacemos referencia al recurso gui.glade, del cual decidimos iniciar la aplicación con el widget _window. El ultimo parámetro VentanaInternacional será el nombre del dominio de traducción de la aplicación, es algo que no debemos de perder en cuenta, más adelante se retomará esto.

Ahora solo falta compilar

mcs -codepage:utf8 -pkg:glade-sharp,gtk-sharp -resource:gui.glade,gui.glade -r:GNU.Gettext GtkApp.cs

y ejecutar el programar para ver que todo funciona bien:

mono GtkApp.exe

el cual simplemente se muestra como algo como esto:

 Aplicación de Servicios Web

Ahora que ya sabemos que todo funciona como debe ser, es hora de realizar el segundo paso, sacar las cadenas que marcamos como traducibles, esto se realiza con la instrucción

xgettext --from-code=UTF-8 GtkApp.cs gui.glade -o en.po

de esta forma sacaremos todas las cadenas de los archivos GtkApp.cs y gui.glade y serán almacenadas en el archivo en.po. Como vamos a realizar una traducción para el ingles se tomo el nombre de archivo en.po. Después de haber ejecutado la sentencia anterior debemos de abrir el archivo en un editor de texto que soporte UTF-8, pues el archivo generado cuando se utiliza un archivo .glade es UTF-8. Después de haberlo traducido deberia quedar algo como lo siguiente:

# I18n example for the Mono Hispano Commnunity (http://www.monohispano.org)
# Copyright (C) 2005 Mario Carrión
# This file is distributed under the same license as the GPL package.
# Mario Carrión <mario.carrion@gmail.com>, 2005.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Ejemplo 0.02\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2005-07-15 14:24-0500\n"
"PO-Revision-Date: 2005-07-15 13:51-06:00\n"
"Last-Translator: Mario Carrión <mario.carrion@gmail.com>\n"
"Language-Team: en_US <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: GtkApp.cs:60
#, csharp-format
msgid "Resultado anterior: {0}"
msgstr "Lastest result: {0}"

#: GtkApp.cs:70
msgid "No es posible convertir el texto en número."
msgstr "Couldn't convert the text to number"

#: gui.glade:8
msgid "Aplicación Gtk#"
msgstr "Gtk# Application"

#: gui.glade:38
msgid "_Primer número: "
msgstr "_First number: "

#: gui.glade:84
msgid "_Segundo número: "
msgstr "_Second number: "

#: gui.glade:144
msgid "_Resultado: "
msgstr "_Result: "

#: gui.glade:247
msgid "_Cerrar"
msgstr "_Close"

#: gui.glade:322
msgid "S_umar"
msgstr "_Add"

De igual forma que en las aplicaciones de modo texto, generamos la librería para las cadenas que no son usadas dentro del archivo .glade generado por Glade2 con

msgfmt --csharp --resource=mi_clase --locale=en_US en.po -d .

de esta forma ya estaría 50% de la aplicación traducida, ahora solo habría que generar el archivo necesario para que GetText# traduzca las cadenas que van contenidas dentro del archivo .glade por defecto, con la instrucción siguiente

msgfmt en.po -o VentanaInternacional.mo

Aquí en esta parte es donde debemos de recordar la sentencia de la cual anteriormente hablamos

_xml = new XML (null, "gui.glade", "_window", "VentanaInternacional");


de esta forma, el argumento -o VentanaInternacional.mo hace una referencia total al identificador que antes le asignamos al código fuente, si por alguna razón utilizáramos algún otro al momento de utilizar msgfmt, la traducción de la ventana generada a partir de Glade2 no se realizaría y se mostraría la escrita por defecto.

Ahora ya tenemos lo necesario para poder iniciar nuestra aplicación gráfica que responda automáticamente a la locale del sistema, solo falta el ultimo paso, copiar el archivo binario de la traducción VentanaInternacional.mo al directorio de la locale que se tradujo, todo esto como usuario root, por ejemplo, en Debian sería

cp VentanaInternacional.mo /usr/share/locale/en_US/LC_MESSAGES/

debe siempre copiar al directorio RUTA_DE_LOCALES/LOCALE_TRADUCIDA/LC_MESSAGES/, por ejemplo, si el lenguaje traducido hubiera sido el francés el directorio dentro de Debian sería /usr/share/locale/fr/LC_MESSAGES/

El directorio /usr/share/locale/ es usualmente el directorio en el cual se guardan las traducciones de gettext de las locales, si por alguna razón en tu distribución cambia, me agradaría saberlo para poder hacer esa mención dentro de este tutorial.

Finalmente veamos el ejemplo del cual estuvimos hablando anteriormente en la locale por defecto y la locale en inglés.

Conclusiones

Preguntas Frecuentes