Localice Aplicaciones Con IA

Otros idiomas: English 中文 Português

Ya sea que estés pensando en localizar tu proyecto o simplemente aprendiendo cómo hacerlo, la IA podría ser un buen punto de inicio. Ofrece un punto de entrada rentable para experimentos y automatizaciones.

En este post, navegaremos a través de un experimento. Vamos a:

Si nunca has tratado con la localización y te gustaría aprender, podría ser una buena idea comenzar aquí. Excepto por algunos detalles técnicos, el enfoque es en gran medida universal, y puedes aplicarlo en otros tipos de proyectos.

Si ya estás familiarizado con los conceptos básicos, y sólo quieres ver a la IA en acción, es posible que quieras saltarte a Traducir los textos o clona mi fork para pasar rápidamente sobre los commits y evaluar los resultados.

Obtén el proyecto

Crear una aplicación sólo para un experimento de localización sería excesivo, así que vamos a hacer un fork de algún proyecto de código abierto. Elegí Spring Petclinic, una aplicación web de ejemplo que se utiliza para mostrar el marco Spring para Java.

Hacer un fork y clonar Petclinic (requiere GitHub CLI)
gh repo fork https://github.com/spring-projects/spring-petclinic --clone=true

Si no has usado Spring antes, algunos fragmentos de código pueden no parecerte familiares, pero, como ya mencioné, esta discusión es agnóstica a la tecnología. Los pasos son más o menos los mismos independientemente del idioma y del marco.

Internacionalización

Antes de que una aplicación pueda ser localizada, tiene que ser internacionalizada.

La internacionalización (también escrita como i18n) es el proceso de adaptar el software para soportar diferentes idiomas. Por lo general, comienza con la externalización de las cadenas de la UI a archivos especiales, comúnmente referidos como bundles de recursos.

Los bundles de recursos contienen los valores de texto para diferentes idiomas:

en.json:

{
  "greeting": "Hello!",
  "farewell": "Goodbye!"
}

es.json:

{
  "greeting": "¡Hola!",
  "farewell": "¡Adiós!"
}

Para que estos valores lleguen a la UI, la UI debe ser explícitamente programada para usar estos archivos.

Esto típicamente involucra una biblioteca de internacionalización o una característica incorporada en el lenguaje, cuyo propósito es reemplazar los textos de la UI con los valores correctos para una localización dada. Ejemplos de dichas bibliotecas incluyen i18next (JavaScript), Babel (Python), y go-i18n (Go).

Java soporta la internacionalización de forma nativa, por lo que no necesitamos traer dependencias adicionales al proyecto.

Examina las fuentes

Java utiliza archivos con extensión .properties para almacenar cadenas localizadas para la interfaz de usuario.

Afortunadamente, ya hay un montón de ellos en el proyecto. Por ejemplo, así es lo que tenemos para inglés y español:


welcome=Welcome
required=is required
notFound=has not been found
duplicate=is already in use
nonNumeric=must be all numeric
duplicateFormSubmission=Duplicate form submission is not allowed
typeMismatch.date=invalid date
typeMismatch.birthDate=invalid date
welcome=Bienvenido
required=Es requerido
notFound=No ha sido encontrado
duplicate=Ya se encuentra en uso
nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida

Externalizar las cadenas de la UI no es algo que todos los proyectos hagan de manera universal. Algunos proyectos pueden tener estos textos codificados directamente en la lógica de la aplicación.

Tip icon

Externalizar los textos de la UI es una buena práctica con ventajas más allá de la internacionalización. Facilita el mantenimiento del código y promueve la consistencia en los mensajes de la UI. Si estás comenzando un proyecto, considera implementar la i18n lo más pronto posible.

Prueba de funcionamiento

Vamos a añadir una forma de cambiar la localización a través de parámetros de URL. Esto nos permitirá probar si todo está completamente externalizado y traducido al menos a un idioma.

Para lograr esto, añadimos la siguiente clase para gestionar el parámetro de localización:

WebConfig.java
import java.util.Locale;

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Bean
	public LocaleResolver localeResolver() {
		SessionLocaleResolver slr = new SessionLocaleResolver();
		slr.setDefaultLocale(Locale.US);
		return slr;
	}

	@Bean
	public LocaleChangeInterceptor localeChangeInterceptor() {
		LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
		lci.setParamName("lang");
		return lci;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(localeChangeInterceptor());
	}
}

Ahora que podemos probar diferentes localizaciones, ejecutamos el servidor, y comparamos la página de inicio para varios parámetros de localización:

Página de inicio de Spring Petclinic con mensaje de bienvenida en español y el resto en inglés

El cambio de la localización se refleja en la UI, lo cual es una buena noticia. Sin embargo, parece que el cambio de localización sólo ha afectado a una parte de los textos. Para el español, Welcome ha cambiado a Bienvenido, pero los enlaces en el encabezado se mantuvieron igual, y las otras páginas siguen estando en inglés. Esto significa que tenemos trabajo por hacer.

Modificar plantillas

El proyecto Spring Petclinic genera las páginas usando plantillas de Thymeleaf, así que vamos a inspeccionar los archivos de plantilla.

En efecto, algunos de los textos están codificados, por lo que necesitamos modificar el código para referirnos a los bundles de recursos en su lugar.

Afortunadamente, Thymeleaf tiene buen soporte para archivos .properties de Java, así que podemos incorporar referencias a las claves correspondientes del bundle de recursos directamente en la plantilla:


findOwners.html
<h2>Find Owners</h2>
findOwners.html
<h2 th:text='#{heading.find.owners}'>Find Owners</h2>
messages.properties
heading.find.owners=Find Owners

El texto previamente codificado aún está ahí, pero ahora sirve como un valor de reserva, que sólo será usado si hay un error al recuperar el mensaje localizado adecuado.

El resto de los textos se externalizan de manera similar; sin embargo, hay varios lugares que requieren atención especial. Por ejemplo, algunas de las advertencias provienen del motor de validación y tienen que ser especificadas usando parámetros de anotaciones de Java:


@Column(name = "first_name")
@NotBlank
private String firstName;
@Column(name = "first_name")
@NotBlank(message = "{field.validation.notblank}")
private String firstName;

En un par de lugares, la lógica tiene que ser cambiada:

<h2>
    <th:block th:if="${pet['new']}">New </th:block>Pet
</h2>

En el ejemplo de arriba, la plantilla utiliza una condición. Si el atributo new está presente, New se añade al texto de la UI. Por consiguiente, el texto resultante es New Pet o Pet dependiendo de la presencia del atributo.

Esto puede romper la localización para algunas localidades, debido al acuerdo entre el sustantivo y el adjetivo. Por ejemplo, en español, el adjetivo sería Nuevo o Nueva dependiendo del género del sustantivo, y la lógica existente no tiene en cuenta esta distinción.

Una posible solución a esta situación es hacer la lógica aún más sofisticada. Generalmente es una buena idea alejarse de la lógica complicada siempre que sea posible, así que opté por desacoplar las ramas en su lugar:

<h2>
    <th:block th:if="${pet['new']}" th:text="#{pet.new}">New Pet</th:block>
    <th:block th:unless="${pet['new']}" th:text="#{pet.update}">Pet</th:block>
</h2>

Las ramas separadas también simplificarán el proceso de traducción y el mantenimiento futuro del código fuente.


El formulario New Pet tiene un truco también. Su desplegable Type se crea pasando la colección de tipos de mascotas a la plantilla selectField.html :

<input th:replace="~{fragments/selectField :: select (#{pet.type}, 'type', ${types})}" />

A diferencia de los otros textos de la UI, los tipos de mascotas son parte del modelo de datos de la aplicación. Se obtienen de una base de datos en tiempo de ejecución. La naturaleza dinámica de estos datos nos impide extraer directamente los textos a un bundle de propiedades.

De nuevo, hay varias formas de manejar esto. Una forma es construir dinámicamente la clave del bundle de propiedades en la plantilla:


<option th:each="item : ${items}"
        th:value="${item}"
        th:text="${item}">dog</option>
<option th:each="item : ${items}"
        th:value="${item}"
        th:text="#{'pettype.' + ${item}}">dog</option>

En este enfoque, en lugar de renderizar directamente cat en la interfaz de usuario, lo pre-argumentamos con pettype., lo que resulta en pettype.cat. Luego utilizamos esta cadena como una llave para recuperar el texto de la interfaz de usuario localizado:


pettype.bird=bird
pettype.cat=cat
pettype.dog=dog
pettype.bird=pájaro
pettype.cat=gato
pettype.dog=perro
Info icon

Podrías haber notado que hemos modificado la plantilla de un componente reutilizable. Dado que los componentes reutilizables están destinados a servir a múltiples clientes, no es correcto introducir la lógica del cliente en él.

En este caso particular, el componente de la lista desplegable se vincula a los tipos de mascotas, lo cual es problemático para cualquiera que quiera usarlo para cualquier otra cosa.

Este defecto estaba allí desde el principio, vea dog como el texto predeterminado de las opciones. Simplemente propagamos este defecto aún más. Esto no debe hacerse en proyectos reales y necesita refactorización.


Por supuesto, hay más código de proyecto para internacionalizar; sin embargo, el resto de este en su mayoría se alinea con los ejemplos anteriores. Para una revisión completa de todos mis cambios, eres bienvenido a examinar los commits en mi fork.

Agregar las claves que faltan

Después de reemplazar todo el texto de la interfaz de usuario con referencias a las llaves del paquete de propiedades, debemos asegurarnos de introducir todas estas nuevas claves. No necesitamos traducir nada en este punto, solo agregue las claves y los textos originales al archivo messages.properties .

IntelliJ IDEA tiene soporte de Thymeleaf. Detecta si una plantilla hace referencia a una propiedad faltante, así que puedes identificar las que faltan sin mucha verificación manual:

IntelliJ IDEA muestra una advertencia en la plantilla que hace referencia a una clave de propiedad faltante IntelliJ IDEA muestra una advertencia en la plantilla que hace referencia a una clave de propiedad faltante

Con todas las preparaciones hechas, llegamos a la parte más interesante del trabajo. Tenemos todas las claves, y tenemos todos los valores en inglés. ¿De dónde obtenemos los valores para los otros idiomas?

Traducir los textos

Para traducir los textos, crearemos un script que utilice un servicio de traducción externo. Hay una gran cantidad de servicios de traducción disponibles, y muchas formas de escribir dicho script. He tomado las siguientes elecciones para la implementación:

No hice una investigación exhaustiva, así que estas elecciones son algo arbitrarias. Siéntete libre de experimentar y descubrir lo que mejor te convenga.

Info icon

Si decides usar el script a continuación, necesitarás crear una cuenta con DeepL y pasar tu clave API personal al script a través de la variable de entorno DEEPL_KEY

Este es el script:

import os
import requests
import json

deepl_key = os.getenv('DEEPL_KEY')
properties_directory = "../src/main/resources/messages/"


def extract_properties(text):
    properties = {}

    for line in text:
        line = line.strip()

        if line and not line.startswith('#') and '=' in line:
            key_value = line.split('=')
            key = key_value[0].strip()
            value = key_value[1].strip()
            if key and value:
                properties[key] = value

    return properties


def missing_properties(properties_file, properties_checklist):
    with open(properties_file, 'r') as f:
        text = f.readlines()

    present_properties = extract_properties(text)
    missing = {k: v for k, v in properties_checklist.items() if k not in present_properties.keys()}
    return missing


def translate_property(value, target_lang):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'DeepL-Auth-Key {deepl_key}',
        'User-Agent': 'LocalizationScript/1.0'

    }
    url = 'https://api-free.deepl.com/v2/translate'
    data = {
        'text': [value],
        'source_lang': 'EN',
        'target_lang': target_lang,
        'preserve_formatting': True
    }

    response = requests.post(url, headers=headers, data=json.dumps(data))

    return response.json()["translations"][0]["text"]


def populate_properties(file_path, properties_checklist, target_lang):
    with open(file_path, 'a+') as file:
        properties_to_translate = missing_properties(file_path, properties_checklist)
        for key, value in properties_to_translate.items():
            new_value = translate_property(value, target_lang)
            property_line = f"{key}={new_value}\n"
            print(property_line)
            file.write(property_line)


with open(properties_directory + 'messages.properties') as base_properties_file:
    base_properties = extract_properties(base_properties_file)

languages = [
    # configure languages here
    "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "fi"
]

for language in languages:
    populate_properties(properties_directory + f"messages_{language}.properties", base_properties, language)

El script extrae las claves del paquete de propiedades por defecto ( messages.properties ) y busca sus traducciones en los paquetes específicos del locale. Si encuentra que una cierta clave carece de una traducción, el script solicitará la traducción a la API de DeepL y la añadirá al paquete de propiedades.

He especificado 10 idiomas objetivo, pero puedes modificar la lista o agregar tus idiomas preferidos, siempre y cuando DeepL los soporte.

El script se puede optimizar aún más para enviar los textos para su traducción en lotes de 50. No lo hice aquí para mantener las cosas simples.

Ejecutar el script

Ejecutar el script en 10 idiomas me tomó ~5 minutos. El panel de uso muestra 8348 caracteres, que habrían costado €0.16 si estuviéramos en un plan de pago.

Como resultado, aparecen los siguientes archivos:

Además, se añaden propiedades faltantes a:

¿Pero qué pasa con las traducciones reales? ¿Podemos verlas ya?

Verificar los resultados

Relancemos la aplicación y probémosla usando diferentes valores de parámetros lang. Por ejemplo:

Personalmente, me satisface mucho ver cada página correctamente localizada. Hemos puesto algún esfuerzo, y ahora está dando resultados:


Página Encontrar Propietarios de Spring Petclinic en inglés
Página Encontrar Propietarios de Spring Petclinic en español
Página Encontrar Propietarios de Spring Petclinic en holandés
Página Encontrar Propietarios de Spring Petclinic en chino
Página Encontrar Propietarios de Spring Petclinic en francés

Abordar los problemas

Los resultados son impresionantes. Sin embargo, si te fijas bien, puede que descubras errores que surgen de la falta de contexto. Por ejemplo:

visit.update = Visit

Visitar puede ser tanto un sustantivo como un verbo. Sin contexto adicional, el servicio de traducción produce una traducción incorrecta en algunos idiomas.

Esto puede solucionarse ya sea mediante la edición manual o ajustando el flujo de trabajo de la traducción. Una posible solución es proporcionar contexto en los archivos .properties utilizando comentarios:

# Noun. Heading. Displayed on the page that allows the user to edit details of a veterinary visit
visit.update = Visit

Luego podemos modificar el script de traducción para analizar dichos comentarios y pasarlos con el parámetro context:

url = 'https://api-free.deepl.com/v2/translate'
data = {
    'text': [value],
    'source_lang': 'EN',
    'target_lang': target_lang,
    'preserve_formatting': True,
    'context': context
}

A medida que profundizamos y consideramos más idiomas, podríamos encontrarnos con más cosas que necesitan ser mejoradas. Este es un proceso iterativo.

Si hay una cosa que es indispensable en este proceso, esa es la revisión y la prueba. Independiente de si mejoramos la automatización o editamos su salida, encontraremos necesario llevar a cabo control de calidad y evaluación.

Más allá del alcance

Spring Petclinic es un proyecto simple, pero realista, al igual que los problemas que acabamos de resolver. Por supuesto, la localización presenta muchos desafíos que están fuera del alcance de este artículo, incluyendo:

Cada uno de estos temas merece un escrito propio. Si te gustaría leer más, estaré feliz de cubrir estos temas en publicaciones separadas.

Resumen

Está bien, ahora que hemos terminado de localizar nuestra aplicación, es hora de reflexionar sobre lo que hemos aprendido:

Espero que hayas disfrutado este artículo, ¡y me encantaría escuchar tus comentarios! Si tienes preguntas de seguimiento, sugerencias, o simplemente quieres charlar, no dudes en ponerte en contacto.

¡Espero verte en las futuras publicaciones!

all posts ->