Uso correcto de la traducción angular en controladores

121

Estoy usando angular-translate para i18n en una aplicación AngularJS.

Para cada vista de la aplicación, hay un controlador dedicado. En los controladores siguientes, configuro el valor que se mostrará como título de la 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';
    }])

Estoy cargando los archivos de traducción usando la extensión angular-translate-loader-url .

Problema

En la carga de la página inicial, se muestra la clave de traducción en lugar de la traducción de esa clave. La traducción es Hello, World!, pero estoy viendo HELLO_WORLD.

La segunda vez que voy a la página, todo está bien y se muestra la versión traducida.

Supongo que el problema tiene que ver con el hecho de que tal vez el archivo de traducción aún no esté cargado cuando el controlador asigna el valor a $scope.pageTitle.

Observación

Al usar <h1>{{ pageTitle | translate }}</h1>y $scope.pageTitle = 'HELLO_WORLD';, la traducción funciona perfectamente desde la primera vez. El problema con esto es que no siempre quiero usar traducciones (por ejemplo, para el segundo controlador solo quiero pasar una cadena sin procesar).

Pregunta

¿Es este un problema / limitación conocido? ¿Cómo se puede solucionar esto?

ndequeker
fuente

Respuestas:

69

EDITAR : consulte la respuesta de PascalPrecht (el autor de angular-translate) para una mejor solución.


La naturaleza asincrónica de la carga causa el problema. Verá, con {{ pageTitle | translate }}, Angular observará la expresión; cuando se cargan los datos de localización, el valor de la expresión cambia y la pantalla se actualiza.

Entonces, puedes hacerlo tú mismo:

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

Sin embargo, esto ejecutará la expresión observada en cada ciclo de resumen. Esto es subóptimo y puede o no causar una degradación visible del rendimiento. De todos modos, es lo que hace Angular, por lo que no puede ser tan malo ...

Nikos Paraskevopoulos
fuente
¡Gracias! Esperaría que usar un filtro en la Vista o en un Controlador se comportara exactamente igual. Ese no parece ser el caso aquí.
ndequeker
Yo diría que usar un $scope.$watches bastante exagerado ya que Angular Translate ofrece un servicio para usar en los controladores. Vea mi respuesta a continuación.
Robin van Baalen
1
No se requiere el filtro Angular Translate, ya que $translate.instant()ofrece lo mismo que un servicio. Además de esto, preste atención a la respuesta de Pascal.
knalli
Estoy de acuerdo, usar $ watch es excesivo. A continuación, las respuestas son un uso más adecuado.
jpblancoder
141

Recomendado: no traduzca en el controlador, traduzca en su vista

Recomendaría mantener su controlador libre de lógica de traducción y traducir sus cadenas directamente dentro de su vista de esta manera:

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

Usando el servicio provisto

Angular Translate proporciona el $translateservicio que puede utilizar en sus controladores.

Un ejemplo de uso del $translateservicio puede ser:

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

El servicio de traducción también tiene un método para traducir cadenas directamente sin necesidad de manejar una promesa, usando $translate.instant():

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

La desventaja de usar $translate.instant() podría ser que el archivo de idioma aún no está cargado si lo está cargando de forma asincrónica.

Usando el filtro provisto

Esta es mi forma preferida ya que no tengo que manejar las promesas de esta forma. La salida del filtro se puede establecer directamente en una variable de alcance.

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

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

Usando la directiva proporcionada

Dado que @PascalPrecht es el creador de esta increíble biblioteca, recomendaría seguir su consejo (ver su respuesta a continuación) y usar la directiva provista que parece manejar las traducciones de manera muy inteligente.

La directiva se encarga de la ejecución asincrónica y también es lo suficientemente inteligente como para dejar de ver los identificadores de traducción en el alcance si la traducción no tiene valores dinámicos.

Robin van Baalen
fuente
Si lo intentara en lugar de escribir ese comentario no relacionado, ya sabría la respuesta. Respuesta corta: sí. Eso es posible.
Robin van Baalen
1
en su ejemplo con el filtro en el controlador: como con instant (), si el archivo de idioma no está cargado, esto no funcionará, ¿verdad? ¿No deberíamos usar un reloj en ese caso? ¿O quiere decir 'use el filtro solo si sabe que las traducciones están cargadas?
Bombinosh
@Bombinosh Yo diría que use el método de filtro si sabe que las traducciones están cargadas. Personalmente, incluso recomendaría no cargar las traducciones de forma dinámica si no es necesario. Es una parte obligatoria de su aplicación, por lo que es mejor que no desee que el usuario la esté esperando. Pero esa es una opinión personal.
Robin van Baalen
El objetivo de las traducciones es que pueden cambiar según las preferencias del usuario o incluso según la acción del usuario. Por lo que necesita, en general, cargarlos dinámicamente. Al menos si la cantidad de cadenas a traducir es importante y / o si tiene muchas traducciones.
PhiLho
4
Cuando la traducción se realiza en HTML, el ciclo de resumen se ejecuta dos veces, pero solo se ejecuta una vez en el controlador. En el 99% de los casos, esto probablemente no importe, pero tuve un problema con un rendimiento terrible en una cuadrícula de interfaz de usuario angular con traducciones en muchas celdas. Un caso de vanguardia seguro, algo a tener en cuenta
tykowale
123

En realidad, debería usar la directiva translate para tales cosas.

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

La directiva se encarga de la ejecución asincrónica y también es lo suficientemente inteligente como para dejar de ver los identificadores de traducción en el alcance si la traducción no tiene valores dinámicos.

Sin embargo, si no hay forma de evitarlo y realmente tiene que usar el $translateservicio en el controlador, debe envolver la llamada en un $translateChangeSuccessevento usando $rootScopeen combinación con $translate.instant()esto:

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

Entonces, ¿por qué $rootScopey no $scope? La razón de ello es que, en angular traducen de eventos se $emited en $rootScopelugar de $broadcastEd en $scopeporque no necesito para transmitir a través de toda la jerarquía alcance.

¿Por qué $translate.instant()y no solo asincrónico $translate()? Cuando $translateChangeSuccessse dispara el evento, es seguro que los datos de traducción necesarios están allí y no se está produciendo una ejecución asíncrona (por ejemplo, ejecución de cargador asíncrono), por lo tanto, podemos usar el $translate.instant()que es síncrono y simplemente asume que las traducciones están disponibles.

Desde la versión 2.8.0 también existe $translate.onReady(), que devuelve una promesa que se resuelve tan pronto como las traducciones están listas. Ver el registro de cambios .

Pascal Precht
fuente
¿Podría haber algún problema de rendimiento si uso la directiva de traducción en lugar del filtro? También creo que internamente, observa el valor de retorno de instant (). Entonces, ¿quita los relojes cuando se destruye el alcance actual?
Nilesh
Intenté usar su sugerencia pero no funciona cuando el valor de la variable de alcance cambia dinámicamente.
Nilesh
10
En realidad, siempre es mejor evitar los filtros siempre que sea posible, ya que ralentizan su aplicación porque siempre configuran nuevos relojes. La directiva, sin embargo, va un poco más allá. Comprueba si tiene que observar el valor de una identificación de traducción o no. Eso permite que su aplicación funcione mejor. ¿Podrías hacer un ruido y vincularme a él, para que pueda echar un vistazo más de cerca?
Pascal Precht
Plunk: plnkr.co/edit/j53xL1EdJ6bT20ldlhxr Probablemente en mi ejemplo, la directiva está decidiendo no mirar el valor. También como un problema separado, se llama a mi controlador de errores personalizado si no se encuentra la clave, pero no muestra la cadena devuelta. Haré otro golpe por él.
Nilesh
2
@PascalPrecht Solo una pregunta, ¿es una buena práctica utilizar vincular una vez con la traducción? Como este {{::'HELLO_WORLD | translate}}'.
Zunair Zubair
5

Para hacer una traducción en el controlador, puede usar el $translateservicio:

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

Esa declaración solo hace la traducción en la activación del controlador, pero no detecta el cambio de tiempo de ejecución en el idioma. Para lograr ese comportamiento, puede escuchar el $rootScopeevento: $translateChangeSuccessy hacer la misma traducción allí:

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

Por supuesto, puede encapsular el $translateservicio en un método y llamarlo en el controlador y en el $translateChangeSucessoyente.

MacLeod
fuente
1

Lo que está sucediendo es que Angular-translate está observando la expresión con un sistema basado en eventos, y al igual que en cualquier otro caso de enlace o enlace bidireccional, se dispara un evento cuando se recuperan los datos y se cambia el valor, lo que obviamente no funciona para la traducción. Los datos de traducción, a diferencia de otros datos dinámicos de la página, deben, por supuesto, mostrarse inmediatamente al usuario. No puede aparecer después de que se cargue la página.

Incluso si puede depurar con éxito este problema, el mayor problema es que el trabajo de desarrollo involucrado es enorme. Un desarrollador tiene que extraer manualmente todas las cadenas del sitio, ponerlas en un archivo .json y hacer referencia a ellas manualmente mediante un código de cadena (es decir, 'pageTitle' en este caso). La mayoría de los sitios comerciales tienen miles de cadenas para las que esto debe suceder. Y eso es sólo el comienzo. Ahora necesita un sistema para mantener las traducciones sincronizadas cuando el texto subyacente cambia en algunas de ellas, un sistema para enviar los archivos de traducción a los distintos traductores, para reintegrarlos en la compilación, para volver a implementar el sitio para que los traductores puedan ver sus cambios de contexto, y así sucesivamente.

Además, como se trata de un sistema 'vinculante' basado en eventos, se activa un evento para cada cadena de la página, que no solo es una forma más lenta de transformar la página, sino que también puede ralentizar todas las acciones de la página. si comienza a agregarle una gran cantidad de eventos.

De todos modos, usar una plataforma de traducción de posprocesamiento tiene más sentido para mí. Usando GlobalizeIt, por ejemplo, un traductor puede simplemente ir a una página del sitio y comenzar a editar el texto directamente en la página para su idioma, y ​​eso es todo: https://www.globalizeit.com/HowItWorks . No se necesita programación (aunque puede ser extensible mediante programación), se integra fácilmente con Angular: https://www.globalizeit.com/Translate/Angular , la transformación de la página ocurre de una vez y siempre muestra el texto traducido con el render inicial de la página.

Divulgación completa: soy cofundador :)

Jeff W
fuente