Problem
I’m rebuilding a basic webpage using AngularJS, and want to implement changing languages when clicking an icon. So far I’ve made a service called 'langService'
, that has functions to set and get language. I then pass the service a value (say ‘eng’ or ‘slo’) with the set language function when clicking an icon on the page.
In my controllers, I’m passing in the 'langService'
, then binding the factory to $scope
like this $scope.lng = Language;
in order to call it from my views. In my views I then build 2 different pages from JSON objects, and display the using ng-show
, according to the variable set in the langService
.
It works. But it produces a lot of duplication in code, I need to pass the langService
to every controller, and bind to scope again. How can I make it simpler?
This is an example from my navbar:
HTML:
<div id="navbar" ng-controller="navbarController">
<ul ng-show="lng.getLanguage()==='slo'">
<li class="dropdown" ng-repeat="menu in slo"><a ng-href="{{ menu.url }}">{{ menu.main }}<span ng-class="menu.icon"></span></a>
<ul class="drop-nav">
<li ng-repeat="submenu in menu.submenu[0]"><a ng-href="{{ submenu.url }}">{{ submenu.text }}</a></li>
</ul>
</li>
<li><a href="" ng-click="changeLanguage('eng')"><img src="images/EN.gif"></a></li>
</ul>
<ul ng-show="lng.getLanguage()==='eng'">
<li class="dropdown" ng-repeat="menu in eng"><a href="{{ menu.url }}">{{ menu.main }}<span ng-class="menu.icon"></span></a>
<ul class="drop-nav">
<li ng-repeat="submenu in menu.submenu[0]"><a ng-href="{{ submenu.url }}">{{ submenu.text }}</a></li>
</ul>
</li>
<li><a href="" ng-click="changeLanguage('slo')"><img src="images/SLO.gif"></a></li>
</ul>
</div>
Service:
angular.module('langService', [])
.factory('Language', function() {
var language = 'slo';
function setLanguage(lang) {
//...optional logic
language = lang;
}
function getLanguage() {
return language;
}
return {
setLanguage: setLanguage,
getLanguage: getLanguage
};
});
Controller:
ZICApp.controller('navbarController', function ($scope, Language) {
//bind language object from langService
$scope.lng = Language;
$scope.changeLanguage = function(lang) {
Language.setLanguage(lang);
};
$scope.slo = [
{
main: "IJS",
url: "https://www.ijs.si/ijsw"
},
{
main: "ZIC",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "predstavitev",
url: "#/"
},
two: {
text: "osebje",
url: "#/osebje"
}
}]
},
{
main: "knjižnica",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "tiskane revije",
url: ""
},
two: {
text: "elektronske revije",
url: ""
},
three: {
text: "baze podatkov",
url: ""
},
four: {
text: "katalog(COBISS)",
url: "http://www.cobiss.si/scripts/cobiss?command=CONNECT&base=50108"
}
}]
},
{
main: "storitve",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "bibliografije",
url: ""
},
two: {
text: "medknjižnična izposoja",
url: ""
},
three: {
text: "fotokopirnica",
url: ""
}
}]
}
];
$scope.eng = [{
main: "IJS"
},
{
main: "ZIC",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "introduction",
url: "#/"
},
two: {
text: "staff",
url: "#/osebje"
}
}]
},
{
main: "library",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "printed journals",
url: ""
},
two: {
text: "electronic journals",
url: ""
},
three: {
text: "databases",
url: ""
},
four: {
text: "catalogue(COBISS)",
url: "http://www.cobiss.si/scripts/cobiss?command=CONNECT&base=50108"
}
}]
},
{
main: "services",
icon: "glyphicon glyphicon-triangle-bottom",
submenu: [{
one: {
text: "bibliographies",
url: ""
},
two: {
text: "interlibrary loan",
url: ""
},
three: {
text: "copy room",
url: ""
}
}]
}];
});
The rest of the views are built in mostly the same way. If you want to look at the whole project here’s the GitHub Repo and the page in action.
Solution
TL;DR: Just use an existing i18n plugin for angular. There’s Angular Translate which I have used for a while and is pretty good.
Now translations are just “view logic”. It doesn’t change anything in the underlying data. It’s like a “face” of an Xbox controller. No matter how many faces you have, you may even have them cataloged alphabetically, you’ll always have the buttons, sticks and triggers working in the same way.
So let’s apply the same concept to Angular. The only thing your controller should be aware about is the data. As for how we will know what text to put in to the UI, we represent the text as “keys”. These will correspond to text defined somewhere in Angular, like a constant. Where the key and your translation meet is the UI, via filters.
controller -(key, data)-> UI -------(key)-------> filter
filter <-(current language)- service
filter <-(translation key)- constant
UI <-(translated text)- filter
// Sample translation key
app.constant('Translations', {
en: {
'PAGE_TITLE': 'Hi',
},
es: {
'PAGE_TITLE': 'Hola!',
}
});
// Sample factory which only holds our current language state
app.factory('LanguageService', function(){
var currentLanguage = 'en';
return {
setCurrentLanguage: function(value){
currentLanguage = value;
},
getCurrentLanguage: function(){
return currentLanguage;
}
}
});
// Sample filter which transforms our UI value
app.filter('translate', function(Translations, LanguageService){
// We receive the input, translate according to the current language and key
// and return the output.
return function(input){
// Translations.en.PAGE_TITLE
return Translations[LanguageService.getCurrentLanguage()][input] || '';
};
});
// Sample controller
app.controller('MyController', function(LanguageService){
$scope.data = {
// Note that it only holds the "key" to the text. The filter will figure it
// out for you. This data will be the same across all languages.
title: 'PAGE_TITLE',
url: '/home',
};
// The only time your controller is
$scope.changeLanguage = function(value){
LanguageService.setCurrentLanguage(value);
};
});
// Sample UI
<!-- This passes title to the translate, then renders the return value -->
<a href="{{ url }}">{{ title | translate }}</a>
<button type="button" ng-click="setCurrentLanguage('en')>English</button>
<button type="button" ng-click="setCurrentLanguage('es')>Español</button>
As you can see, the UI isn’t aware of language-specific stuff. The only thing it knows is that there’s a title and URL. Any language-specific things should be represented as keys in the data, in this case title
. Since url
remains the same, we can simply render it in the UI straight away.
The only inconvenience I see is that your data should carry keys instead of text, and that it should correspond to the keys found in the constants.
In order to add more translations, you just add more key-value pairs in the constant, making sure each key is in different languages. The filter will take care of transforming your data on the UI level given that the data provides the right keys.
Long story short, this is pretty much how Angular Translate works.