Cómo internacionalizar su aplicación AngularJS
Publicado: 2022-03-11La internacionalización de su aplicación puede hacer que el desarrollo de software sea una experiencia dolorosa, especialmente si no comienza a hacerlo desde el principio o si adopta un enfoque de lo que sea.
Las aplicaciones modernas, donde el front-end y el back-end están claramente separados entre sí, pueden ser aún más difíciles de manejar cuando se trata de internacionalización. De repente, ya no tiene acceso a la plétora de herramientas probadas que una vez ayudaron a internacionalizar sus aplicaciones web tradicionales generadas por páginas del lado del servidor.
En consecuencia, una aplicación AngularJS requiere la entrega a pedido de datos de internacionalización (i18n) y localización (l10n) para que se entreguen al cliente para que se represente en la configuración regional adecuada. A diferencia de las aplicaciones renderizadas tradicionales del lado del servidor, ya no puede confiar en el servidor para entregar páginas que ya están localizadas. Puede obtener información sobre cómo crear una aplicación PHP multilingüe aquí
En este artículo, aprenderá cómo puede internacionalizar su aplicación AngularJS y conocerá las herramientas que puede usar para facilitar el proceso. Hacer que su aplicación AngularJS sea multilingüe puede plantear algunos desafíos interesantes, pero ciertos enfoques pueden facilitar la solución de la mayoría de esos desafíos.
Una simple aplicación AngularJS compatible con i18n
Para permitir que el cliente cambie el idioma y la configuración regional sobre la marcha en función de las preferencias del usuario, deberá tomar una serie de decisiones de diseño clave:
- ¿Cómo diseña su aplicación para que sea independiente del idioma y la configuración regional desde el principio?
- ¿Cómo se estructuran los datos i18n y l10n?
- ¿Cómo entrega estos datos de manera eficiente a los clientes?
- ¿Cómo se abstrae la mayor parte de los detalles de implementación de bajo nivel para simplificar el flujo de trabajo del desarrollador?
Responder a estas preguntas lo antes posible puede ayudar a evitar obstáculos en el proceso de desarrollo en el futuro. Cada uno de estos desafíos se abordará en este artículo; algunos a través de robustas bibliotecas AngularJS, otros a través de ciertas estrategias y enfoques.
Bibliotecas de internacionalización para AngularJS
Hay una serie de bibliotecas de JavaScript creadas específicamente para internacionalizar aplicaciones de AngularJS.
angular-translate
es un módulo AngularJS que proporciona filtros y directivas, junto con la capacidad de cargar datos i18n de forma asíncrona. Admite la pluralización a través de MessageFormat
y está diseñado para ser altamente extensible y configurable.
Si está utilizando angular-translate
en su proyecto, puede encontrar algunos de los siguientes paquetes muy útiles:
-
angular-sanitize
: se puede usar para protegerse contra ataques XSS en las traducciones. -
angular-translate-interpolation-messageformat
: pluralización con soporte para formato de texto sensible al género. -
angular-translate-loader-partial
: se utiliza para entregar cadenas traducidas a los clientes.
Para una experiencia verdaderamente dinámica, puede agregar angular-dynamic-locale
al grupo. Esta biblioteca le permite cambiar la configuración regional dinámicamente, y eso incluye la forma en que se formatean las fechas, los números, las monedas, etc.
Primeros pasos: Instalación de paquetes relevantes
Suponiendo que ya tiene listo su modelo de AngularJS, puede usar NPM para instalar los paquetes de internacionalización:
npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat
Una vez que los paquetes estén instalados, no olvide agregar los módulos como dependencias de su aplicación:
// /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);
Tenga en cuenta que el nombre del módulo es diferente del nombre del paquete.
Traducir tu primera cadena
Supongamos que su aplicación tiene una barra de herramientas con texto y un campo con texto de marcador de posición:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Hello</a> </div> <div class="collapse navbar-collapse"> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" ng-model="vm.query" placeholder="Search"> </div> ... </div> </div> </nav>
La vista anterior tiene dos fragmentos de texto que puede internacionalizar: "Hola" y "Buscar". En términos de HTML, uno aparece como texto interno de una etiqueta de anclaje, mientras que el otro aparece como un valor de un atributo.
Para internacionalizarlos, tendrá que reemplazar ambos literales de cadena con tokens que AngularJS puede luego reemplazar con las cadenas traducidas reales, según la preferencia del usuario, mientras representa la página.
AngularJS puede hacer esto usando sus tokens para realizar una búsqueda en las tablas de traducción que proporcione. El módulo angular-translate
espera que estas tablas de traducción se proporcionen como objetos JavaScript simples o como objetos JSON (si se cargan de forma remota).
Aquí hay un ejemplo de cómo se verían generalmente estas tablas de traducción:
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }
Para internacionalizar la vista de la barra de herramientas desde arriba, debe reemplazar los literales de cadena con tokens que AngularJS puede usar para buscar en la tabla de traducción:
<!-- /src/app/toolbar/toolbar.html --> <a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a> <!-- or --> <a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>
Observe cómo, para el texto interno, puede usar la directiva de translate
o el filtro de translate
. (Puede obtener más información sobre la directiva de translate
aquí y sobre los filtros de translate
aquí).
Con estos cambios, cuando se representa la vista, angular-translate
insertará automáticamente la traducción adecuada correspondiente a TOOLBAR.HELLO
en el DOM según el idioma actual.
Para tokenizar los literales de cadena que aparecen como valores de atributo, puede utilizar el siguiente enfoque:
<!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">
Ahora, ¿qué sucede si sus cadenas tokenizadas contienen variables?
Para manejar casos como "Hola, {{nombre}}", puede realizar el reemplazo de variables usando la misma sintaxis de interpolador que AngularJS ya admite:
Tabla de traducción:
// /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }
A continuación, puede definir la variable de varias maneras. Aquí hay algunos:
<!-- /src/app/toolbar/toolbar.html --> <a ... translate="TOOLBAR.HELLO" translate-values='{ name: vm.user.name }'></a> <!-- or --> <a ... translate="TOOLBAR.HELLO" translate-value-name='{{vm.user.name}}'></a> <!-- or --> <a ...>{{'TOOLBAR.HELLO | translate:'{ name: vm.user.name }'}}</a>
Lidiando con la Pluralización y el Género
La pluralización es un tema bastante difícil cuando se trata de i18n y l10n. Diferentes idiomas y culturas tienen diferentes reglas sobre cómo un idioma maneja la pluralización en diversas situaciones.
Debido a estos desafíos, los desarrolladores de software a veces simplemente no abordan el problema (o al menos no lo abordan adecuadamente), lo que da como resultado un software que produce oraciones tontas como estas:
He saw 1 person(s) on floor 1. She saw 1 person(s) on floor 3. Number of people seen on floor 2: 2.
Afortunadamente, existe un estándar sobre cómo manejar esto, y una implementación de JavaScript del estándar está disponible como MessageFormat.
Con MessageFormat, puede reemplazar las oraciones mal estructuradas anteriores con lo siguiente:
He saw 1 person on the 2nd floor. She saw 1 person on the 3rd floor. They saw 2 people on the 5th floor.
MessageFormat
acepta expresiones como las siguientes:
var message = [ '{GENDER, select, male{He} female{She} other{They}}', 'saw', '{COUNT, plural, =0{no one} one{1 person} other{# people}}', 'on the', '{FLOOR, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}', 'floor.' ].join(' ');
Puede construir un formateador con la matriz anterior y usarlo para generar cadenas:
var messageFormatter = new MessageFormat('en').compile(message); messageFormatter({ GENDER: 'male', COUNT: 1, FLOOR: 2 }) // 'He saw 1 person on the 2nd floor.' messageFormatter({ GENDER: 'female', COUNT: 1, FLOOR: 3 }) // 'She saw 1 person on the 3rd floor.' messageFormatter({ COUNT: 2, FLOOR: 5 }) // 'They saw 2 people on the 5th floor.'
¿Cómo puede usar MessageFormat
con angular-translate
para aprovechar su funcionalidad completa dentro de sus aplicaciones?
En la configuración de su aplicación, simplemente le dice a angular-translate
que la interpolación de formato de mensaje está disponible de la siguiente manera:
/src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });
Así es como podría verse una entrada en la tabla de traducción:
// /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }
Y en la vista:
<!-- /src/app/main/social/social.html --> <div translate="SHARED" translate-values="{ GENDER: 'male' }" translate-interpolation="messageformat"></div> <div> {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }} </div>
Aquí debe indicar explícitamente que se debe usar el interpolador de formato de mensaje en lugar del interpolador predeterminado en AngularJS. Esto se debe a que los dos interpoladores difieren ligeramente en su sintaxis. Puedes leer más sobre esto aquí.
Proporcionar tablas de traducción a su aplicación
Ahora que sabe cómo AngularJS puede buscar traducciones para sus tokens en las tablas de traducción, ¿cómo sabe su aplicación sobre las tablas de traducción en primer lugar? ¿Cómo le dices a tu aplicación qué configuración regional/idioma se debe usar?
Aquí es donde aprenderá sobre $translateProvider
.
Puede proporcionar las tablas de traducción para cada configuración regional que desee admitir directamente en el archivo core.config.js
de su aplicación de la siguiente manera:
// /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $translateProvider.translations('en', { TOOLBAR: { HELLO: 'Hello, {{name}}.' } }); $translateProvider.translations('tr', { TOOLBAR: { HELLO: 'Merhaba, {{name}}.' } }); $translateProvider.preferredLanguage('en'); });
Aquí proporciona tablas de traducción como objetos JavaScript para inglés (en) y turco (tr), mientras declara que el idioma actual es inglés (en). Si el usuario desea cambiar el idioma, puede hacerlo con el servicio $translate:

// /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, $translate) { $scope.changeLanguage = function (languageKey) { $translate.use(languageKey); // Persist selection in cookie/local-storage/database/etc... }; });
Todavía queda la cuestión de qué idioma debe usarse de forma predeterminada. Codificar el idioma inicial de nuestra aplicación puede no ser siempre aceptable. En tales casos, una alternativa es intentar determinar el idioma automáticamente usando $translateProvider:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });
determinePreferredLanguage
busca valores en window.navigator
y selecciona un valor predeterminado inteligente hasta que el usuario proporciona una señal clara.
Tablas de traducción de carga diferida
La sección anterior mostró cómo puede proporcionar tablas de traducción directamente en el código fuente como objetos de JavaScript. Esto puede ser aceptable para aplicaciones pequeñas, pero el enfoque no es escalable, razón por la cual las tablas de traducción a menudo se descargan como archivos JSON desde un servidor remoto.
El mantenimiento de las tablas de traducción de esta manera reduce el tamaño de la carga útil inicial entregada al cliente, pero presenta una complejidad adicional. Ahora se enfrenta al desafío de diseño de entregar datos i18n al cliente. Si esto no se maneja con cuidado, el rendimiento de su aplicación puede verse afectado innecesariamente.
¿Por qué es tan complejo? Las aplicaciones de AngularJS están organizadas en módulos. En una aplicación compleja, puede haber muchos módulos, cada uno con sus propios datos i18n distintos. Por lo tanto, se debe evitar un enfoque ingenuo, como cargar y proporcionar datos de i18n de una sola vez.
Lo que necesita es una forma de organizar sus datos i18n por módulo. Esto le permitirá cargar justo lo que necesita cuando lo necesita y almacenar en caché lo que se cargó previamente para evitar volver a cargar los mismos datos (al menos hasta que el caché no sea válido).
Aquí es donde entra en juego partialLoader
.
Digamos que las tablas de traducción de su aplicación están estructuradas así:
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json
Puedes configurar $translateProvider
para usar partialLoader
con un patrón de URL que coincida con esta estructura:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });
Como era de esperar, "lang" se reemplaza con el código de idioma en tiempo de ejecución (por ejemplo, "en" o "tr"). ¿Qué pasa con "parte"? ¿Cómo sabe $translateProvider qué "parte" cargar?
Puede proporcionar esta información dentro de los controladores con $translatePartialLoader
:
// /src/app/main/main.controller.js app.controller('MainCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('main'); }); // /src/app/toolbar/toolbar.config.js app.controller('ToolbarCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('toolbar'); });
El patrón ahora está completo y los datos i18n para una vista determinada se cargan cuando su controlador se ejecuta por primera vez, que es exactamente lo que desea.
Almacenamiento en caché: reducción de los tiempos de carga
¿Qué pasa con el almacenamiento en caché?
Puede habilitar el caché estándar en la configuración de la aplicación con $translateProvider
:
// /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });
Si necesita reventar el caché para un idioma determinado, puede usar $translate
:
$translate.refresh(languageKey); // omit languageKey to refresh all
Con estas piezas en su lugar, su aplicación está completamente internacionalizada y admite varios idiomas.
Localización de números, monedas y fechas
En esta sección, aprenderá cómo puede usar angular-dynamic-locale
para admitir el formato de elementos de la interfaz de usuario, como números, monedas, fechas y similares, en una aplicación AngularJS.
Necesitará instalar dos paquetes más para esto:
npm i -S angular-dynamic-locale angular-i18n
Una vez que se instalan los paquetes, puede agregar el módulo a las dependencias de su aplicación:
// /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);
Reglas locales
Las reglas de configuración regional son archivos JavaScript simples que brindan especificaciones sobre cómo los componentes que dependen del servicio $locale deben formatear fechas, números, monedas y similares.
La lista de configuraciones regionales admitidas actualmente está disponible aquí.
Aquí hay un fragmento de angular-locale_en-us.js
ilustra el formato de mes y fecha:
... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...
A diferencia de los datos de i18n, las reglas de configuración regional son globales para la aplicación, lo que requiere que las reglas para una configuración regional determinada se carguen todas a la vez.
De forma predeterminada, angular-dynamic-locale
espera que los archivos de reglas de configuración regional estén ubicados en angular/i18n/angular-locale_{{locale}}.js
. Si están ubicados en otro lugar, se debe usar tmhDynamicLocaleProvider
para anular el valor predeterminado:
// /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });
El almacenamiento en caché lo gestiona automáticamente el servicio tmhDynamicLocaleCache
.
La invalidación de la memoria caché es menos preocupante aquí, ya que es menos probable que cambien las reglas locales que las traducciones de cadenas.
Para cambiar entre configuraciones regionales, angular-dynamic-locale
proporciona el servicio tmhDynamicLocale
:
// /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, tmhDynamicLocale) { $scope.changeLocale = function (localeKey) { tmhDynamicLocale.set(localeKey); // Persist selection in cookie/local-storage/database/etc... }; });
Generación de tablas de traducción con traducción automática
Las reglas de configuración regional se envían con el paquete angular-i18n
, por lo que todo lo que tiene que hacer es hacer que el contenido del paquete esté disponible para su aplicación según sea necesario. Pero, ¿cómo genera los archivos JSON para sus tablas de traducción? No hay exactamente un paquete que pueda descargar y conectar a nuestra aplicación.
Una opción es usar API de traducción programática, especialmente si las cadenas en su aplicación son literales simples sin variables o expresiones en plural.
Con Gulp y un par de paquetes adicionales, solicitar traducciones programáticas para su aplicación es pan comido:
import gulp from 'gulp'; import map from 'map-stream'; import rename from 'gulp-rename'; import traverse from 'traverse'; import transform from 'vinyl-transform'; import jsonFormat from 'gulp-json-format'; function translateTable(to) { return transform(() => { return map((data, done) => { const table = JSON.parse(data); const strings = []; traverse(table).forEach(function (value) { if (typeof value !== 'object') { strings.push(value); } }); Promise.all(strings.map((s) => getTranslation(s, to))) .then((translations) => { let index = 0; const translated = traverse(table).forEach(function (value) { if (typeof value !== 'object') { this.update(translations[index++]); } }); done(null, JSON.stringify(translated)); }) .catch(done); }); }); } function translate(to) { return gulp.src('src/app/**/i18n/en.json') .pipe(translateTable(to)) .pipe(jsonFormat(2)) .pipe(rename({ basename: to })) .pipe(gulp.dest('src/app')); } gulp.task('translate:tr', () => translate('tr')); This task assumes the following folder structure: /src/app/main/i18n/en.json /src/app/toolbar/i18n/en.json /src/app/navigation/i18n/en.json ...
El script primero lee todas las tablas de traducción al inglés, solicita de forma asincrónica traducciones para sus recursos de cadena y luego reemplaza las cadenas en inglés con las cadenas traducidas para producir una tabla de traducción en un nuevo idioma.
Finalmente, la nueva tabla de traducción se escribe como un hermano de la tabla de traducción al inglés, lo que produce:
/src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json /src/app/navigation/i18n/en.json /src/app/navigation/i18n/tr.json ...
La implementación de getTranslation
también es sencilla:
import bluebird from 'bluebird'; import MicrosoftTranslator from 'mstranslator'; bluebird.promisifyAll(MicrosoftTranslator.prototype); const Translator = new MicrosoftTranslator({ client_id: process.env.MICROSOFT_TRANSLATOR_CLIENT_ID, client_secret: process.env.MICROSOFT_TRANSLATOR_CLIENT_SECRET }, true); function getTranslation(string, to) { const text = string; const from = 'en'; return Translator.translateAsync({ text, from, to }); }
Aquí, estamos usando Microsoft Translate, pero uno podría usar fácilmente otro proveedor como Google Translate o Yandex Translate.
Si bien las traducciones programáticas son convenientes, existen varios inconvenientes, que incluyen:
- Las traducciones robóticas son buenas para cadenas cortas, pero aun así, podría haber problemas con palabras que tienen diferentes significados en diferentes contextos (p. ej., "piscina" puede significar nadar o agruparse).
- Es posible que las API no puedan manejar cadenas con variables o cadenas que dependan del formato del mensaje.
En estos casos y otros, se pueden requerir traducciones humanas; sin embargo, ese es un tema para otra publicación de blog.
Internacionalizar solo los front-ends parece desalentador
En este artículo, aprendió a usar estos paquetes para internacionalizar y localizar aplicaciones AngularJS.
angular-translate
, angular-dynamic-locale
y gulp
son herramientas poderosas para internacionalizar una aplicación AngularJS que encapsulan detalles dolorosos de implementación de bajo nivel.
Para ver una aplicación de demostración que ilustra las ideas discutidas en esta publicación, consulte este repositorio de GitHub.