Code


<?php $localeFound = false; // Detect the selected locale if ($_SERVER["REQUEST_METHOD"] === "GET") { if (isset($_GET["localeId"])) { if ($_GET["localeId"] === "it-it") { // Include Italian locale include("locale/it_it.php"); $localeFound = true; } } } if (!$localeFound) { // Include English locale (default) include("locale/en_us.php"); } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="../../angularjs/angular.js"></script> <script src="../../angularjs/i18n/<?php echo $locData['AngularJSLocaleFileName']; ?>"></script> <script src="script.js"></script> </head> <body ng-app="mainModule"> <div ng-controller="mainController"> <label for="textEdit"><?php echo $locData['Index.textEdit']; ?></label> <input id="textEdit" type="text" ng-model="viewState.textEditValue" /><br /> <br /> <label for="selectionCheck"><?php echo $locData['Index.selectionCheck']; ?></label> <input id="selectionCheck" type="checkbox" ng-model="viewState.selectionCheckValue" /><br /> <br /> <?php echo $locData['Index.todaysDateStr']; ?> {{currentDate | date:"fullDate"}}<br /> <br /> <h4><?php echo $locData['Index.selectLanguageStr']; ?></h4> <button ng-click="changeLanguage('en-us')"><?php echo $locData['Index.englishBtn']; ?></button> <button ng-click="changeLanguage('it-it')"><?php echo $locData['Index.italianBtn']; ?></button> </div> </body> </html>
angular.module("mainModule", ["ngLocale"]) .provider("viewStateManager", function () { return { $get: function ($http, $location, $window, $timeout) { return { saveState: function (stateObj) { // Save the current view state by storing it in the web session // (or any other persistence media) on the server side. var params = { method: "saveState" }; var config = { params: params }; return $http.post("server.php", stateObj, config); }, restoreState: function () { var params = { method: "restoreState" }; var config = { params: params }; return $http.get("server.php", config); }, changeLanguage: function (localeId) { // Set the localeId in the URL parameters $location.search("localeId", localeId); // I need to make the page reload asynchronous otherwise // the browser's window location will not be updated until // the "changeLanguage" function ends (thus reloading the // same URL we had before the "$location.search()" call). $timeout(function() { // Reload the page $window.location.reload(); }, 10); } }; } }; }) .config(function ($locationProvider) { // Use HTML5 mode (in compatible browsers) $locationProvider.html5Mode(true).hashPrefix('!'); }) .controller("mainController", function ($scope, viewStateManager) { // Load the saved view state if it exists viewStateManager.restoreState().then( function (result) { // "result.data" contains the JSON object with the session's view state data // returned by the server or "" if the view state has not been saved // in the web session. if (result.data != "") { $scope.viewState = result.data; } }); // Set the current date/time in a variable $scope.currentDate = new Date(); $scope.changeLanguage = function (localeId) { // Save the current view state viewStateManager.saveState($scope.viewState) .then(function () { // Change the language by reloading the page with a different localeId viewStateManager.changeLanguage(localeId); }); }; });
<?php session_start(); if ($_SERVER["REQUEST_METHOD"] === "GET") { $result = ""; if (isset($_GET["method"])) { if ($_GET["method"] === "restoreState" && isset($_SESSION["clientViewState"])) { // Return the state saved in the session // and remove it from the session (otherwise // a page reload invoked by the user on the browser // will reload an old state instead of loading a // clean page). $result = $_SESSION["clientViewState"]; unset($_SESSION["clientViewState"]); } } echo $result; } else if ($_SERVER["REQUEST_METHOD"] === "POST") { if (isset($_GET["method"])) { if ($_GET["method"] === "saveState") { // Store the view state of the client in the web session if (isset($HTTP_RAW_POST_DATA)) { $_SESSION["clientViewState"] = $HTTP_RAW_POST_DATA; } } } // Nothing to return echo ""; } ?>
<?php $locData = array( "AngularJSLocaleFileName" => "angular-locale_en-us.js", "Index.textEdit" => "Write something:", "Index.selectionCheck" => "Select the state:", "Index.todaysDateStr" => "Today's date is", "Index.selectLanguageStr" => "Select the language:", "Index.englishBtn" => "English", "Index.italianBtn" => "Italian" ); ?>
<?php $locData = array( "AngularJSLocaleFileName" => "angular-locale_it-it.js", "Index.textEdit" => "Scrivi qualcosa:", "Index.selectionCheck" => "Seleziona lo stato:", "Index.todaysDateStr" => "La data di oggi &egrave;", "Index.selectLanguageStr" => "Seleziona la lingua:", "Index.englishBtn" => "Inglese", "Index.italianBtn" => "Italiano" ); ?>

Example


Description


In the previous example, we've talked about localization in AngularJS and we said that the specific locale must already be decided at the bootstrap phase of AngularJS (locale-specific elements of AngularJS are inside a module named ngLocale and module dependencies are loaded just once, when the page is loaded). We also need to make another decision in dealing with localization: how can we handle the different translated strings in our web page and render what's right for the chosen locale? We could use filters to render the strings (the filter takes as input a string ID and returns the translated string for the locale) and keep all the translations in an array on the client side or we could define directives that do the same thing (retrieve the right string for the chosen locale, given the string ID). Unfortunately, there's a problem with both approaches. If we use filters, we need to make AngularJS evaluate a lot of expressions whenever something changes in our HTML page and when we reach a considerable number of expressions, the application slows down too much (it doesn't take that much, especially if the application is going to run on a mobile device with limited processing power). If we use directives, we simply cannot use them in every single place, for example we cannot use a directive to translate in-place a string specified in a HTML attribute of a HTML tag. Fortunately, there's another solution: let the server return the translated page so the browser doesn't have to deal with it (it looks like it's statically translated). The last solution is more suitable also because switching locale is not something that the user does many times (usually the user sets a preference and uses the website with that locale) so it doesn't make sense to steal processing power to the browser for something like this. Now we're going to take a look at a possible way of serving to the browser already translated pages and switching locale on the fly in our web application.

Let's start by describing all the steps of this solution, this will make it easier to take a look at the code later.

  1. The user loads the page without specifying a locale, so the default one is loaded (let's say en-us).
  2. The server returns the right AngularJS locale script in the head tag of the page (the right file in the i18n directory).
  3. For each string in the HTML page, the server returns the right translation by looking at the string ID in a locale-specific array of strings.
  4. The user makes some changes in the web page (by typing something in text fields, selecting checkboxes, etc...).
  5. The user decides that he/she wants to use a different locale (let's say it-it) and presses the button to select the new one.
  6. The client sends all the view state (the state of the client web page that includes all the elements changed by the user) to the server that stores it temporarily in a web session variable.
  7. The client reloads the web page completely by specifying the new locale that has to be loaded as a search parameter in the URL.
  8. When the web page is loaded, the client asks the server if there's a view state previously saved for this web session (the session isn't different from the one we had before reloading the page).
  9. The server sends the view state and removes it from the web session.
  10. The client loads the view state in the scope and with data binding the elements of the page are updated to the previous state.

This is the basic idea: we keep all the view state in an object on the scope (this is the model that will be used also in the HTML elements with data binding), everything that needs to be restored in case we switch locale, and this is just an object that can be automatically serialized to a JSON and stored in the web session on the server; the same JSON can be sent back later to the client and turned into a JavaScript object automatically.

Now, let's take a look at the files in this example. We have the files locale/en_us.php and locale/it_it.php. Each one of them just defines an array with a mapping between string IDs and the corresponding string values for a specific locale. In the main entry point of our web application, index.php, we first check if a particular locale has been specified by reading the value of the localeId search parameter in the URL of the HTTP request and then we include the appropriate localization file from the locale directory. After this, all the strings in the HTML page are translated according to the $locData array defined in the loaded localization file. Inside the script.js file we've defined the viewStateManager provider with some utility methods: saveState sends the view state to the server to let it store everything in the web session, restoreState retrieves the previously stored view state from the server, changeLanguage sets the selected locale ID in the URL's search parameters and completely reloads the web page with the new URL. When the page is loaded, mainController is initialized. It calls restoreState to retrieve the view state from the web session and then puts it in the scope. If there's no previously saved view state, it means that the user did a full page reload from the browser or the user just landed on this web page coming from a different one, so in these cases we want to load a clean web page. The changeLanguage method defined inside mainController is just the handler of a locale change invoked by the used (by pressing the corresponding buttons in the web page), so it simply saves the view state on the server and after that it changes the locale by reloading the page with a different locale ID. The server.php file is simply the server-side logic and is easily understandable with all we've already said.

The comments in the source code might help you to understand better what the different methods do. In particular, the changeLanguage method inside the viewStateManager provider uses the $timeout service. The reason for this is simple: when the search parameter with the locale ID is set in the URL by using the $location service, the browser's window location will not be updated until the changeLanguage method ends and if we immediately invoke a page reload (with $window.location.reload()) in fact we're still using the old URL we had before setting the new localeId search parameter. With the timeout, we let the changeLanguage method end instead, thus giving the chance to the browser to update the window location. After that, we can asynchronously call the page reload with the updated URL. Note that the waiting time (10 milliseconds in this example) specified in $timeout is not important since the window location is updated synchronously as soon as our changeLanguage method ends, so the timeout is just to break the execution and do the next step later.

OK, so what's next? Well, in a web application you're likely to need also some localized strings in your script files, not just the HTML pages. You can deal with this in the same way that the server-side translation works. For example, you could make your script file dynamic (a PHP file in this case) and make it return a different array of strings in JavaScript depending on the locale and those strings could be located in an AngularJS service in the script with some utility functions to retrieve them easily through string IDs. This is a possible idea that doesn't change the basics of this whole example.

We're at the end of the explanation, so let's see again all the steps that we've previously described in a generic way, but this time we're going to say which method calls of our example code are involved in each step.

  1. The user loads the page without specifying a locale, so the default one (en-us) is loaded because index.php cannot find the localeId search parameter in the URL and it loads the locale/en_us.php file. That file contains the $locData array with all the string mappings for the locale.
  2. The server returns the right AngularJS locale script in the head tag of the page by looking at the AngularJSLocaleFileName ID in the $locData array. In this case, the i18n/angular-locale_en-us.js script. When the script.js is loaded, AngularJS has our definition of mainModule and also the definition of mainController. The first thing that mainController does when the web application is loaded, is call the restoreState function of the viewStateManager provider to ask server.php if there's a previous view state to load. Since this is the first time that the page is loaded, there's no view state to retrieve and server.php returns an empty string.
  3. For each string in the HTML page, the server returns the right translation by looking at the string ID in the $locData array.
  4. The user makes some changes in the web page (by typing something in text fields, selecting checkboxes, etc...) and all the changes are automatically stored in the viewState model variable of the scope thanks to data binding.
  5. The user decides that he/she wants to use a different locale and presses the button to select the it-it locale. That button calls the changeLanguage method defined inside mainController and specifies the it-it locale as a parameter.
  6. The client calls the saveState method of the viewStateManager provider to send the whole view state (the viewState model variable) to the server (server.php) that stores it temporarily in a web session variable (clientViewState). The view state is sent by the client as a JSON.
  7. When the saveState method call returns, the client calls the changeLanguage method of the viewStateManager provider to reload the web page completely with the it-it locale. The changeLanguage method sets the new locale as a search parameter in the URL, then it lets the method end by using the $timeout service to make the page reload asynchronous ($window.location.reload()).
  8. When the web page is loaded again, index.php finds the localeId search parameter in the URL and this time loads the locale/it_it.php for the localization. mainController is loaded as well and it calls the restoreState function of the viewStateManager provider to ask server.php if there's a previous view state to load.
  9. server.php checks the clientViewState variable in the web session and returns its content as a JSON to the client. It also removes that variable from the web session because it can be used only when the page is loaded as a consequence of a locale change (it doesn't make sense in case the user decides to reload the page in the browser, for example).
  10. When the restoreState function returns, the client loads the view state in the scope ($scope.viewState) and with data binding the elements of the page are updated to the previous state.