Caution
This was an incredibly useful, and fun project to work on during the early times of NodeJS and AngularJS. Today, this project hopefully has no purpose anymore and is officially archived as of 30-07-2024.
I have an application that uses Node and express. I use i18n-node for internationalization.
On the frontend, I use AngularJS and I have certain strings (in controllers, services and templates) which I also need to have translated. I could use angular-translate here, but I would really like to use the same resources which I have already generated with i18n-node on my backend.
The solution is available in npm and bower packages for the backend and frontend respectively. So you can just...
-
...install them with:
npm install i18n-node-angular bower install i18n-node-angular
-
...register the express extensions:
Note: Up until 1.4.0 it was always possible to add previously unknown translation literals to the translation files. This is no longer supported due to security implications.
The feature is only available in development environments whereNODE_ENV
is set todevelopment
.var i18n = require( "i18n" ); var i18nRoutes = require( "i18n-node-angular" ); // The order of these calls matters! app.use( i18n.init ); app.use( i18nRoutes.getLocale ); // This line should be removed with express 4.0 app.use( app.router ); i18nRoutes.configure( app );
-
...put the locale into the DOM (this is using jade):
html( ng-app="yourApp", i18n-locale=i18n.getLocale() )
-
...use the translations in Angular:
// Depend on i18n module var yourApp = angular.module( "yourApp", [ "ngRoute", "i18n" ] ) .config( [ "$routeProvider", "$locationProvider", function( $routeProvider, $locationProvider ) { $routeProvider .when( "/", { templateUrl : "examples", controller : IndexController /* By enabling the resolver below, the i18n service won't be injected into IndexController until the locale has been loaded from the server. To enable the resolver, just remove these two characters ↓ */ /* ,resolve : { i18n : [ "i18n", function( i18n ) { return i18n.i18n(); } ] } //*/ } ); $locationProvider.html5Mode( true ); } ] ) .factory( "MyService", function( i18n ) { // Use i18n service injected into this service. console.log( i18n.__( "My translation phrase" ) ); } ) .controller( "IndexController", [ "i18n", "$scope", IndexController ] ); function IndexController( i18n, $scope ) { // Inject the service into the scope, so we can access __() and 'loaded'. $scope.i18n = i18n; // Try to instantly translate a phrase. This can fail, because the locale might not have been loaded yet. console.log( "Instant: " + i18n.__( "My translation phrase" ) ); i18n.ensureLocaleIsLoaded().then( function() { // Chaining on the promise returned from ensureLocaleIsLoaded() will make sure the translation is loaded. console.log( "Insured: " + i18n.__( "My translation phrase" ) ); } ); }
-
The users locale must be injected using the
i18n-locale
directive. The value of it is observed, so you can change the locale at any time. -
If you want to use the i18n service, you'd generally want to use the
i18n.i18n
promise in a resolver for your route. This way you don't have to useensureLocaleLoaded
all the time.In general, if you inject i18n into your controller, you probably want to have
i18n : [ "i18n", function( i18n ) { return i18n.i18n(); } ]
in your resolver.i18n.i18n
returns a promise to return the service once the locale has been loaded. At that point in time, your controller will be instantiated and have the service injected; ready for you to use. -
In your view, pass your translatable string through the
i18n
filter. This will cause items to be automatically re-translated if the locale changes.If you want to use pluralization, pass the count into the
i18n
filter and supply the singular and plural terms as additional arguments, like{{2|i18n:"%s cat":"%s cats"}}
.If you want to hide something until the translation is loaded, inject the i18n service into your scope and use
i18n.loaded
withngShow
.
If you're using i18n-node's object notation functionality, additional configuration is required if you're using a delimiter other than .
.
Provide the delimiter in your i18nRoutes.configure
call. For example:
i18nRoutes.configure( app, { directory : path.join( applicationRoot, "locales" ), objectNotation : "→" } );
Provide the delimiter in your angular.module
call, by using the i18nProvider
. For example:
var yourApp = angular.module( "yourApp", [ "i18n" ] )
.config( [ "i18nProvider", function( i18nProvider ) {
i18nProvider.setObjectNotation( "→" );
} ] );
To make this approach work, we have to make several changes to the application at hand. The final setup is as follows:
- Communicate the locale, which was detected by i18n-node on the backend, to the frontend through the DOM.
- Establishing express routes that allow for retrieval of translations.
- Accessing those routes from Angular.
First of all, we want to place the locale that was determined by i18n-node into our DOM for easy retrieval later on. Of course, you could detect the locale on the client side, but this approach makes sure that the same locale is used on both the backend and the frontend. To do that, we first create a function that we can call when compiling our template.
app.use( function( req, res, next ) {
res.locals.acceptedLanguage = function() {
return i18n.getLocale.apply( req, arguments );
};
next();
} );
Now we can use that method in our template. In a jade template it would be as simple as:
body(data-language=acceptedLanguage())
Note: You can automatically create these routes through i18n-node-routes.js, which is available through npm (npm install i18n-node-angular
).
We now define two new routes in our express application. The first route will provide our full i18n-node translation document and the second will translate a single phrase.
module.exports = function( app ) {
app.get( "/i18n/:locale", routes.i18n );
app.get( "/i18n/:locale/:phrase", routes.translate );
}
Let's look at the implementation of those routes.
The content of our translation document can then later be used freely on the frontend. For convenience, we'll also implement a service which we'll check out further below.
/**
* Sends a translation file to the client.
* @param request
* @param response
*/
exports.i18n = function( request, response ) {
var locale = request.params.locale;
response.sendfile( "locales/" + locale + ".json" );
};
The translate route could theoretically be used to dynamically load single translated phrases from the backend. We primarily use it to have previously unknown translation phrases added to our JSON files by i18n-node.
/**
* Translate a given string and provide the result.
* @param request
* @param response
*/
exports.translate = function( request, response ) {
var locale = request.params.locale;
var phrase = request.params.phrase;
var result = i18n.__( {phrase: phrase, locale: locale} );
response.send( result );
};
Note: You can automatically create the service (and more) through i18n-node-angular.js, which is available through bower (bower install i18n-node-angular
).
We now create an AngularJS service, named i18n
with a method to access translations, named __()
, thus making the usage equivalent to that on the backend.
See i18n-node-angular.js
for a complete example.
We can now access translations easily, by injecting our i18n
service.
function MyController( i18n ) {
console.log( i18n.__( "My translation phrase" ) );
}
servicesModule.factory( "MyService", function( i18n ) {
console.log( i18n.__( "My translation phrase" ) );
}
If a term wasn't translated yet, the service will invoke the /i18n/locale/phrase
route and cause i18n-node to add it to the translation JSON file.
i18n-node-angular.js
also defines a filter named i18n
. This filter can be used to translate strings in templates. While a lot of strings that appear in page templates are probably already translated on the backend, strings appearing in templates in directives aren't usually passed through the same channel; this is where we use the filter.
angular.module( "foo", [])
.directive( "bar", function() {
return {
template: "{{'My translation phrase'|i18n}}"
};
});
It is possible that you might invoke i18n.__
before the translation map was loaded from the server. This will cause an error message to be written to your console, telling you that you need to call ensureLocaleIsLoaded
.
ensureLocaleIsLoaded
returns a promise. So you can simply wrap the call around your use of i18n.__
or include the call earlier in your load hierarchy. Whatever suits your needs best.
An example would be:
function MyController( i18n ) {
i18n.ensureLocaleIsLoaded().then( function() {
console.log( i18n.__( "My translation phrase" ) );
} );
}
servicesModule.factory( "MyService", function( i18n ) {
i18n.ensureLocaleIsLoaded().then( function() {
console.log( i18n.__( "My translation phrase" ) );
} );
}
Alternatively, you can also make the controller creation wait for the locale to be loaded. An example of such a configuration with the i18n
service would be:
$routeProvider.
when( "/", {
templateUrl: "/partials/index",
controller : IndexController,
resolve : { i18n : [ "i18n", function( i18n ) { return i18n.i18n(); } ] }
} );