Skip to content

Instantly share code, notes, and snippets.

@ErikAndreas
Last active December 18, 2015 16:49
Show Gist options
  • Save ErikAndreas/5814231 to your computer and use it in GitHub Desktop.
Save ErikAndreas/5814231 to your computer and use it in GitHub Desktop.
Proof of concept of full stack and tooling for AngularJS i18n gettext-style using Jed, pybabel and po2json. UPDATE: will now be maintained at https://github.com/ErikAndreas/lingua and tooling at https://github.com/ErikAndreas/grunt-lingua

Been looking for a full stack including tools for gettext-style i18n with AngularJS.

  • Gettext-style support in markup (html and javascript) supporting singular, plural and interpolation/sprintf
  • Tooling for extraction of strings to be translated (to .pot) from html and javascript
  • Tooling for generating .json of .po files

Ended up (working proof of concept) with the following:

  • "lingua", an AngularJS module wrapping Jed
  • Some AngularJS bootstrapping
  • pybabel for xgettext style translations extraction (to .pot)
  • po2json for generating .json files (per translation) from .po files
<!-- sample usage html/partial/view markup -->
{{_("Your last.fm username")}}:<br/>
<input ng-model="lastFMuserName"/> <button ng-click="setLastFMuserName()">{{_("Set")}}</button>
<h3>{{_n("Suggestion","Suggestions",suggs.length)}}</h3>
// sample usage in Angular service
angular.module('swl').factory('artistAlbumModelService',['linguaService', ... ,function(linguaService, ...) {
addArtistAlbum:function(ar,al) {
if (artistAlbumModelService.containsArtistAlbum(ar,al)) {
statusService.add('error',linguaService._("Skipping duplicate, %1$s %2$s is already in the list",[ar,al]));
...
}]);
// sample usage in Angular controller
angular.module('swl').controller('SettingsCtrl',['$scope', ..., function($scope, ...) {
statusService.add('info',$scope._("import complete"));
}]);
// init
var Lingua = {
init:function(doc,cb) {
"use strict";
var locale = localStorage.getItem('locale');
console.log(locale);
if (locale) {
var s = doc.createElement('script');
s.setAttribute('src', "//code.angularjs.org/1.1.5/i18n/angular-locale_"+locale+".js");
doc.body.appendChild(s);
} else {
locale = "en-us";
}
microAjax('l_'+locale+'.json',function(data) {
data = JSON.parse(data);
var i18n = new Jed({
"domain" : locale,
"missing_key_callback" : function(key) {
console.error("Missing key " + key + " for " + locale);
},
locale_data : data
});
console.log(i18n);
window.i18n = i18n;
cb();
});
}
};
// the lingua module
angular.module('lingua',[]);
angular.module('lingua').factory('linguaService',function() {
var linguaService = {
_:function(singular, vars) {
return i18n.translate(singular).fetch(vars);
},
_n:function(singular, plural,n, vars) {
if (n) {
return i18n.translate(singular).ifPlural(n, plural).fetch(n);
} else {
return i18n.translate(singular).fetch(vars);
}
}
};
return linguaService;
});
// usage in view/partial: ng-click:changeLocale('sv-se);
angular.module('lingua').controller('linguaController',['$scope', '$window',function linguaController($scope,$window) {
$scope.changeLocale = function(locale) {
// so, only way to reload $locale is on full page reload
// and load angular-locale_xx-yy.js
localStorage.setItem('locale',locale);
$window.location.reload();
};
}]);
// Angular init stuff
angular.module('swl').run(['$rootScope',...,'linguaService',function($rootScope,...,linguaService) {
$rootScope._ = linguaService._;
$rootScope._n = linguaService._n;
...
}]);
<!doctype html>
<html lang="en" xmlns:ng="http://angularjs.org"> <!-- manual bootstrap so no ng-app -->
...
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>
<script src="js/directives.js"></script>
<script src="js/lingua.js"></script>
<script src="js/vendor/jed.js"></script>
<script src="js/vendor/microajax.js"></script>
<script>
angular.element(document).ready(function() {
console.log('angular doc ready');
Lingua.init(document, function() {
angular.bootstrap(document, ['swl']);
});
});
</script>
</body>
</html>

##Requirements:

  1. python + pybabel + jinja2
  2. nodejs + >npm install -g po2json

##Simple babel.cfg file: [javascript:*.js] encoding = utf-8

[jinja2: *.html] encoding = utf-8

##Workflow: ###Generate .pot file:

pybabel extract -F babel.cfg -k _n:1,2 -k _ -o translations/messages.pot . partials js

###Translations tool like poedit to create a catalog and make translations (outputs .po/.mo files)

###Generate .json

po2json translations/sv-se.po l_sv-se.json

@paumoreno
Copy link

Thanks for this nice gist! It has been very enlightening :)

I found, though, that some AngularJS syntax can break the jinja2 exctraction method. The problematic token I found is || (e.g. <span>{{newsItem.total_votes.votes || 0}}</span>). Whenever the extractor finds the double pipe, it fails silently for that file, and none of the translation strings found there appears in the .pot file.

Have you encountered any other problem concerning the incompatibilities between AngularJS templates and jinja2?

@ErikAndreas
Copy link
Author

Glad if it helped!

I've yet not found any issues, however - the extraction (look at the pybabel command) and the functionality (wrapping Jed) is based on pure function calls within an expression (no filtering), i.e _("string to extract") or _n("singular", "plural", n)

I don't understand how (or why) you'd want to extract (for translation) what your example denotes.

But, it is problematic when/if pybabel fails (silently) and stops extraction for that entire file.

This is the most gettext-like workflow for Angular (with no server-side magic) I've been able to find/come up with, any suggestions for improvements will be most appreciated!

@0x-r4bbit
Copy link

Not gettext-style but maybe interesting for you guys @paumoreno @ErikAndreas: angular-translate -http://pascalprecht.github.io/angular-translate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment