AngularJS Localization done right.
A localization module for AngularJS complete with core service and accompanying filter, directives etc.
It is based on a number of angularjs localization modules already available out there on the web, and borrows heavily from the following list including but not limited to:
- http://dailyjs.com/2014/04/08/angular-localize/
- https://github.com/blueimp/angular-localize
- http://alicoding.com/how-to-localized-angularjs-app/
It was inspired by Jim Lavin's AngularJS Resource Localization Service who made an excellent first tutorial for his original version at Coding Smackdown TV which was later updated to include performance improvements seen here.
- Simplified format of the translation file.
- Support for parameterized messages.
- Parameters in the directive may be bound to
$scope
variables from the nearest parent controller. - HTML element tag attributes can also be natively localized.
- Ability to configure a whole slew of things for more customizability to play nicely with your application.
- node.js >= v0.10.x
- npm
- grunt CLI
npm install -g grunt-cli
- bower
npm install -g bower
$ npm install -d
$ npm test
In lieu of a formal style guide, please take good care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Grunt. Please refer to this [document][commit-message-format] for a detailed explanation of git commit guidelines - source: AngularJS [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
The easiest way to install the ngLocalize
module is via Bower:
bower install angular-localization --save
Two other options are available:
- Download the latest release.
- Clone the repo:
git clone https://github.com/doshprompt/angular-localize.git
.
You can then include angular-localization
after its dependencies, angular and angular-cookies:
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<script src="bower_components/angular-localization/angular-localization.js"></script>
- Include the required libraries
- Ensure that you inject
ngLocalize
into your app by adding it to the dependency list.
angular.module('myApp', ['ngLocalize']);
All overridable configuration options are part of localeConf
within the ngLocalize.Config
module that comes bundled along with this plugin and works alongside ngLocalize
the folder off the root of your web app where the resource files are located (it can also be used as a relative path)
the locale that your app is initialized with for a new user
commonly occuring words, phrases, strings etc. are stored in this file (it is used to check whether the service itself is ready or not as it is loaded during bootstrap)
the extension for all resource files spanning across all languages
whether to save the selected language to cookies for retrieval on later uses of the app
works in conjuntion with
persistSelection
and provides the cookie name for storage
a regular expression which is used to match which custom data attibutes may be watched by the
i18n
directive for 2-way bindings to replace in a tokenized string
the delimiter to be used when passing params along with the string token to the service, filter etc.
angular.module('myApp', [
'ngLocalize',
'ngLocalize.Config'
]).value('localeConf', {
basePath: 'languages',
defaultLocale: 'en-US',
sharedDictionary: 'common',
fileExtension: '.lang.json',
persistSelection: true,
cookieName: 'COOKIE_LOCALE_LANG',
observableAttrs: new RegExp('^data-(?!ng-|i18n)'),
delimiter: '::'
});
NOTE: There is one caveat; if you want to override at least one particular option, then you must explicitly set any other options explicitly to their default values (shown above).
Publicly exposed events may be found as part of localeEvents
situated within the ngLocalize.Events
module that comes pre-loaded along with this plugin and is leveraged internally by the service.
localeEvents.resourceUpdates
: when a new language file is pulled into memorylocaleEvents.localeChanges
: when the user chooses a different locale
angular.module('myApp', [
'ngLocalize',
'ngLocalize.Events'
]).controller('myAppControl', ['$scope', 'localeEvents',
function ($scope, localeEvents) {
$scope.$on(localeEvents.resourceUpdates, function () {
// do something
});
$scope.$on(localeEvents.localeChanges, function (event, data) {
console.log('new locale chosen: ' + data);
});
}
]);
NOTE: All of these are sent by $rootScope
so that they may be accessible to all other $scopes
within your app.
Otherwise you could alternatively always choose to inject $rootScope
and consume (ingest) the aforementioned events.
This plugin relies on the developer(s) to let it know of all languages currently part of the codebase.
These languages must be defined as per the [Language Culture Name specification] (http://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx).
It is broken up into two parts, both of which are located in the ngLocalize.InstalledLanguages
modules separated out as localeSupported
and localeFallbacks
.
The first is responsible for the list of languages currently supported as the name suggests while the latter takes care of fallbacks when a particular language is present but not for the region from which the app is being opened (e.g. en-GB).
As a last resort, if none of these are valid and/or available, it will fallback to the defaultLocale configured as part of the options during initialization.
angular.module('myApp', [
'ngLocalize',
'ngLocalize.InstalledLanguages'
])
.value('localeSupported', [
'en-US',
'fr-FR',
])
.value('localeFallbacks', {
'en': 'en-US',
'fr': 'fr-FR'
});
NOTE: Since the plugin does not rely on auto-discovery mechanisms of any kind, and none of the sort is planned for the future, it is possible to start creating language resource files before they must be fully integrated. Simply leaving them out of the declaration will suffice meaning it will cause them to be ignored when deciding on which language to show up on the page(s).
Each localization file is pretty simple. It consists of a flat JSON object:
{
"helloWorld": "Hello World",
"yes": "Yes",
"no": "No"
}
The key is used to look up the localized string, the value will be returned from the lookup.
As mentioned earlier, this plugin is able to handle substitutions of tokens based on arguments passed to it along with the token, provided that the token contains some indication of where it is to be placed within that said token. There are a few ways of doing this:
{
"helloWorld": "Hello %name"
}
{
"helloWorld": "Hello {1}",
}
{
"helloWorld": "Hello {firstName} {lastName}"
}
{
"helloWorld": "Hello %1 %2"
}
Please take note of the fact that multiple ordered params may also be given to it.
The localization key can be defined as the value of the i18n
attribute:
<any i18n="common.helloWorld"></any>
If the attribute value is not a valid token, then it will itself be used as the element's content.
NOTE: Localizations defined by the i18n
attribute cannot contain HTML tags, as the translation result will be assigned as text, not as HTML.
This limitation enables a slightly faster localization, as no sanitization is required.
It is also possible to provide dynamic user data to the translation function.
The i18n
directive observes all non-directive data-*
attributes and passes them as a normalized map of key/value pairs to the translation function:
<p data-i18n="common.helloWorld" data-name="{{ user.name }}"></p>
Whenever user.name
is updated, it's indicator in the token helloWorld
is subsitituted for the new value when the translation function gets called with an object, e.g. { name: 'Bob' }
as an argument and the element content is then updated accordingly.
The i18n Attribute directive is pretty much the same as the i18n directive except that it expects a json-like object structure represented as a set of key-value pair combinations. The key corresponds to the HTML attribute to be localized, and the value is the localization resource string token which will be passed to the service itself.
If you want to pass dynamic values for the string, those should come after the value for each key; the series of additional parameters is expected to be appendeded to the token, prepended with a separator so that the directive will walk through and replace the numbered place holders with their values.
NOTE: They work the same way as the original i18n
directive, but instead of updating the element content, they update their associated HTML attribute.
The locale
service is an equivalent to the i18n
directive and can be used to generate localized results in situations where the directive cannot be used:
angular.module('myApp', ['ngLocalize'])
.controller('exampleCtrl', ['$scope', 'locale',
function ($scope, locale) {
locale.ready('common').then(function () {
$scope.sampleText = locale.getString('common.helloWorld', {
firstName: 'John',
lastName: 'Smith'
});
});
}
]);
As you can see, The locale
service expects the localization key as the first argument and an optional {Object|Array|String} with user data as the second argument.
The i18n
filter provides the same functionality as the service.
It can be useful in templates where the localization strings are dynamic, e.g. for error messages:
<any>{{ errorMessage | i18n }}</any>
It is also possible to pass an object or an array with localization arguments or even a single string to the i18n
filter:
<p>{{ errorMessage | i18n:data }}</p>
The filter also serves a special purpose targeted at maximum compatibilty with other third party plugin modules, directives, components etc. where it is not possible to provide the parameters as a separate argument. In such a situation, the token is modified to have the substitution text params appended to it like so:
angular.module('myApp', ['ngLocalize'])
.controller('exampleCtrl', ['$scope', '$filter', 'locale',
function ($scope, $filter, locale) {
locale.ready('common').then(function () {
$scope.sampleText = $filter('i18n')('common.helloWorld::["John", "Smith"]');
});
}
]);
Notice how we use the delimiter
from ngLocalize.Config
's localeConf
which can be changed to suit different needs and requirements as well as you see fit in your own app.
The part after the token must be in a valid JSON format as either an array, object or simple string (if it is a single param that needs to be passed in).
I've created a sample app that uses this plugin to provide the text for the entire application.
I registered 'ngLocalize' in my app's dependency list and I then use a combination of
ng-bind="'home.title' | i18n"
,
{{ 'home.title' | i18n }}
,
data-i18n="home.title"
and
data-i18n-attr="{placeholder: 'home.title'}
to insert the text into the page at run time along with their variously supported use-cases.
The MIT License (MIT)
Copyright (c) 2014 Rahul Doshi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.