1

I am using the excellent Angular Translate ($translate) directive/service to deal with multiple Locale Languages and since I have multiple locale files I use the convenient $translateProvider.useStaticFilesLoader to load my translation files through a structure of localeAbbr.json, for example en.json, es.json, etc... I built a Plunker to show my open source project and that project uses the locale through Git raw files (pointing to the actual Github repository, meaning not local to the plunker demo). My project is built as a Directive and a Service, I made a small Plunker to show my timing issue with the JSON file loading.

All that to say that it seems $translateProvider.useStaticFilesLoader works asynchronous while I would really need it to be synchronous because by the time the plunker runs, the JSON files are not yet parsed while I already called a $translate.instant() on my messages.

I have a Plunker showing the problem.

And here is part of my quick Service demo:

app.factory('validationService', ['$filter', '$translate', function ($filter, $translate) {
  var service = this;
  var validationSummary = [];
  var errorMessages = [
    'INVALID_ALPHA',
    'INVALID_ALPHA_SPACE',
    'INVALID_ALPHA_NUM',
    'INVALID_BOOLEAN'
  ];

  //var $translate = $filter('translate');

  for(var i=0, ln=errorMessages.length; i < ln; i++) {
    validationSummary.push({  
      field: i,
      message: $translate.instant(errorMessages[i])
    });
  }

  // attach public functions
  service.getValidationSummary = getValidationSummary;
  return service;

  // function declaration
  function getValidationSummary() {
    return validationSummary;
  }
}]);

The $translateProvider configuration

app.config(['$translateProvider', function ($translateProvider) {
  $translateProvider.useStaticFilesLoader({
    prefix: 'https://rawgit.com/ghiscoding/angular-validation/master/locales/validation/',
    suffix: '.json'
    });

    // load English ('en') table on startup
    $translateProvider.preferredLanguage('en').fallbackLanguage('en');
}]);

Call my Service through the Controller:

app.controller("TestController", function($scope, validationService) {
  var vm = this;
  vm.displayValidationSummary = true;

  vm.validationSummary = validationService.getValidationSummary();
});

and finally the HTML using the controller:

<div class="alert alert-danger alert-dismissable" ng-show="vm.displayValidationSummary">
  <button type="button" class="close" data-dismiss="alert" aria-hidden="true" ng-click="displayValidationSummary = false">&times;</button>
  <h4><strong>{{ 'ERRORS' | translate }}!</strong></h4>
  <ul>
      <li ng-repeat="item in vm.validationSummary">{{item.field }}: {{item.message}}</li>
  </ul>
</div>

Since I'm using AngularJS 1.3+, I also found that $translate only gets translated once, so the author suggest to use translateFilter.$stateful = true; and I tried but that doesn't seem to help.

Again here is the Plunker

I have been spending weeks on trying to find and code all kind of solution but I never got it to work and I'm really sad of seeing my raw translation code :(

Please Help!!!

EDIT
I realized that my question was not covering everything related to my problem. On top of the translation delay problem, I also have to pass extra arguments and that is a huge problem passing them to the translation anonymous function. By the time the promise is finished, the state of my arguments have already changed. For example:

$translate(validator.message).then(function(translation) {
    // only log the invalid message in the $validationSummary
    addToValidationSummary(formElmObj, translation);

    // error Display
    if(!isValid) {
      updateErrorMsg(translation, isValid);
    }else if(!!formElmObj && formElmObj.isValid) {
      addToValidationSummary(formElmObj, '');
    }
}, function(data) {
    throw 'Failed to translate' + data;
});
ghiscoding
  • 12,308
  • 6
  • 69
  • 112
  • possible duplicate of [Correct use for angular-translate in controllers](http://stackoverflow.com/questions/20540877/correct-use-for-angular-translate-in-controllers) – Dmitry Gonchar Apr 23 '15 at 15:52

3 Answers3

2

When working with AngularJS, or JavaScript for that matter you really need to embrace the asynchronous paradigm. In order to make dealing with asynchronous code less cumbersome you can employ the use of Promises. Angular gives you a service called $q which does the heavy lifting for you

https://docs.angularjs.org/api/ng/service/$q

getting ones head around Promises can take time but well worth the effort in the long run.

Essentially what you need to do with your validationService is to make use of $translate's promise api which will give you the translation you require based on the supplied key when it is in a position to do so. What this boils down to is that you ask $translate for all of the translationId's you wish to get a translation for and when all have been fetched you populate the validationSummary array with your messages.

app.factory('validationService', ['$q', '$translate', function ($q, $translate) {

  var translationsPromises = [], 
    validationSummary = [],
    errorMessages = [
      'INVALID_ALPHA',
      'INVALID_ALPHA_SPACE',
      'INVALID_ALPHA_NUM',
      'INVALID_BOOLEAN'
    ];


  angular.forEach(errorMessages, function(val, key) {
    translationsPromises.push($translate(val));
  });

  $q.all(translationsPromises)
    .then(function(translations) {
      angular.forEach(translations, function(val, key) {
        validationSummary.push({
          filed: key,
          message: val
        });
      });
    })
    .catch(function (err) {
      console.error('Failed to translate error messages for validation summary', err);  
    });

  // function declaration
  function getValidationSummary() {
    return validationSummary;
  }

  return {
    getValidationSummary: getValidationSummary
  };

}]);

I've forked your plunker and modified it to include the above sample

http://plnkr.co/edit/7DCwvY9jloXwfetKtcDA?p=preview

Another observation is that you are using the translate filter in the HTML. Please be aware that this can prove to be expensive if you have a large DOM as Angular will make the call to translate each key on every digest. An approach to consider would be to provide your vm with a labels object and use the $filter service to populate them upon controller instantiation.

Aides
  • 3,643
  • 5
  • 23
  • 39
Pat Nolan
  • 21
  • 2
  • Thanks I will take a look at it, the $filter was only there as another multiple code changes that I have tried but can't get anything to work properly. – ghiscoding Apr 10 '15 at 03:20
  • The only problem I have with this implementation is that I am doing extra manipulation on my translation, for example I have a translation named INVALID_MIN_CHAR and the text goes like "Must be at least :param characters. "... I then replace the ":param" within a for loop, by the time the promise is finish, my loop is already done and the params are undefined, unless I pass them as argument to the anonymous function inside the promise, but I can't seem to find how to do that on the $translate promise. – ghiscoding Apr 10 '15 at 21:46
  • Why don't you make use of Angular's built in interpolation? The :param in your translation file can be replaced with say {{vm.minChars}} and expose that value on the vm. Obviously you have more work to do if you are going to have multiple varying minimum length messages on the form. With the implementation provided you have access to each translation in the angular.forEach(translations, function(val, key) { validationSummary.push({ filed: key, message: val }); where you can process the :param should you continue with that particular implementation. – Pat Nolan Apr 11 '15 at 06:03
  • Yes I found that actually the interpolation is also handled by $translate as well, you pass the translation as a 2nd argument like so `$translate('INVALID_MIN_CHAR', { value: 5 });` but even though it's completely breaking all of my code, I based all of my code on the assumption that I have the text available on the spot, going with promise is required a load of recoding on my side, which is quite sad. I would rather use a solution that run my code only after the JSON file is loaded, unfortunately `$translateProvider.useStaticFilesLoader` does not return a promise. – ghiscoding Apr 11 '15 at 19:17
1

I found out the answer to my problem of passing extra arguments to the anonymous function of the promise is to use Closures, in this way the variables are the same before the promise and inside it too. So I basically have to wrap my $translate call into the closure, something like the following:

(function(formElmObj, isValid, validator) {
    $translate(validator.message).then(function(translation) {
        message = message.trim();

        // only log the invalid message in the $validationSummary
        addToValidationSummary(formElmObj, message);

        // error Display
        if(!isValid) {
          updateErrorMsg(message, isValid);
        }else if(!!formElmObj && formElmObj.isValid) {
          addToValidationSummary(formElmObj, '');
        }
    }, function(data) {
        throw 'Failed to translate' + data;
    });
})(formElmObj, isValid, validator);

and now finally, my variables are correct and keep the value at that point in time :)

ghiscoding
  • 12,308
  • 6
  • 69
  • 112
0

While it is true that $translateProvider.useStaticFilesLoader does not return a promise, I looked inside the $translate service and found that it provides a handy callback onReady() which does return a promise. This callback is invoked when the $translate service has finished loading the currently selected language, and is useful for making sure that instant translations will work as expected after page initialization:

$translate.onReady(function () {
    // perform your instant translations here
    var translatedMsg = $translate.instant('INVALID_ALPHA');
});