Decorators and i18n

Decorators are quite useful in Angular.
If you are not familiar with their usage, let’s say that they allow you to extend or overwrite a service (or directive or filter) in specific parts of your application without modifying the original one.

One of the advantages is that you don’t have to change too much of the implementation or your in-page code, but every part of your application will use the correct (decorated or original) version of every component.

If you need more information about decorators in Angular read the official documentation here

If you need more information about the decorator pattern in general you can find something here

i18n is the internationalization module provided by Angular: it is really useful for multi language applications as it offers an easy management not only for your labels, but calendar and currency as well.

Even if there is not a strict relation between Decorators and i18n, it’s quite possible that in multi language applications you need not only different translations, but custom rules depending on the language/country and in that situation the decorators come in handy.

Assuming you have a <home> directive it is possible that the home can be slightly different depending on the language.

One thing that you don’t want is to add in your home directive logic a sequence of if-else depending on the country nor add logic to add <home_en-gb> or <home_it-it> programmatically in your page.

The ideal scenario is that depending on the i18n file you include in the page, the <home> directive will use the default version or the custom if it exists.

So assuming that the default language is British English, if you have in your page

  <html>
    <head>
       <script src="/angular/angular.min.js"></script>
       <script src="/javascripts/i18n/angular-locale_en-gb.js"></script>
       <script type="text/javascript" src="/javascripts/build/decorators-angular.min.js"></script>
    </head>
    <body>
       <home></home>
    </body>
  </html>

the application should use the original version. In case you have something like this

  <html>
     <head>
       <script src="/angular/angular.min.js"></script>
       <script src="/javascripts/i18n/angular-locale_it-it.js"></script>
       <script type="text/javascript" src="/javascripts/build/decorators-angular.min.js"></script>
     </head>
     <body>
        <home></home>
     </body>
  </html>

if a home_it-it directive has been defined it should be automatically used otherwise the default one.

Based on these premises we can’t manually declare decorators because it would mean that for every directive/service/filter that would have a decorator we should write code, so something smarter has to take place.

But let’s start writing a manual decorator that uses the i18n information.

The Decorators App

Our application will be called “decorators”.
Our home directive will be something simple.

angular.module('decorators')
    .directive('home', [function () {
        return {
            restrict: 'E',
            replace: true,
            templateUrl: '../public/javascripts/src/templates/homeDirective.html'
        }
    }]);

 

<div class="home">
    HOME!</div>

We create a simple alternative for the Italian page

angular.module('decorators')
    .directive('home_it-it', [function () {
        return {
            restrict: 'E',
            replace: true,
            templateUrl: '../public/javascripts/src/templates/homeDirective_it-it.html'
        }
    }]);
<div class="home">
    HOME ITA</div>

Then we add the decorator logic.

angular.module('decorators', [])
   .config(['$provide', function($provide) {
      $provide.decorator('homeDirective', ['$delegate', '$locale', '$injector'
         , function ($delegate, $locale, $injector) {
             if ($injector.has('home_' + $locale.id + 'Directive')) {
                return $injector.get('home_' + $locale.id + 'Directive');
             }
             return $delegate;
           }
      ]);
   }]);

That means: if you find any directive registered as ‘home_’ + $locale.id (e.g. it-it) return it, otherwise return the original one ($delegate).

Even if you are used to Angular, this code can seem a bit funny.
We have created a decorator for “homeDirective” even if our directives never use the suffix “Directive” in their declaration.

We need to do that way given how the injector registers directives.
Basically for the directives the word “Directive” is appended by Angular and this is the reason we have to decorate “homeDirective”; for the same reason we have to check if the injector has any “home_” + $local.id + “Directive” before trying to return it.

While directives and filters are registered by angular appending “Directive” and “Filter”, if you want to decorate services you don’t have to append anything.

Given this code if in our page we import angular-locale_de-de.js for the German users, if we don’t have differences in the logic and we don’t create a home_de-de directive, the default one (en-gb) will be used.

If you are used to server side languages/frameworks that use  a “convention over configuration” approach, please keep in mind that Angular doesn’t scan your directories for registering the dependencies.

The registration system looks at the name of the directive/service/filter, not at the file name.

So if you have home.js and home_it-it.js please remember that in the home_it-it.js file the name of the directive has to be “home_it-it” as well. If you keep in both files the name “home” angular will throw an exception.

The code works pretty well, the only problem is that, as stated at the beginning, we should duplicate it for every directive/service/filter specifying manually what we want to decorate.

So I’m going to make it generic.

...
...
.config(['$provide', function($provide) {
  createDecorators(['decorators']);

  function createDecorators(modules) {
    var decorators = [];
    modules.forEach(function (module) {
        var _module = angular.module(module);
        _module._invokeQueue.forEach(function (service) {
            var serviceName = (service[2][0].indexOf('_') !== -1) ? service[2][0].split('_')[0] : service[2][0];
            if (service[2][0].indexOf('_') !== -1) {
                if (decorators.indexOf(serviceName) === -1) {
                    if (service[1] == 'directive') {
                        serviceName += 'Directive';
                    }
                    decorators.push(serviceName);
                }
            }
        });
        decorators.forEach(function (decorator) {
            $provide.decorator(decorator, ['$delegate', '$locale', '$injector'
                , function ($delegate, $locale, $injector) {
                    if (decorator.indexOf('Directive') !== -1) {
                        decorator = decorator.replace('Directive', '');
                        decorator = decorator + '_' + $locale.id + 'Directive';
                    } else {
                        decorator += '_' + $locale.id;
                    }
                    if ($injector.has(decorator)) {
                        return $injector.get(decorator);
                    }
                    return $delegate;
                }
            ]);
        });
    });
  }
...
...

The createDecorators function accepts an array of strings where every item is a module’s name.
For each module it scans all the registered services/directives and avoiding duplication it looks if there is anything that has to be decorated, given the naming convention nameOfDirective_{{$locale.id}} (e.g. en-gb) and the i18n file included in the page.

I made the code above working for directives and services, but not for filters.
It won’t crash if you create filters based on the convention we made up, but it won’t decorate.

Another way to improve the code is to extract the function and put it in an Angular provider so the config is cleaner and we can reuse it easily in other configs if needed.

So I move the function into another file and create a provider version of it.

angular.module('decorators')
    .provider('decoratorSetup', ['$provide'
        , function ($provide) {
            var _self = this;
            _self.createDecorators = function (modules) {
                var decorators = [];
                modules.forEach(function (module) {
                    var _module = angular.module(module);
                    _module._invokeQueue.forEach(function (service) {
                        var serviceName = (service[2][0].indexOf('_') !== -1) ? service[2][0].split('_')[0] : service[2][0];
                        if (service[2][0].indexOf('_') !== -1) {
                            if (decorators.indexOf(serviceName) === -1) {
                                if (service[1] == 'directive') {
                                    serviceName += 'Directive';
                                }
                                decorators.push(serviceName);
                            }
                        }
                    });
                    decorators.forEach(function (decorator) {
                        $provide.decorator(decorator, ['$delegate', '$locale', '$injector'
                            , function ($delegate, $locale, $injector) {
                                if (decorator.indexOf('Directive') !== -1) {
                                    decorator = decorator.replace('Directive', '');
                                    decorator = decorator + '_' + $locale.id + 'Directive';
                                } else {
                                    decorator += '_' + $locale.id;
                                }
                                if ($injector.has(decorator)) {
                                    return $injector.get(decorator);
                                }
                                return $delegate;
                            }
                        ]);
                    });
                });
            };

            this.$get = [function () {

            }];
        }]);

Then the config can be simplified like this

angular.module('decorators', ['templates-decorators'])
    .config(['decoratorSetupProvider', function (decoratorSetupProvider) {
        decoratorSetupProvider.createDecorators(['decorators']);
    }]);

What about testing?

this approach doesn’t make testing your directives/services/filters more complicated, you can test any of them independently with the usual approach you use.

The decoratorSetupProvider itself can be tested as usual mocking the dependencies.

One more thing about decorating Directives

To run this example decorating directives was an easy choice, but in a real scenario, honestly, I won’t do that.

Why?

Well, for me a directive shouldn’t have too much logic and it should use as much services as possible for the heavy job.
A directive is responsible for rendering and display information and I expect that by country the differences are in the template more than in the link function.

Based on these premises there are more elegant and better solutions to load a template dynamically, even based on i18n if you like.
Overriding the whole directive just to change a template is totally overkill.

I can’t exclude that in some scenarios decorating a directive is inevitable, but till now those scenarios never happened to me and I’ve happily decorated only my services.

So…

The whole solution is far from be perfect, but it is a good start to play with the concept.
For instance the point about decorating is not necessarily to substitute the whole component, but this code works only in this way so it’s completely missing the logic to just extend something.

In my repository on github I decorated services as well to give you a more exhaustive example.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s