Como internacionalizar seu aplicativo AngularJS

Publicados: 2022-03-11

Internacionalizar seu aplicativo pode tornar o desenvolvimento de software uma experiência dolorosa, especialmente se você não começar a fazê-lo desde o início ou adotar uma abordagem involuntária em relação a isso.

Aplicativos modernos, onde o front-end e o back-end são distintamente separados um do outro, podem ser ainda mais complicados de lidar quando se trata de internacionalização. De repente, você não tem mais acesso a uma infinidade de ferramentas testadas pelo tempo que antes ajudaram a internacionalizar seus aplicativos da Web tradicionais gerados pela página do lado do servidor.

Da mesma forma, um aplicativo AngularJS requer entrega sob demanda de dados de internacionalização (i18n) e localização (l10n) a serem entregues ao cliente para se renderizar na localidade apropriada. Ao contrário dos aplicativos tradicionais renderizados do lado do servidor, você não pode mais confiar no servidor para entregar páginas que já estão localizadas. Você pode aprender sobre como construir um aplicativo PHP multilíngue aqui

Neste artigo, você aprenderá como internacionalizar seu aplicativo AngularJS e conhecerá as ferramentas que você pode usar para facilitar o processo. Tornar seu aplicativo AngularJS multilíngue pode representar alguns desafios interessantes, mas certas abordagens podem facilitar a solução da maioria desses desafios.

Um aplicativo AngularJS simples capaz de i18n

Para permitir que o cliente altere o idioma e a localidade rapidamente com base nas preferências do usuário, você precisará tomar várias decisões importantes de design:

  • Como você projeta seu aplicativo para ser independente de idioma e localidade desde o início?
  • Como você estrutura os dados i18n e l10n?
  • Como você entrega esses dados de forma eficiente aos clientes?
  • Como você abstrai os detalhes de implementação de baixo nível para simplificar o fluxo de trabalho do desenvolvedor?

Responder a essas perguntas o mais cedo possível pode ajudar a evitar obstáculos no processo de desenvolvimento no futuro. Cada um desses desafios será abordado neste artigo; alguns por meio de bibliotecas AngularJS robustas, outros por meio de certas estratégias e abordagens.

Bibliotecas de internacionalização para AngularJS

Existem várias bibliotecas JavaScript criadas especificamente para internacionalizar aplicativos AngularJS.

angular-translate é um módulo AngularJS que fornece filtros e diretivas, juntamente com a capacidade de carregar dados i18n de forma assíncrona. Ele suporta pluralização por meio de MessageFormat e foi projetado para ser altamente extensível e configurável.

Se você estiver usando angular-translate em seu projeto, poderá achar alguns dos seguintes pacotes super úteis:

  • angular-sanitize : pode ser usado para proteger contra ataques XSS em traduções.
  • angular-translate-interpolation-messageformat : pluralização com suporte para formatação de texto sensível ao gênero.
  • angular-translate-loader-partial : usado para entregar strings traduzidas aos clientes.

Para uma experiência verdadeiramente dinâmica, você pode adicionar angular-dynamic-locale ao grupo. Essa biblioteca permite que você altere a localidade dinamicamente — e isso inclui a forma como datas, números, moedas etc. são formatados.

Primeiros passos: instalando pacotes relevantes

Supondo que você já tenha seu clichê do AngularJS pronto, você pode usar o NPM para instalar os pacotes de internacionalização:

 npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat

Depois que os pacotes estiverem instalados, não esqueça de adicionar os módulos como dependências do seu app:

 // /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);

Observe que o nome do módulo é diferente do nome do pacote.

Traduzindo sua primeira string

Suponha que seu aplicativo tenha uma barra de ferramentas com algum texto e um campo com algum texto de espaço reservado:

 <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>

A visualização acima tem dois pedaços de texto que você pode internacionalizar: “Hello” e “Search”. Em termos de HTML, um aparece como o texto interno de uma tag âncora, enquanto o outro aparece como um valor de um atributo.

Para internacionalizá-los, você terá que substituir os dois literais de string por tokens que o AngularJS pode substituir pelas strings traduzidas reais, com base na preferência do usuário, durante a renderização da página.

O AngularJS pode fazer isso usando seus tokens para realizar uma pesquisa nas tabelas de tradução que você fornece. O módulo angular-translate espera que essas tabelas de tradução sejam fornecidas como objetos JavaScript simples ou como objetos JSON (se carregar remotamente).

Aqui está um exemplo de como essas tabelas de tradução geralmente seriam:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }

Para internacionalizar a visualização da barra de ferramentas acima, você precisa substituir os literais de string por tokens que o AngularJS pode usar para pesquisar na tabela de tradução:

 <!-- /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 como, para texto interno, você pode usar a diretiva de translate ou o filtro de translate . (Você pode aprender mais sobre a diretiva de translate aqui e sobre os filtros de translate aqui.)

Com essas alterações, quando a visualização for renderizada, angular-translate inserirá automaticamente a tradução apropriada correspondente a TOOLBAR.HELLO no DOM com base no idioma atual.

Para tokenizar literais de string que aparecem como valores de atributo, você pode usar a seguinte abordagem:

 <!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">

Agora, e se suas strings tokenizadas contivessem variáveis?

Para lidar com casos como “Hello, {{name}}.”, você pode realizar a substituição de variáveis ​​usando a mesma sintaxe de interpolador que o AngularJS já suporta:

Tabela de tradução:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }

Você pode então definir a variável de várias maneiras. Aqui estão alguns:

 <!-- /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>

Lidando com Pluralização e Gênero

A pluralização é um tópico bastante difícil quando se trata de i18n e l10n. Línguas e culturas diferentes têm regras diferentes sobre como uma língua lida com a pluralização em várias situações.

Por causa desses desafios, os desenvolvedores de software às vezes simplesmente não abordam o problema (ou pelo menos não o abordam adequadamente), resultando em software que produz frases bobas 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.

Felizmente, existe um padrão de como lidar com isso, e uma implementação JavaScript do padrão está disponível como MessageFormat.

Com MessageFormat, você pode substituir as frases mal estruturadas acima pelas seguintes:

 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 aceita expressões como as seguintes:

 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(' ');

Você pode construir um formatador com o array acima e usá-lo para gerar strings:

 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.'

Como você pode usar o MessageFormat com angular-translate para aproveitar toda a funcionalidade em seus aplicativos?

Na configuração do seu aplicativo, você simplesmente informa angular-translate que a interpolação do formato da mensagem está disponível da seguinte maneira:

 /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });

Aqui está como uma entrada na tabela de tradução pode parecer:

 // /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }

E na 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>

Aqui você deve indicar explicitamente que o interpolador de formato de mensagem deve ser usado em vez do interpolador padrão no AngularJS. Isso ocorre porque os dois interpoladores diferem ligeiramente em sua sintaxe. Você pode ler mais sobre isso aqui.

Fornecendo tabelas de tradução para seu aplicativo

Agora que você sabe como o AngularJS pode pesquisar traduções para seus tokens de tabelas de tradução, como seu aplicativo sabe sobre as tabelas de tradução em primeiro lugar? Como você informa ao seu aplicativo qual localidade/idioma deve ser usado?

É aqui que você aprende sobre $translateProvider .

Você pode fornecer as tabelas de tradução para cada localidade que deseja oferecer suporte diretamente no arquivo core.config.js do seu aplicativo da seguinte maneira:

 // /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'); });

Aqui você está fornecendo tabelas de tradução como objetos JavaScript para inglês (en) e turco (tr), enquanto declara que o idioma atual é inglês (en). Se o usuário deseja alterar o idioma, você pode fazê-lo com o serviço $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... }; });

Ainda há a questão de qual idioma deve ser usado por padrão. A codificação do idioma inicial do nosso aplicativo pode nem sempre ser aceitável. Nesses casos, uma alternativa é tentar determinar o idioma automaticamente usando $translateProvider:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });

determinePreferredLanguage procura valores em window.navigator e seleciona um padrão inteligente até que um sinal claro seja fornecido pelo usuário.

Tabelas de tradução de carregamento lento

A seção anterior mostrou como você pode fornecer tabelas de tradução diretamente no código-fonte como objetos JavaScript. Isso pode ser aceitável para aplicativos pequenos, mas a abordagem não é escalável, e é por isso que as tabelas de tradução geralmente são baixadas como arquivos JSON de um servidor remoto.

Manter as tabelas de tradução dessa forma reduz o tamanho da carga útil inicial entregue ao cliente, mas introduz complexidade adicional. Agora você enfrenta o desafio de design de entregar dados i18n ao cliente. Se isso não for tratado com cuidado, o desempenho do seu aplicativo pode sofrer desnecessariamente.

Por que é tão complexo? Os aplicativos AngularJS são organizados em módulos. Em uma aplicação complexa, pode haver muitos módulos, cada um com seus próprios dados i18n distintos. Uma abordagem ingênua, como carregar e fornecer dados i18n de uma só vez, deve ser evitada.

O que você precisa é de uma maneira de organizar seus dados i18n por módulo. Isso permitirá que você carregue apenas o que você precisa quando precisar e armazene em cache o que foi carregado anteriormente para evitar recarregar os mesmos dados (pelo menos até que o cache seja inválido).

É aqui que partialLoader entra em ação.

Digamos que as tabelas de tradução do seu aplicativo estejam estruturadas assim:

 /src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json

Você pode configurar $translateProvider para usar partialLoader com um padrão de URL que corresponda a esta estrutura:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });

Como seria de esperar, “lang” é substituído pelo código do idioma em tempo de execução (por exemplo, “en” ou “tr”). E a “parte”? Como $translateProvider sabe qual “parte” carregar?

Você pode fornecer essas informações dentro dos controladores com $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'); });

O padrão agora está completo e os dados i18n para uma determinada visualização são carregados quando seu controlador é executado pela primeira vez, que é exatamente o que você deseja.

Cache: reduzindo os tempos de carregamento

E quanto ao cache?

Você pode habilitar o cache padrão na configuração do aplicativo com $translateProvider :

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });

Se você precisar quebrar o cache para um determinado idioma, você pode usar $translate :

 $translate.refresh(languageKey); // omit languageKey to refresh all

Com essas peças no lugar, seu aplicativo é totalmente internacionalizado e suporta vários idiomas.

Localização de números, moedas e datas

Nesta seção, você aprenderá como usar angular-dynamic-locale para dar suporte à formatação de elementos de interface do usuário, como números, moedas, datas e similares, em um aplicativo AngularJS.

Você precisará instalar mais dois pacotes para isso:

 npm i -S angular-dynamic-locale angular-i18n

Depois que os pacotes estiverem instalados, você poderá adicionar o módulo às dependências do seu aplicativo:

 // /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);

Regras de localidade

As regras de localidade são arquivos JavaScript simples que fornecem especificações sobre como datas, números, moedas e similares devem ser formatados por componentes que dependem do serviço $locale.

A lista de localidades atualmente suportadas está disponível aqui.

Aqui está um trecho de angular-locale_en-us.js ilustrando a formatação de mês e data:

 ... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...

Ao contrário dos dados i18n, as regras de localidade são globais para o aplicativo, exigindo que as regras de uma determinada localidade sejam carregadas de uma só vez.

Por padrão, angular-dynamic-locale espera que os arquivos de regras de localidade estejam localizados em angular/i18n/angular-locale_{{locale}}.js . Se eles estiverem localizados em outro lugar, tmhDynamicLocaleProvider deve ser usado para substituir o padrão:

 // /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });

O armazenamento em cache é tratado automaticamente pelo serviço tmhDynamicLocaleCache .

Invalidar o cache é uma preocupação menor aqui, já que as regras de localidade são menos propensas a mudar do que as traduções de strings.

Para alternar entre localidades, angular-dynamic-locale fornece o serviço 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... }; });

Gerando tabelas de tradução com tradução automática

As regras de localidade são enviadas com o pacote angular-i18n , portanto, tudo o que você precisa fazer é disponibilizar o conteúdo do pacote para seu aplicativo conforme necessário. Mas como você gera os arquivos JSON para suas tabelas de tradução? Não há exatamente um pacote que você possa baixar e conectar em nosso aplicativo.

Uma opção é usar APIs de tradução programática, especialmente se as strings em seu aplicativo forem literais simples sem variáveis ​​ou expressões pluralizadas.

Com o Gulp e alguns pacotes extras, solicitar traduções programáticas para seu aplicativo é muito fácil:

 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 ...

O script primeiro lê todas as tabelas de tradução em inglês, solicita traduções de forma assíncrona para seus recursos de string e, em seguida, substitui as strings em inglês pelas strings traduzidas para produzir uma tabela de tradução em um novo idioma.

Finalmente, a nova tabela de tradução é escrita como um irmão da tabela de tradução em inglês, resultando em:

 /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 ...

A implementação de getTranslation também é simples:

 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 }); }

Aqui, estamos usando o Microsoft Translate, mas pode-se facilmente usar outro provedor, como o Google Translate ou o Yandex Translate.

Embora as traduções programáticas sejam convenientes, existem várias desvantagens, incluindo:

  • As traduções de robôs são boas para strings curtas, mas mesmo assim, pode haver armadilhas com palavras que têm significados diferentes em contextos diferentes (por exemplo, “piscina” pode significar nadar ou agrupar).
  • As APIs podem não ser capazes de lidar com strings com variáveis ​​ou strings que dependem do formato da mensagem.

Nesses casos e em outros, traduções humanas podem ser necessárias; no entanto, isso é assunto para outra postagem no blog.

Internacionalizar front-ends parece assustador

Neste artigo, você aprendeu como usar esses pacotes para internacionalizar e localizar aplicativos AngularJS.

angular-translate , angular-dynamic-locale e gulp são ferramentas poderosas para internacionalizar um aplicativo AngularJS que encapsula detalhes dolorosos de implementação de baixo nível.

Para um aplicativo de demonstração que ilustra as ideias discutidas neste post, confira este repositório do GitHub.