126

I'm using angular-translate for i18n in an AngularJS application.

For every application view, there is a dedicated controller. In the controllers below, I set the value to be shown as the page title.

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

I'm loading the translation files using the angular-translate-loader-url extension.

Problem

On the initial page load, the translation key is shown instead of the translation for that key. The translation is Hello, World!, but I'm seeing HELLO_WORLD.

The second time I go to the page, all is well and the translated version is shown.

I assume the issue has to do with the fact that maybe the translation file is not yet loaded when the controller is assigning the value to $scope.pageTitle.

Remark

When using <h1>{{ pageTitle | translate }}</h1> and $scope.pageTitle = 'HELLO_WORLD';, the translation works perfect from the first time. The problem with this is that I don't always want to use translations (eg. for the second controller I just want to pass a raw string).

Question

Is this a known issue / limitation? How can this be solved?

Dmitry Gonchar
  • 1,642
  • 2
  • 16
  • 27
ndequeker
  • 7,932
  • 7
  • 61
  • 93

5 Answers5

146

Recommended: don't translate in the controller, translate in your view

I'd recommend to keep your controller free from translation logic and translate your strings directly inside your view like this:

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

Using the provided service

Angular Translate provides the $translate service which you can use in your Controllers.

An example usage of the $translate service can be:

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

The translate service also has a method for directly translating strings without the need to handle a promise, using $translate.instant():

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

The downside with using $translate.instant() could be that the language file isn't loaded yet if you are loading it async.

Using the provided filter

This is my preferred way since I don't have to handle promises this way. The output of the filter can be directly set to a scope variable.

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

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

Using the provided directive

Since @PascalPrecht is the creator of this awesome library, I'd recommend going with his advise (see his answer below) and use the provided directive which seems to handle translations very intelligent.

The directive takes care of asynchronous execution and is also clever enough to unwatch translation ids on the scope if the translation has no dynamic values.

Community
  • 1
  • 1
Robin van Baalen
  • 3,632
  • 2
  • 21
  • 35
  • If you tried it instead of writing that unrelated comment, you wouldv'e known the answer by now. Short answer: yes. Thats possible. – Robin van Baalen Jun 04 '15 at 17:44
  • 1
    in your exemple with the filter in the controller : like with instant(), if the language file is not loaded, this will not work right? Shouldnt we use a watch in that case? Or you mean to say 'use the filter only if you know the translations are loaded? – Bombinosh Jun 15 '15 at 22:22
  • @Bombinosh I'd say use filter method if you know translations are loaded. Personally I would even recommend not loading translations dynamically if you don't have to. It's a mandatory part of your application, so you better don't want the user to be waiting for it. But that's a personal opinion. – Robin van Baalen Jul 07 '15 at 16:49
  • The point of translations is that they can change on user preferences or even on user action. So you need, in general, to load them dynamically. At least if the number of strings to translate is important, and / or if you have lot of translations. – PhiLho Oct 07 '15 at 10:01
  • 4
    When the translation is done in the HTML the digest cycle is run twice, but only run once in the controller. 99% of cases this probably won't matter, but I had an issue with terrible performance in an angular ui grid with translations in many cells. An edge case for sure, just something to be aware of – tykowale Feb 12 '16 at 20:22
124

Actually, you should use the translate directive for such stuff instead.

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

The directive takes care of asynchronous execution and is also clever enough to unwatch translation ids on the scope if the translation has no dynamic values.

However, if there's no way around and you really have to use $translate service in the controller, you should wrap the call in a $translateChangeSuccess event using $rootScope in combination with $translate.instant() like this:

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

So why $rootScope and not $scope? The reason for that is, that in angular-translate's events are $emited on $rootScope rather than $broadcasted on $scope because we don't need to broadcast through the entire scope hierarchy.

Why $translate.instant() and not just async $translate()? When $translateChangeSuccess event is fired, it is sure that the needed translation data is there and no asynchronous execution is happening (for example asynchronous loader execution), therefore we can just use $translate.instant() which is synchronous and just assumes that translations are available.

Since version 2.8.0 there is also $translate.onReady(), which returns a promise that is resolved as soon as translations are ready. See the changelog.

Dónal
  • 185,044
  • 174
  • 569
  • 824
Pascal Precht
  • 8,803
  • 7
  • 41
  • 53
  • Could there be any performance issues if I use translate directive instead of filter? Also I believe internally, it watches return value of instant(). So does it remove watches when the current scope is destroyed? – Nilesh Jun 11 '14 at 18:44
  • I tried using your suggestion but it doesn't work when scope variable's value changes dynamically. – Nilesh Jun 11 '14 at 23:06
  • 10
    Actually it's always better to avoid filters where possible, since they slow down your app because they always set up new watches. The directive however, goes a bit further. It checks if it has to watch the value of a translation id or not. That lets perform your app better. Could you make a plunk and link me to it, so I can take a further look? – Pascal Precht Jun 12 '14 at 06:52
  • Plunk: http://plnkr.co/edit/j53xL1EdJ6bT20ldlhxr Probably in my example, directive is deciding to not to watch value. Also as a separate issue, my custom error handler gets called if key is not found, but it doesn't display the returned string. I will make another plunk for it. – Nilesh Jun 12 '14 at 18:05
  • Here is a plunk that shows issue with custom error handler: http://plnkr.co/edit/OaRwyI08cLFnpWKKP4hV?p=preview I saw that similar issue with instant translation was fixed recently so this might be related that.. and let me say that I love angular-translate & especially how well documented it is. Great work!! – Nilesh Jun 12 '14 at 18:39
  • Using the $translate directive creates a new scope, can't be used with other directives that create a new scope, and makes ng-click not work correctly (at least for strings that are not translated) – wiherek Aug 10 '14 at 08:42
  • this pattern is not helpful when u have dyanmic values.http://stackoverflow.com/questions/25743715/how-to-include-angular-translator-in-better-way – praveenpds Sep 11 '14 at 09:57
  • 2
    @PascalPrecht Just a question, is it a good practice to use bind-once with translation? Like this `{{::'HELLO_WORLD | translate}}'`. – Zunair Zubair Aug 18 '15 at 07:59
  • @PascalPrecht The event $translateChangeSuccess is not fired on app start. It is only fired when translation lang is changed. Does $translate.onReady() could be used as a better mechanism to wait for correctly loaded translation? – Max Barnas Sep 22 '15 at 05:39
  • I use dynamic values in nested `ng-repeat`s and `ng-if`s. I was able to get the problem field working in Firefox (where I had my original bug) by implementing the `$rootScope` watcher, but I couldn't get the .instant() function to work. – Kraken Aug 24 '16 at 12:33
71

EDIT: Please see the answer from PascalPrecht (the author of angular-translate) for a better solution.


The asynchronous nature of the loading causes the problem. You see, with {{ pageTitle | translate }}, Angular will watch the expression; when the localization data is loaded, the value of the expression changes and the screen is updated.

So, you can do that yourself:

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

However, this will run the watched expression on every digest cycle. This is suboptimal and may or may not cause a visible performance degradation. Anyway it is what Angular does, so it cant be that bad...

Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90
  • Thank you! I would expect that using a filter in the View or in a Controller would behave exactly the same. That doesn't seem to be the case here. – ndequeker Dec 12 '13 at 13:20
  • I'd say using a `$scope.$watch` is rather overkill since Angular Translate is offering a Service to be used in the controllers. See my answer below. – Robin van Baalen Jun 20 '14 at 13:12
  • 1
    The Angular Translate filter is not required, since `$translate.instant()` offers the same as a service. Beside this, please pay attention to Pascal's answer. – knalli Oct 28 '14 at 09:05
  • I agree, using $watch is overkill. Below answers are more proper usage. – jpblancoder Nov 13 '14 at 21:58
5

To make a translation in the controller you could use $translate service:

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

That statement only does the translation on controller activation but it doesn't detect the runtime change in language. In order to achieve that behavior, you could listen the $rootScope event: $translateChangeSuccess and do the same translation there:

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

Of course, you could encapsulate the $translateservice in a method and call it in the controller and in the $translateChangeSucesslistener.

John Cardozo
  • 743
  • 7
  • 10
1

What is happening is that Angular-translate is watching the expression with an event-based system, and just as in any other case of binding or two-way binding, an event is fired when the data is retrieved, and the value changed, which obviously doesn't work for translation. Translation data, unlike other dynamic data on the page, must, of course, show up immediately to the user. It can't pop in after the page loads.

Even if you can successfully debug this issue, the bigger problem is that the development work involved is huge. A developer has to manually extract every string on the site, put it in a .json file, manually reference it by string code (ie 'pageTitle' in this case). Most commercial sites have thousands of strings for which this needs to happen. And that is just the beginning. You now need a system of keeping the translations in synch when the underlying text changes in some of them, a system for sending the translation files out to the various translators, of reintegrating them into the build, of redeploying the site so the translators can see their changes in context, and on and on.

Also, as this is a 'binding', event-based system, an event is being fired for every single string on the page, which not only is a slower way to transform the page but can slow down all the actions on the page, if you start adding large numbers of events to it.

Anyway, using a post-processing translation platform makes more sense to me. Using GlobalizeIt for example, a translator can just go to a page on the site and start editing the text directly on the page for their language, and that's it: https://www.globalizeit.com/HowItWorks. No programming needed (though it can be programmatically extensible), it integrates easily with Angular: https://www.globalizeit.com/Translate/Angular, the transformation of the page happens in one go, and it always displays the translated text with the initial render of the page.

Full disclosure: I'm a co-founder :)

Jeff W
  • 11
  • 2