Uso correto para conversão angular em controladores

121

Estou usando angular-translate para i18n em um aplicativo AngularJS.

Para todas as visualizações de aplicativos, há um controlador dedicado. Nos controladores abaixo, defino o valor a ser mostrado como o título da página.

Código

HTML

<h1>{{ pageTitle }}</h1>

Javascript

.controller('FirstPageCtrl', ['$scope', '$filter', function ($scope, $filter) {
        $scope.pageTitle = $filter('translate')('HELLO_WORLD');
    }])

.controller('SecondPageCtrl', ['$scope', '$filter', function ($scope, $filter) {
        $scope.pageTitle = 'Second page title';
    }])

Estou carregando os arquivos de tradução usando a extensão angular-translate-loader-url .

Problema

No carregamento inicial da página, a chave de conversão é mostrada em vez da tradução dessa chave. A tradução é Hello, World!, mas estou vendo HELLO_WORLD.

Na segunda vez que vou à página, tudo está bem e a versão traduzida é mostrada.

Suponho que o problema esteja relacionado ao fato de que talvez o arquivo de tradução ainda não esteja carregado quando o controlador estiver atribuindo o valor $scope.pageTitle.

Observação

Ao usar <h1>{{ pageTitle | translate }}</h1>e$scope.pageTitle = 'HELLO_WORLD'; , a tradução funciona perfeitamente desde a primeira vez. O problema disso é que nem sempre quero usar traduções (por exemplo, para o segundo controlador, só quero passar uma string bruta).

Questão

Esse é um problema / limitação conhecido? como isso pode ser resolvido?

ndequeker
fonte

Respostas:

69

EDIT : Por favor, veja a resposta de PascalPrecht (o autor do angular-translate) para uma solução melhor.


A natureza assíncrona do carregamento causa o problema. Veja, com {{ pageTitle | translate }}Angular observará a expressão; Quando os dados de localização são carregados, o valor da expressão muda e a tela é atualizada.

Então, você pode fazer isso sozinho:

.controller('FirstPageCtrl', ['$scope', '$filter', function ($scope, $filter) {
    $scope.$watch(
        function() { return $filter('translate')('HELLO_WORLD'); },
        function(newval) { $scope.pageTitle = newval; }
    );
});

No entanto, isso executará a expressão observada em todos os ciclos de digestão. Isso é abaixo do ideal e pode ou não causar uma degradação visível no desempenho. Enfim, é o que o Angular faz, então não pode ser tão ruim assim ...

Nikos Paraskevopoulos
fonte
Obrigado! Eu esperaria que o uso de um filtro na View ou em um Controller se comportasse exatamente da mesma maneira. Esse não parece ser o caso aqui.
Ndequeker 12/12
Eu diria que usar um $scope.$watché um exagero, já que o Angular Translate está oferecendo um serviço para ser usado nos controladores. Veja minha resposta abaixo.
precisa
1
O filtro Angular Translate não é necessário, pois $translate.instant()oferece o mesmo que um serviço. Além disso, preste atenção à resposta de Pascal.
knalli
Eu concordo, usar $ watch é um exagero. As respostas abaixo são de uso mais adequado.
jpblancoder
141

Recomendado: não traduza no controlador, traduza na sua opinião

Eu recomendo manter seu controlador livre da lógica de tradução e traduzir suas strings diretamente dentro da sua visualização da seguinte maneira:

<h1>{{ 'TITLE.HELLO_WORLD' | translate }}</h1>

Usando o serviço fornecido

O Angular Translate fornece o $translateserviço que você pode usar em seus Controladores.

Um exemplo de uso do $translateserviço pode ser:

.controller('TranslateMe', ['$scope', '$translate', function ($scope, $translate) {
    $translate('PAGE.TITLE')
        .then(function (translatedValue) {
            $scope.pageTitle = translatedValue;
        });
});

O serviço de conversão também possui um método para traduzir diretamente as strings sem a necessidade de lidar com uma promessa, usando $translate.instant():

.controller('TranslateMe', ['$scope', '$translate', function ($scope, $translate) {
    $scope.pageTitle = $translate.instant('TITLE.DASHBOARD'); // Assuming TITLE.DASHBOARD is defined
});

A desvantagem de usar $translate.instant()pode ser que o arquivo de idioma ainda não esteja carregado se você estiver carregando de forma assíncrona.

Usando o filtro fornecido

Esta é a minha maneira preferida, pois não preciso lidar com promessas dessa maneira. A saída do filtro pode ser definida diretamente para uma variável de escopo.

.controller('TranslateMe', ['$scope', '$filter', function ($scope, $filter) {
    var $translate = $filter('translate');

    $scope.pageTitle = $translate('TITLE.DASHBOARD'); // Assuming TITLE.DASHBOARD is defined
});

Usando a diretiva fornecida

Como o @PascalPrecht é o criador desta incrível biblioteca, recomendo que siga o conselho dele (veja a resposta abaixo) e use a diretiva fornecida, que parece lidar com traduções de maneira muito inteligente.

A diretiva cuida da execução assíncrona e também é inteligente o suficiente para desbloquear os IDs de tradução no escopo se a tradução não tiver valores dinâmicos.

Robin van Baalen
fonte
Se você tentasse em vez de escrever esse comentário não relacionado, já saberia a resposta agora. Resposta curta: sim. Isso é possível.
Robin van Baalen
1
no seu exemplo com o filtro no controlador: como no instante (), se o arquivo de idioma não estiver carregado, isso não funcionará certo? Nesse caso, não devemos usar um relógio? Ou você quer dizer 'use o filtro apenas se souber que as traduções foram carregadas?
Bombinosh
@Bombinosh Eu diria que use o método de filtro se você souber que as traduções estão carregadas. Pessoalmente, eu recomendaria não carregar traduções dinamicamente se você não precisar. É uma parte obrigatória do seu aplicativo, por isso é melhor você não querer que o usuário o aguarde. Mas essa é uma opinião pessoal.
Robin van Baalen
O objetivo das traduções é que elas podem mudar nas preferências do usuário ou mesmo na ação do usuário. Então você precisa, em geral, carregá-los dinamicamente. Pelo menos se o número de sequências a serem traduzidas for importante e / ou se você tiver muitas traduções.
PhiLho 7/10
4
Quando a tradução é feita no HTML, o ciclo de resumo é executado duas vezes, mas apenas uma vez no controlador. 99% dos casos provavelmente não importam, mas tive um problema com um desempenho terrível em uma grade de interface do usuário angular com traduções em muitas células. Um caso de ponta, com certeza, apenas algo para estar ciente #
tykowale 12/02/16
123

Na verdade, você deve usar a diretiva translate para essas coisas.

<h1 translate="{{pageTitle}}"></h1>

A diretiva cuida da execução assíncrona e também é inteligente o suficiente para desbloquear os IDs de tradução no escopo se a tradução não tiver valores dinâmicos.

No entanto, se não houver maneira de contornar e você realmente precisar usar o $translateserviço no controlador, encerre a chamada em um $translateChangeSuccessevento usando $rootScopeem combinação com o $translate.instant()seguinte:

.controller('foo', function ($rootScope, $scope, $translate) {
  $rootScope.$on('$translateChangeSuccess', function () {
    $scope.pageTitle = $translate.instant('PAGE.TITLE');
  });
})

Então, por que $rootScopenão $scope? A razão disso é que, nos eventos do angular-translate, são $emiteditados $rootScopee não $broadcasteditados $scopeporque não precisamos transmitir por toda a hierarquia do escopo.

Por que $translate.instant()e não apenas assíncrono $translate()? Quando o $translateChangeSuccessevento é disparado, é certo que os dados de conversão necessários estão lá e nenhuma execução assíncrona está acontecendo (por exemplo, execução assíncrona do carregador); portanto, podemos apenas usar o $translate.instant()que é síncrono e apenas assume que as traduções estão disponíveis.

Desde a versão 2.8.0, também há $translate.onReady() , o que retorna uma promessa que é resolvida assim que as traduções estiverem prontas. Veja o changelog .

Pascal Precht
fonte
Pode haver algum problema de desempenho se eu usar a diretiva translate em vez do filtro? Também acredito internamente, ele assiste o valor de retorno de instant (). Então, ele remove os relógios quando o escopo atual é destruído?
Nilesh
Tentei usar sua sugestão, mas ela não funciona quando o valor da variável de escopo muda dinamicamente.
Nilesh
10
Na verdade, é sempre melhor evitar filtros sempre que possível, pois eles tornam o aplicativo mais lento porque sempre configuram novos relógios. A diretiva, no entanto, vai um pouco mais longe. Ele verifica se é necessário observar o valor de um ID de tradução ou não. Isso permite executar melhor seu aplicativo. Você poderia fazer uma jogada e me ligar a ela, para que eu possa dar uma olhada?
Pascal Precht
Plunk: plnkr.co/edit/j53xL1EdJ6bT20ldlhxr Provavelmente, no meu exemplo, a diretiva está decidindo não observar o valor. Também como um problema separado, meu manipulador de erros personalizado é chamado se a chave não for encontrada, mas não exibe a string retornada. Eu farei outro esforço por isso.
Nilesh
2
@PascalPrecht Apenas uma pergunta, é uma boa prática usar o bind-once com tradução? Assim {{::'HELLO_WORLD | translate}}'.
Zunair Zubair
5

Para fazer uma tradução no controlador, você pode usar o $translateserviço:

$translate(['COMMON.SI', 'COMMON.NO']).then(function (translations) {
    vm.si = translations['COMMON.SI'];
    vm.no = translations['COMMON.NO'];
});

Essa declaração faz apenas a tradução na ativação do controlador, mas não detecta a alteração no tempo de execução no idioma. Para atingir esse comportamento, você pode ouvir o $rootScopeevento: $translateChangeSuccesse fazer a mesma tradução lá:

    $rootScope.$on('$translateChangeSuccess', function () {
        $translate(['COMMON.SI', 'COMMON.NO']).then(function (translations) {
            vm.si = translations['COMMON.SI'];
            vm.no = translations['COMMON.NO'];
        });
    });

Obviamente, você pode encapsular o $translateserviço em um método e chamá-lo no controlador e no $translateChangeSucessouvinte.

MacLeod
fonte
1

O que está acontecendo é que o Angular-translate está assistindo a expressão com um sistema baseado em eventos e, como em qualquer outro caso de ligação ou de mão dupla, um evento é disparado quando os dados são recuperados e o valor alterado, o que é alterado. obviamente não funciona para tradução. Os dados de tradução, diferentemente de outros dados dinâmicos da página, devem, é claro, aparecer imediatamente para o usuário. Não pode aparecer após o carregamento da página.

Mesmo que você possa depurar com êxito esse problema, o maior problema é que o trabalho de desenvolvimento envolvido é enorme. Um desenvolvedor precisa extrair manualmente todas as strings do site, colocá-las em um arquivo .json, referenciá-las manualmente pelo código da string (por exemplo, 'pageTitle' neste caso). A maioria dos sites comerciais possui milhares de strings para as quais isso precisa acontecer. E isso é apenas o começo. Agora você precisa de um sistema para manter as traduções sincronizadas quando o texto subjacente for alterado em algumas delas, um sistema para enviar os arquivos de tradução para os vários tradutores, reintegrá-los na compilação, reimplementar o site para que os tradutores possam ver suas mudanças de contexto, e assim por diante.

Além disso, como este é um sistema baseado em eventos "vinculativo", um evento está sendo acionado para cada sequência de caracteres na página, o que não só é uma maneira mais lenta de transformar a página, mas também pode retardar todas as ações da página, se você começar a adicionar um grande número de eventos a ele.

De qualquer forma, usar uma plataforma de tradução pós-processamento faz mais sentido para mim. Usando o GlobalizeIt, por exemplo, um tradutor pode simplesmente ir para uma página no site e começar a editar o texto diretamente na página para o idioma deles, e é isso: https://www.globalizeit.com/HowItWorks . Sem necessidade de programação (embora possa ser programaticamente extensível), ele se integra facilmente ao Angular: https://www.globalizeit.com/Translate/Angular , a transformação da página ocorre de uma só vez e sempre exibe o texto traduzido com a renderização inicial da página.

Divulgação completa: Sou co-fundador :)

Jeff W
fonte