Many of you are likely familiar with Pascal Precht’s i18n localization library, angular-translate. It’s well made, easy to use, and has a lot of configuration as to how you want to prepare or load your localizations. The only thing it lacks, in my and others opinions, is one-time binding (issues #738, #967, #1018, and #1043). This becomes a problem if you asynchronously load your localization files, which is a beneficial tool to large angular applications. Pascal writes about how to asynchronously load your localization files. We personally use angular-translate-loader-static-files. While the cost of evaluating translation expressions is relatively inexpensive, we still preferred throwing a one-time binding on it, and since we were early on in the project, I figured we’d provide a solution that could also benefit others in the community.

translate-once Directive

translate-once adds support for one-time bindings for translations with a new translate-once</a> directive. It extends the existing functionality of angular-translate and doesn’t introduce any new dependencies. It is written to be considered an extension within the same namespace as pascalprecht.translate.

<span translate-once="TRANSLATION_KEY"></span>

Installation is done via bower, and if you already include the pascalprecht.translate module into your angular project, you’ll be good to go.

bower install angular-translate-once

Why do we need this?

There’s no good way to perform one-time bindings when asynchronously loading your static localization assets. One-time bindings for translations are useful for any instances of static copy.

  • Page titles
  • Navigation elements
  • Static bylines, paragraphs, explainers, etc

Let’s look at what is currently available in the angular-translate package.

Use it as a filter

TRANSLATION_KEY

Use it as a directive

<span translate="TRANSLATION_KEY"></span>

Use it as a **directive with values **

<span translate="TRANSLATION_KEY" translate-values="{foo: 'bar'}"></span>

Use it as a directive and compile elements formed from the translation

<span translate="TRANSLATION_KEY" translate-compile></span>

Or even use it in within javascript in one of two ways:

Asynchronously

$translate('TRANSLATION_KEY').then(function (translation) {
  alert(translation);
});

Synchronously

alert($translate.instant('TRANSLATION_KEY'));

But what about one-time binding?

Let’s first look at some approaches one might make with the current toolkit. Intuitively, one might try to do this in how a standard one-time binding would work after Angular 1.3 introduced one-time bindings.

<span ng-bind=""></span>

Your intuition would lead you to think that the output from this would be a span tag with the translation inside and question why I’m even here writing this post. Unfortunately, that is not the case if you asynchronously load your localization files, as many large applications do. You may be safe after your application has completed any deferred asset loading, but before then, your first page rendered will likely be missing all its one-time bound translations.

First, let’s understand how the filter works. The translate filter’s definition makes use of the synchronous lookup function, $translate.instant, as a filter is by design synchronous. That means $translate.instant is a hit-or-miss lookup, where if the localization is not loaded, it misses and does not return the translation since it doesn’t exist. When you use the filter in a binding, it’s going to process your string through $translate.instant. This works without one-time bindings because your expression is reevaluated each digest cycle, and if there’s a change, it triggers the watchers to re-render the output. If $translate.instant('TRANSLATION_KEY') misses the first time, but hits a successive time, that value will have changed and the new value will be rendered to the view.

When you introduce a one-time binding to the expression, your binding will only exist in the $$watchers once, and then you’re done. That means you only get one chance to retrieve a data value to bind, and any successive digest loops will not trigger your update if that value changes (in this case, once the localization becomes loaded and returns the final translation value). So if $translate.instant misses the first time, that’s the final value of your binding. You don’t get a second chance to lookup the localization entry again to re render the correct value. It’s dependent on the digest cycle.

How translate-once works

translate-once makes use of the link function and the asynchronous resolver of $translate(). The directive’s link function takes the translation key, looks it up asynchronously with $translate(), and once resolved, writes it to the element. Since the link function only fires once, when the element enters context, it is essentially one-time binding the translation. Of course, if it leaves and re-enters context, perhaps with an ngIf condition going from false and back to true.

Let’s look at how this works:

function linker (scope, element, attrs) {
  var translateValues = {};
  if (attrs.translateValues) {
    translateValues = $parse(attrs.translateValues)(scope);
  }

  $translate(attrs.translateOnce, translateValues).then(function (translation) {
    element.html(translation);
    if (attrs.hasOwnProperty('translateCompile')) {
      $compile(element.contents())(scope);
    }
  });
}

The first thing that happens is a backward compatible step to ensure we expose existing functionality that angular-translate offers – passing translate-values to be used in dynamic localization entries.

if (attrs.translateValues) {
  translateValues = $parse(attrs.translateValues)(scope);
}

We take the translate-values attributes, and $parse it on the shared scope. Note: the scope is not isolated, it is shared with the context the directive exists in, such that when we parse the values, they are parsed in the scope that the expression exists in.

The second thing that happens is calling $translate(). This asynchronously looks up the localization entry, and once it is available, it resolves with the answer if the entry exists.

$translate(attrs.translateOnce, translateValues).then(function .. );

We then take the translation value, and set it to the element’s content.

var output = translation;
if (attrs.hasOwnProperty('translateCompile')) {
  output = $compile(translation)(scope);
}
element.html(output);

If the consumer requests that we compile the translation value, as it may contain elements with other bindings, the attribute flag translate-compile can be provided and is used in a backward compatible manner. Then we process the translation through $compile with the shared scope.

More one-time binding tools

Just as you may want to set the content of an element to a translation value, many times you may want to do this for other attributes on an element, such as

  • An <input /> element’s value
  • An <input /> text field placeholder
  • An <a /> element’s title attribute
  • An <img /> element’s alt attribute
  • etc..

A similar process takes place for one-time binding an element’s properties. The following attribute directives can be used:

  • translate-once-value
  • translate-once-placeholder
  • translate-once-title
  • translate-once-alt

The same process takes place as when using translate-once, the only difference is that once $tranlsate() resolves, it updates the element’s corresponding attribute. So if we do translate-once-placeholder="TRANSLATION_KEY", <input placeholder="translation value" /> will be rendered in the end.

Contributing

As always, I welcome anyone to contribute a pull request over on the Github repo. Please make sure that tests are written for any changes or additions made.