Skip to content

Instantly share code, notes, and snippets.

@cvbuelow
Last active Aug 29, 2015
Embed
What would you like to do?
i18n in Angular

Internationalization

The i18n library is an AngularJS module composed of services and filters used to easily internationalize applications.


Configuring

setLocale(locale)

To configure the locale of your application, all you need to do is inject the TranslateProvider into your Angular config block and pass it a String that indicates the desired locale.

angular.module('myMod', ['i18n'])
  .config(function (TranslateProvider) {
    TranslateProvider.setLocale('en-US');
  });

loadMap(file, [namespace])

The problem this library aims to solve is easily translating data into different languages, so it is left up to you (the developer) to come up with the actual translations. This library is opinionated in the fact that it expects you to supply them in JSON format

e.g.

{
  "btn_cancel": "Cancel",
  "btn_retry": "Retry"
}

It is also opinionated in where it expects to find these translation files. By default it uses XMLHttpRequest and expects there to be a data folder in the root of your project. It then expects a share folder inside of the data folder

Inside of the share folder, the library then expects a folder for each locale you wish to support. Each folder should be named after its respective locale.

data
  | - share
       | - en-US
             | - labels.json
       | - de-DE
             | - labels.json

Now that we have our dictionaries for our different locales, now we need to load one of them based on the current locale.

angular.module('myMod', ['i18n'])
  .config(function (TranslateProvider) {
    TranslateProvider.setLocale('en-US');
    TranslateProvider.loadMap('labels.json');
  });

The file path is created based on the locale, so setting the locale to 'en-US' will result in the library loading data/share/en-US/labels.json

You can load in as many JSON files as you like, the common use case for this is when dealing with shared bower components that require translations.

If you're concerned that a component might have conflicting key in your dictionary, you can also specify a namespace to act as a prefix for a particular file

TranslateProvider.loadMap('labels.json', 'labels');

So in the above example btn_cancel then becomes labels.btn_cancel


It's also possible to pass in an object of files/namespaces if you need to pull in multiple dictionaries at the same time.

Translate.provider.loadMap({
  'appshop.json': null, // no namespace for this file
  'labels.json', : 'labels'
});

Translate Service

The translate service comes with two primary methods of translating data

get(key, [data])

The get method is just a getter to your previously loaded translated dictionaries. The first argument it expects is the key name within your JSON file

$scope.label = Translate.get('btn_cancel');

This is fine for simple translations, but what happens when we need to embed dynamic data inside of one of our translations? That's easily achieved by passing in a second argument specifying the data to be bound.

Pretend that the following data was loaded into our translation dictionary

{
  "greeting": "Hello, {{name}}!"
}

We could then inject dynamic data into this by simply passing an object in as our second argument.

$scope.greeting = Translate.get('greeting', {
  name: 'Dave'
});

This would then yield 'Hello, Dave!'


read(file, [data])

The read method is for digesting files that are not loaded into the dictionary

  • this is better suited for large chunks of HTML/Text that you don't want to stick in a translation dictionary.

data/share/en-US/lorem.html

<div>
  Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
  tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
  nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
  eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
  in culpa qui officia deserunt mollit anim id est laborum.
</div>
$scope.lorem = Translate.read('lorem.html');

Just like reading translation dictionaries, the library assumes that your file lives in the data/share directory.

It is also possible to inject dynamic data into a file using Angular expressions in your template and passing in an object to the read method as a second argument

data/share/en-US/lorem-dynamic.html

<div>
  Lorem ipsum dolor sit amet {{name}}, sed do eiusmod tempor incididunt ut labore
  et dolore magna aliqua, {{day}}.
</div>
$scope.loremDynamic = Translate.read('lorem-dynamic.html', {
  name: 'Beemo',
  day: 'Saturday'
});

Translate Filters

Sometimes injecting a service and binding data to the scope seems a bit tedious just to translate some text. For that reason there are filters available for both the get() and read() methods.

translate

The translate filter simply retrieves an item from the translation dictionary

<button>{{ 'btn_retry' | translate }}</button>

It is also possible to pass an object to the translate filter in a similar fashion to the Translate.get() method for dynamic data

<div>{{ 'greeting' | translate:{ name: 'Dave' } }}</div>

translateFile

If you wish to just dump out an entire file in a similar fashion to Translate.read(), you can alternatively use the translateFile filter.

<section>{{ 'lorem.html' | translateFile }}</section>

And just like its service counterpart, you can also pass in data to be interpolated.

<section>{{ 'lorem-dynamic.html' | translateFile:{ name: 'Beemo' } }}</section>

Integrating with Karma

Ideally you would not need to include the actual Translate library in your unit tests, however if any of your tests are dependent on live text, you may run into some problems.

1. Update your karma.conf.js file to include your dictionaries as fixtures via the file array.

files: [

  {
    pattern: 'data/share/l10n/**/*.json',
    watched: true,
    included: false,
    served: true
  }

]

2. Because Karma doesn't actually spin up a web server for our code, we need to proxy any requests for dictionaries to the data folder. This is also done in the karma.conf.js file.

proxies: {
  '/data': 'http://127.0.0.1:9876/base/data'
}

3. The last step is to run/configure the module within our unit tests. This is done in your actual spec files

// global module declaration
angular.module('mock.i18n', ['i18n'])
  config(function (TranslateProvider) {
    TranslateProvider.loadMap('labels.json');
  });


// later on down the line when we're initializing our module for testing..
describe('AppModule', function () {

  beforeEach(module('AppModule', 'mock.i18n'));

  // assertions/tests/etc..

});
angular.module('i18n', [])
/**
* Provider/Service for Translations
*
* The provider can be used to set the locale and load additional
* JSON files containing translations within config() blocks
*
* The service can then be used to access the translations hash or to read
* different files that are not to be stored in the translations hash
*/
.provider('Translate', function () {
var DEFAULT_LOCALE = 'en-US';
// current desired language..
var locale = DEFAULT_LOCALE;
// files to load
var queue = [];
// map of translations
var translations = {};
/**
* Generates path to locale file
* @param {string} locale - en-US
* @param {string} file - labels.json
* @return {string} actual path
*/
function getLocalePath (loc, file) {
return ['data', 'share', 'l10n', loc, file].join('/');
}
/**
* Attempts to use the file manager to read a localized file
* @param {string} file - labels.json
* @param {boolean} let's us know if we're coming from a failed request
* @return {string} content
*/
function readFile (file, isRetry) {
var fileLocale = !isRetry ? locale : DEFAULT_LOCALE;
var filePath = getLocalePath(fileLocale, file);
var xhr = new XMLHttpRequest();
var hasGoodStatus;
xhr.open('GET', filePath, false);
xhr.send();
hasGoodStatus = xhr.status === 200 || xhr.status === 0;
if ((!hasGoodStatus || !xhr.responseText.length) && !isRetry) {
return readFile(file, true);
}
return xhr.responseText;
}
/**
* Runs through queue of dictionaries to load and
* loads them
*/
function digest () {
angular.forEach(queue, function (map, i) {
var content = JSON.parse(readFile(map.file) || '{}');
if (!map.namespace) {
angular.extend(translations, content);
return;
}
angular.forEach(content, function (value, key) {
var newKey = [map.namespace, key].join('.');
translations[newKey] = value;
});
});
}
/**
* Used to extend the translations map
* @param {string|object} file - labels.json
* @param {string} namespace - optional
*/
this.loadMap = function (file, namespace) {
if (angular.isObject(file)) {
angular.forEach(file, function (n, f) {
this.loadMap(f, n);
}, this);
} else {
queue.push({
file: file,
namespace: namespace
});
}
};
/**
* Sets the current locale and then attempts to load
* that locales label file
* @param {string} locale - en-US
*/
this.setLocale = function (loc) {
locale = loc || DEFAULT_LOCALE;
};
/**
* This is the actual injectable service
* @return {object} Translate
*/
this.$get = ['$interpolate', function ($interpolate) {
// load all the dictionaries
digest();
/**
* Attemps to return an item from the translations hash
* @param {string} key
* @param {object} data
* @return {string} translation
*/
function getTranslation (key, data) {
var translation = translations[key] || '';
if (data) {
return $interpolate(translation)(data);
}
return translation;
}
/**
* Attempts to readfile and if any data needs to be bound,
* injects the dynamic data into the content
* @param {string} file path
* @param {object} data
* @return {string} compiled data
*/
function readFileWithData (file, data) {
var content = readFile(file);
if (data) {
return $interpolate(content)(data);
}
return content;
}
function getLocale () {
return locale;
}
return {
get: getTranslation,
read: readFileWithData,
getLocale: getLocale
};
}];
});
/**
* This filter provides a simple way to read an entire file from the
* translations directory and inject it into a template
*
* e.g.
* <div>{{'terms.html' | translateFile}}</div>
*/
angular.module('i18n')
.filter('translateFile', function (Translate) {
return Translate.read;
});
/**
* This filter provides a simple way to pull items from the translations
* hash from within an angular template
*
* e.g.
* <div>{{'translation_key' | translate}}</div>
*/
angular.module('i18n')
.filter('translate', function (Translate) {
return Translate.get;
});
angular.module("ihu-common.i18n",[]).provider("Translate",function(){function a(a,b){return["data","share","l10n",a,b].join("/")}function b(c,f){var g,h=f?d:e,i=a(h,c),j=new XMLHttpRequest;return j.open("GET",i,!1),j.send(),g=200===j.status||0===j.status,g&&j.responseText.length||f?j.responseText:b(c,!0)}function c(){angular.forEach(f,function(a){var c=JSON.parse(b(a.file)||"{}");return a.namespace?void angular.forEach(c,function(b,c){var d=[a.namespace,c].join(".");g[d]=b}):void angular.extend(g,c)})}var d="en-US",e=d,f=[],g={};this.loadMap=function(a,b){angular.isObject(a)?angular.forEach(a,function(a,b){this.loadMap(b,a)},this):f.push({file:a,namespace:b})},this.setLocale=function(a){e=a||d},this.$get=["$interpolate",function(a){function d(b,c){var d=g[b]||"";return c?a(d)(c):d}function f(c,d){var e=b(c);return d?a(e)(d):e}function h(){return e}return c(),{get:d,read:f,getLocale:h}}]}),angular.module("ihu-common.i18n").filter("translateFile",["Translate",function(a){return a.read}]),angular.module("ihu-common.i18n").filter("translate",["Translate",function(a){return a.get}]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment