Roland Oldengarm - Independent IT Contractor

Living in the coolest little capital Wellington, New Zealand!

Angular 2: Automated i18n workflow using gulp

Update: New post can be found here.

i18n (internationalization) support for Angular 2 is now built-in the framework. Previously we used ng2-translate, but I found it time-consuming to add all translations manually to the .json files. Since a week or so the documentation is also available at Angular.io. While useful, it did not provide information, it involves a lot of manual steps. I’m going to provide a set of Gulp tasks that will automate everything!

The following steps are required for i18n in Angular2:

  1. Add i18n attributes to your components HTML
  2. Run “ng-xi18n” to generate an .XLF file with all translations
  3. Copy this file to all of your languages. Optional: For your default language (e.g. English), copy all source elements to the target as they should be the same.
  4. Create a .ts file which exports the contents of the XLF file as a string

Steps 2 and 4 have to be done every time you make a change to an existing i18n element, or add a new one! As I hate manual steps, I’ve transformed this into a set of Gulp tasks. The final result is one gulp task “i18n-build” that runs the following sequence:

// this task will generate the xlf file, re-creates the default English translation file, copy to i18n folders, and then
// add missing translations to all translations
gulp.task('i18n-build', function() {
runSequence('i18n-extract-xlf', 'i18n-default','i18n-merge-to-translations', 'i18n-xlf2ts');
});

  • i18n-extract-xlf runs the i18n-build tool
  • i18n-default copies the generated messages.xlf to messages.en.xlf, and copies all source values to the target elements
  • i18n-merge-to-translations copies the modifed/added elements to all translations. We can’t just overwrite existing translations as that will overwrite already made translations; we just need to append missing elements. We also need to remove obsolete elements from the translation files
  • 18n-xlf2ts generates TS files that exports the contents of the XLF file as a string.

So now every time we make an update to an existing i18n element, or add a new one, we only need to run “gulp i18n-build”, and everything works again! At regular intervals we send the xlf file for Chinese (in our case) to our translators.

I’m using a couple of gulp plugins, you’ll need to add as devDependencies with:


npm i --save-dev gulp-cheerio gulp-modify-file gulp-rename gulp-run merge-stream run-sequence

The final set of gulp tasks:

var sourceElements = [];
gulp.task('i18n-get-source', function() {
 return gulp.src('./src/i18n/messages.en.xlf')
 .pipe(cheerio({
 run: function ($, file) { 
 
 $('trans-unit').each(function() { 
 sourceElements.push($(this));
 }); 
 },
 parserOptions: {
 xmlMode: true
 }
 }));
});
 
gulp.task('i18n-merge-to-translations', ['i18n-get-source'], function() {
 var languages = ['zh'];
 var tasks = [];
 for(var language of languages) {
 var path = "./src/i18n/messages." + language + ".xlf";
 tasks.push(
 gulp.src(path)
 .pipe(cheerio({
 run: function ($, file) {
 var sourceIds = [];
 for (var sourceElement of sourceElements) {
 var id = $(sourceElement).attr('id');
 sourceIds.push(id);
 var targetElement = $('#' + id);
 if (targetElement.length == 0) {
 // missing translation
 $('body').append(sourceElement);
 }
 }
 // now remove all redundant elements (i.e. removed)
 $('trans-unit').map((function() {
 var id = $(this).attr('id');
 var existing = sourceIds.find((item) => { return item == id} );
 
 if (!existing) {
 console.log("REMOVING");
 // remove it 
 $('#' + id).remove(); 
 } 
 }));
 
 
 } ,
 parserOptions: {
 xmlMode: true
 } 
 }))
 .pipe(gulp.dest('./src/i18n')));
 }
 return mergeStream(tasks);
})


// run ng-xi18n
gulp.task('i18n-extract-xlf', function() {
 return run('ng-xi18n').exec();
});



// create .ts files for all .xlf files so we can import it 
gulp.task('i18n-xlf2ts', function () {
 return gulp.src("./src/i18n/*.xlf")
 .pipe(rename(function (path) {
 path.extname = ".ts"
 }))
 .pipe(modifyFile(function (content, path, file) {
 var filename = path.replace(/^.*[\\\/]/, '')
 var language = filename.split(".")[1].toUpperCase();
 return "export const TRANSLATION_" + language + " = `" + content + "`;";
 }))
 .pipe(gulp.dest("./src/i18n"));
});

// copy all source values to the target value as a default translation and make that our English translation
gulp.task('i18n-default', function() {
 return gulp.src('./messages.xlf')
 .pipe(cheerio({
 run: function ($, file) {
 // Each file will be run through cheerio and each corresponding `$` will be passed here.
 // `file` is the gulp file object
 
 $('source').each(function() {
 var source = $(this);
 var target = source.parent().find('target');
 //source.text(source.text().toUpperCase());
 target.html(source.html());
 }); 
 },
 parserOptions: {
 xmlMode: true
 }

 }))
 .pipe(rename('messages.en.xlf'))
 .pipe(gulp.dest("./src/i18n"))
});

 

9 Comments

  1. Jose Carlos Roldan

    October 24, 2016 at 3:06 am

    Hi,

    i’m trying to use your gulp file but i get this error:

    Error: Cannot determine the module for component LoginComponent!
    at /Users/jcroldan/git/bfab/client/node_modules/@angular/compiler-cli/src/extract_i18n.js:98:27
    at Array.map (native)
    at /Users/jcroldan/git/bfab/client/node_modules/@angular/compiler-cli/src/extract_i18n.js:94:52
    at Array.map (native)
    at Extractor.extract (/Users/jcroldan/git/bfab/client/node_modules/@angular/compiler-cli/src/extract_i18n.js:92:28)
    at extract (/Users/jcroldan/git/bfab/client/node_modules/@angular/compiler-cli/src/extract_i18n.js:14:35)
    at Object.main (/Users/jcroldan/git/bfab/client/node_modules/@angular/tsc-wrapped/src/main.js:30:16)
    at Object. (/Users/jcroldan/git/bfab/client/node_modules/@angular/compiler-cli/src/extract_i18n.js:153:9)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    Extraction failed

    Can you help me?

  2. Thanks for the guide, Roland! Could you give any tips on how to consume the translations once generated? Like a sample i18n-providers.ts or the like? Thank you!

    • rolandoldengarm

      December 14, 2016 at 8:24 am

      Hi, at this stage i18n only works in HTML templates. There is no TranslationService or something similar to access translations from JS/TS. Follow this thread for more information: https://github.com/angular/angular/issues/9104

      For now, the solution we are going to implement is to add the strings we use in our TS code to a hidden div, with a certain ID. Then, in our backend code we retrieve the contents of that particular div. It’s not elegant, but that’s the only solution.

      By the way, we have changed our approach for translation quite a bit since I wrote this post. I will write a blog post about it as soon as we’ve implemented it all, so stay tuned! Sign up for my news letter or follow me on Twitter to be the first to know.

      • Thanks! Let’s see, I think I phrased poorly. I indeed meant merely consuming the values in the templates by somehow hooking up the TypeScript string constants you’ve generated to seed the bootstrapModule call. But in the mean time I have managed to cobble together a working solution to just that, albeit not very automated yet. So I’m unblocked but not quite ready to show anything yet.

  3. Deepika Bhandari

    April 11, 2017 at 1:18 am

    Thanks alot for the guidance. This was very helpful to automate the i18n workflow. It reduces our efforts by just writing a gulp script . I have managed to integrate it into my project and it works absolutely fine !

  4. I found issue concerning tags inside xlf file after using gulp task that merges missing id’s. It modifies the source tag leaving it open without closing /source tag and adds some weird characters inside target tag for ex ‘&#xA1’.

Leave a Reply

Your email address will not be published.

*