2
\$\begingroup\$

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&amp;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&amp;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.

\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ thanks a lot, tried it out in codepen, working as expected. Only thing I needed to modify was to make the filter stateful (translateFilter.$stateful = true;) in order for the filter to updated when the value of the translation changes. Definitely learned some new stuff here. \$\endgroup\$ – Miha Šušteršič Jan 7 '16 at 13:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.