Utilisation correcte de la translation angulaire dans les contrôleurs

121

J'utilise angular-translate pour i18n dans une application AngularJS.

Pour chaque vue d'application, il existe un contrôleur dédié. Dans les contrôleurs ci-dessous, j'ai défini la valeur à afficher comme titre de page.

Code

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

Je charge les fichiers de traduction à l'aide de l' extension angular-translate-loader-url .

Problème

Lors du chargement initial de la page, la clé de traduction est affichée à la place de la traduction de cette clé. La traduction est Hello, World!, mais je vois HELLO_WORLD.

La deuxième fois que je vais sur la page, tout va bien et la version traduite est affichée.

Je suppose que le problème est lié au fait que le fichier de traduction n'est peut-être pas encore chargé lorsque le contrôleur attribue la valeur à $scope.pageTitle.

Remarque

Lors de l'utilisation <h1>{{ pageTitle | translate }}</h1>et$scope.pageTitle = 'HELLO_WORLD'; , la traduction fonctionne parfaitement dès la première fois. Le problème avec ceci est que je ne veux pas toujours utiliser des traductions (par exemple, pour le deuxième contrôleur, je veux juste passer une chaîne brute).

Question

S'agit-il d'un problème / limitation connu? Comment cela peut-il être résolu?

ndequeker
la source

Réponses:

69

EDIT : Veuillez consulter la réponse de PascalPrecht (l'auteur de angular-translate) pour une meilleure solution.


La nature asynchrone du chargement provoque le problème. Vous voyez, avec {{ pageTitle | translate }}, Angular surveillera l'expression; lorsque les données de localisation sont chargées, la valeur de l'expression change et l'écran est mis à jour.

Donc, vous pouvez le faire vous-même:

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

Cependant, cela exécutera l'expression surveillée à chaque cycle de résumé. Ceci est sous-optimal et peut ou non provoquer une dégradation visible des performances. Quoi qu'il en soit, c'est ce que fait Angular, donc ça ne peut pas être si mal ...

Nikos Paraskevopoulos
la source
Je vous remercie! Je m'attendrais à ce que l'utilisation d'un filtre dans la vue ou dans un contrôleur se comporte exactement de la même manière. Cela ne semble pas être le cas ici.
ndequeker
Je dirais que l'utilisation d'un $scope.$watchest plutôt exagéré car Angular Translate propose un service à utiliser dans les contrôleurs. Voir ma réponse ci-dessous.
Robin van Baalen
1
Le filtre Angular Translate n'est pas requis, car il $translate.instant()offre la même chose qu'un service. A côté de cela, faites attention à la réponse de Pascal.
knalli
Je suis d'accord, utiliser $ watch est excessif. Les réponses ci-dessous sont une utilisation plus appropriée.
jpblancoder
141

Recommandé: ne traduisez pas dans le contrôleur, traduisez dans votre vue

Je vous recommande de garder votre contrôleur exempt de logique de traduction et de traduire vos chaînes directement dans votre vue comme ceci:

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

Utilisation du service fourni

Angular Translate fournit le $translateservice que vous pouvez utiliser dans vos contrôleurs.

Un exemple d'utilisation du $translateservice peut être:

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

Le service de traduction a également une méthode pour traduire directement des chaînes sans avoir besoin de gérer une promesse, en utilisant $translate.instant():

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

L'inconvénient de l'utilisation $translate.instant()pourrait être que le fichier de langue n'est pas encore chargé si vous le chargez de manière asynchrone.

Utilisation du filtre fourni

C'est ma méthode préférée car je n'ai pas à gérer les promesses de cette façon. La sortie du filtre peut être directement définie sur une variable de portée.

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

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

Utilisation de la directive fournie

Puisque @PascalPrecht est le créateur de cette bibliothèque géniale, je recommanderais de suivre son conseil (voir sa réponse ci-dessous) et d'utiliser la directive fournie qui semble gérer les traductions très intelligemment.

La directive prend en charge l'exécution asynchrone et est également suffisamment intelligente pour déverrouiller les identifiants de traduction sur la portée si la traduction n'a pas de valeurs dynamiques.

Robin van Baalen
la source
Si vous l'essayez au lieu d'écrire ce commentaire sans rapport, vous connaissez la réponse maintenant. Réponse courte: oui. C'est possible.
Robin van Baalen le
1
dans votre exemple avec le filtre dans le contrôleur: comme avec instant (), si le fichier de langue n'est pas chargé, cela ne fonctionnera pas? Ne devrions-nous pas utiliser une montre dans ce cas? Ou vous voulez dire «n'utilisez le filtre que si vous savez que les traductions sont chargées?
Bombinosh
@Bombinosh Je dirais que j'utilise la méthode de filtrage si vous savez que les traductions sont chargées. Personnellement, je recommanderais même de ne pas charger les traductions dynamiquement si vous n'êtes pas obligé. C'est une partie obligatoire de votre application, il vaut donc mieux ne pas vouloir que l'utilisateur l'attende. Mais c'est une opinion personnelle.
Robin van Baalen
L'intérêt des traductions est qu'elles peuvent changer les préférences de l'utilisateur ou même l'action de l'utilisateur. Vous devez donc, en général, les charger dynamiquement. Du moins si le nombre de chaînes à traduire est important, et / ou si vous avez beaucoup de traductions.
PhiLho
4
Lorsque la traduction est effectuée dans le HTML, le cycle de résumé est exécuté deux fois, mais une seule fois dans le contrôleur. 99% des cas, cela n'aura probablement pas d'importance, mais j'ai eu un problème avec des performances terribles dans une grille d'interface utilisateur angulaire avec des traductions dans de nombreuses cellules. Un cas de pointe à coup sûr, juste quelque chose à savoir
tykowale
123

En fait, vous devriez plutôt utiliser la directive translate pour ce genre de choses.

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

La directive prend en charge l'exécution asynchrone et est également suffisamment intelligente pour déverrouiller les identifiants de traduction sur la portée si la traduction n'a pas de valeurs dynamiques.

Cependant, s'il n'y a pas moyen de contourner et vous vraiment avoir à utiliser le $translateservice dans le contrôleur, vous devez envelopper l'appel dans un $translateChangeSuccessévénement à l' aide $rootScopeen combinaison avec $translate.instant()comme ceci:

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

Alors pourquoi $rootScopeet pas $scope? La raison en est que, dans angular-translate, les événements sont $emitédités $rootScopeplutôt que $broadcastmodifiés $scopeparce que nous n'avons pas besoin de diffuser à travers toute la hiérarchie de portée.

Pourquoi $translate.instant()et pas seulement asynchrone $translate()? Lorsque l' $translateChangeSuccessévénement est déclenché, il est certain que les données de traduction nécessaires sont présentes et qu'aucune exécution asynchrone ne se produit (par exemple, l'exécution du chargeur asynchrone), nous pouvons donc simplement utiliser $translate.instant()ce qui est synchrone et suppose simplement que les traductions sont disponibles.

Depuis la version 2.8.0, il y a aussi $translate.onReady(), qui renvoie une promesse qui est résolue dès que les traductions sont prêtes. Voir le changelog .

Pascal Precht
la source
Pourrait-il y avoir des problèmes de performances si j'utilise la directive translate au lieu du filtre? Je crois aussi qu'en interne, il regarde la valeur de retour de instant (). Alors, supprime-t-il les montres lorsque la lunette actuelle est détruite?
Nilesh le
J'ai essayé d'utiliser votre suggestion mais cela ne fonctionne pas lorsque la valeur de la variable d'étendue change de manière dynamique.
Nilesh du
10
En fait, il est toujours préférable d'éviter les filtres lorsque cela est possible, car ils ralentissent votre application car ils configurent toujours de nouvelles montres. La directive va cependant un peu plus loin. Il vérifie s'il doit surveiller la valeur d'un identifiant de traduction ou non. Cela permet de mieux exécuter votre application. Pourriez-vous faire un plunk et m'y relier, pour que je puisse y jeter un coup d'œil?
Pascal Precht
Plunk: plnkr.co/edit/j53xL1EdJ6bT20ldlhxr Probablement dans mon exemple, la directive décide de ne pas regarder la valeur. Également comme problème séparé, mon gestionnaire d'erreurs personnalisé est appelé si la clé n'est pas trouvée, mais il n'affiche pas la chaîne retournée. Je vais faire une autre tentative pour cela.
Nilesh
2
@PascalPrecht Juste une question, est-ce une bonne pratique d'utiliser bind-once avec la traduction? Comme ça {{::'HELLO_WORLD | translate}}'.
Zunair Zubair
5

Pour faire une traduction dans le contrôleur, vous pouvez utiliser le $translateservice:

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

Cette instruction ne fait que la traduction lors de l'activation du contrôleur, mais elle ne détecte pas le changement de langue à l'exécution. Afin d'obtenir ce comportement, vous pouvez écouter l' $rootScopeévénement: $translateChangeSuccesset y faire la même traduction:

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

Bien sûr, vous pouvez encapsuler le $translateservice dans une méthode et l'appeler dans le contrôleur et dans l' $translateChangeSucessécouteur.

MacLeod
la source
1

Ce qui se passe, c'est que Angular-translate surveille l'expression avec un système basé sur des événements, et comme dans tout autre cas de liaison ou de liaison bidirectionnelle, un événement est déclenché lorsque les données sont récupérées et la valeur modifiée, ce qui ne fonctionne évidemment pas pour la traduction. Les données de traduction, contrairement aux autres données dynamiques de la page, doivent bien sûr apparaître immédiatement à l'utilisateur. Il ne peut pas apparaître après le chargement de la page.

Même si vous parvenez à déboguer ce problème avec succès, le plus gros problème est que le travail de développement impliqué est énorme. Un développeur doit extraire manuellement chaque chaîne du site, la mettre dans un fichier .json, la référencer manuellement par code de chaîne (c'est-à-dire «pageTitle» dans ce cas). La plupart des sites commerciaux ont des milliers de chaînes pour lesquelles cela doit se produire. Et ce n'est que le début. Vous avez maintenant besoin d'un système permettant de synchroniser les traductions lorsque le texte sous-jacent change dans certains d'entre eux, d'un système d'envoi des fichiers de traduction aux différents traducteurs, de les réintégrer dans la construction, de redéployer le site pour que les traducteurs puissent voir leurs changements de contexte, et ainsi de suite.

De plus, comme il s'agit d'un système basé sur des événements, un événement est déclenché pour chaque chaîne de la page, ce qui non seulement est un moyen plus lent de transformer la page, mais peut ralentir toutes les actions sur la page, si vous commencez à y ajouter un grand nombre d'événements.

Quoi qu'il en soit, utiliser une plate-forme de traduction post-traitement a plus de sens pour moi. En utilisant GlobalizeIt par exemple, un traducteur peut simplement accéder à une page du site et commencer à éditer le texte directement sur la page pour sa langue, et c'est tout: https://www.globalizeit.com/HowItWorks . Aucune programmation nécessaire (bien qu'il puisse être extensible par programme), il s'intègre facilement à Angular: https://www.globalizeit.com/Translate/Angular , la transformation de la page se produit en une seule fois, et il affiche toujours le texte traduit avec le rendu initial de la page.

Divulgation complète: je suis co-fondateur :)

Jeff W
la source