Internationalizing messages and contents for REST

It was necessary to talk about validation before talking about internationalizing content and messages. With global and cloud-based services, supporting content in only one language is often not sufficient.

In this recipe, we provide an implementation that suits our design and therefore continue to meet our scalability standards for not relying on HTTP Sessions.

We will see how to define the MessageSource beans in charge of fetching the most suited message for a given location. We will see how to serialize resource properties to make them available to the frontend. We will implement a dynamic translation of content on this frontend with AngularJS and angular-translate.

How to do it…

There is both backend and a frontend work in this recipe.

Backend

  1. The following bean has been registered in the core context (csm-core-config.xml):
    <bean id="messageBundle" class="edu.zc.csm.core.i18n.SerializableResourceBundleMessageSource">
    <property name="basenames" value="classpath:/META-INF/i18n/messages,classpath:/META-INF/i18n/errors"/>
      <property name="fileEncodings" value="UTF-8" />
      <property name="defaultEncoding" value="UTF-8" />
    </bean>
  2. This bean references a created SerializableResourceBundleMessageSource that gathers the resource files and extracts properties:
    /**
     * @author rvillars
     * {@link https://github.com/rvillars/bookapp-rest} 
     */
    public class SerializableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
       public Properties getAllProperties(Locale locale) {
          clearCacheIncludingAncestors();
          PropertiesHolder propertiesHolder = getMergedProperties(locale);
          Properties properties = propertiesHolder.getProperties();
        return properties;
      }
    }
  3. This bean bundle is accessed from two places:

    A newly created PropertiesController exposes publicly (serializing) all the messages and errors for a specific location (here, just a language):

    @RestController
    @ExposesResourceFor(Transaction.class)
    @RequestMapping(value="/properties")
    public class PropertiesController{
      @Autowired
      protected SerializableResourceBundleMessageSource messageBundle;
      @RequestMapping(method = RequestMethod.GET, produces={"application/json; charset=UTF-8"})
      @ResponseBody
      public Properties list(@RequestParam String lang) {
        return messageBundle.getAllProperties(new Locale(lang));
      }
    }

    A specific service layer has been built to easily serve messages and errors across controllers and services:

    @Service
    @Transactional(readOnly = true)
    public class ResourceBundleServiceImpl implements ResourceBundleService {
      @Autowired
    protected SerializableResourceBundleMessageSource messageBundle;
      private static final Map<Locale, Properties> localizedMap = new HashMap<>();
      @Override
      public Properties getAll() {
        return getBundleForUser();
      }
      @Override
      public String get(String key) {
        return getBundleForUser().getProperty(key);
      }
      @Override
      public String getFormatted(String key, String... arguments) {
        return MessageFormat.format( getBundleForUser().getProperty(key), arguments
        );
      }
      @Override
      public boolean containsKey(String key) {
        return getAll().containsKey(key);
      }
      private Properties getBundleForUser(){
        Locale locale = AuthenticationUtil.getUserPrincipal().getLocale();
        if(!localizedMap.containsKey(locale)){
          localizedMap.put(locale, messageBundle.getAllProperties(locale));
        }
        return localizedMap.get(locale);
    }
    }

    Note

    The ResourceBundleServiceImpl uses the same SerializableResourceBundleMessageSource for now. It also extracts the locale from the logged-in user (Spring Security) with a fallback to English.

  4. This ResourceBundleServiceImpl service is injected in our WebContentInterceptor CloudstreetApiWCI:
      @Autowired
      protected ResourceBundleService bundle;
  5. In the TransactionController, for example, the bundle is targeted to extract error messages:
    if(!transaction.getUser().getUsername()
        .equals(getPrincipal().getUsername())){
      throw new AccessDeniedException( bundle.get(I18nKeys.I18N_TRANSACTIONS_USER_FORBIDDEN)
    );
    }
  6. I18nKeys is just a class that hosts resource keys as constants:
    public class I18nKeys {
      //Messages
    public static final String I18N_ACTION_REGISTERS = "webapp.action.feeds.action.registers";
    public static final String I18N_ACTION_BUYS = "webapp.action.feeds.action.buys";
    public static final String I18N_ACTION_SELLS = "webapp.action.feeds.action.sells";
     ...
    }
  7. The resource files are located in the core module:
    Backend

Frontend

  1. Two dependencies for angular-translate have been added in the index.jsp:
    <script src="js/angular/angular-translate.min.js"></script>
    <script src="js/angular/angular-translate-loader-url.min.js"></script>
  2. The translate module is configured as follows in the index.jsp:
    cloudStreetMarketApp.config(function ($translateProvider) {
       	$translateProvider.useUrlLoader('/api/properties.json');
      $translateProvider.useStorage('UrlLanguageStorage');
      $translateProvider.preferredLanguage('en');
      $translateProvider.fallbackLanguage('en');
    });

    Note

    You can see that it targets our API endpoint that only serves messages and errors.

  3. The user language is set from the main menu (main_menu.js). The user is loaded and the language is extracted from user object (defaulted to EN):
    cloudStreetMarketApp.controller('menuController',  function ($scope, $translate, $location, modalService, httpAuth, genericAPIFactory) {
        $scope.init = function () {
        ...
      genericAPIFactory.get("/api/users/"+httpAuth.getLoggedInUser()+".json")
      .success(function(data, status, headers, config) {
          $translate.use(data.language);
          $location.search('lang', data.language);
      });
      }
      ...
      }
  4. In the DOM, the i18n content is directly referenced to be translated through a translate directive. Check out in the stock-detail.html file for example:
    <span translate="screen.stock.detail.will.remain">Will remain</span>

    Another example from the index-detail.html file is the following:

    <td translate>screen.index.detail.table.prev.close</td>

    In home.html, you can find scope variables whose values are translated as follows:

    {{value.userAction.presentTense | translate}}
  5. In the application, update your personal preferences and set your language to French for example. Try to access, for example, a stock-detail page that can be reached from the stock-search results:
    Frontend
  6. From a stock-detail page, you can process a transaction (in French!):
    Frontend

How it works...

Let's have a look at the backend changes. What you first need to understand is the autowired SerializableResourceBundleMessageSource bean from which internationalized messages are extracted using a message key.

This bean extends a specific MessageSource implementation. Several types of MessageSource exist and it is important to understand the differences between them. We will revisit the way we extract a Locale from our users and we will see how it is possible to use a LocaleResolver to read or guess the user language based on different readability paths (Sessions, Cookies, Accept header, and so on).

MessageSource beans

First of all, a MessageSource is a Spring interface (org.sfw.context.MessageSource). The MessageSource objects are responsible for resolving messages from different arguments.

The most interesting arguments being the key of the message we want and the Locale (language/country combination) that will drive the right language selection. If no Locale is provided or if the MessageSource fails to resolve a matching language/country file or message entry, it falls back to a more generic file and tries again until it reaches a successful resolution.

As shown here, MessageSource implementations expose only getMessage(…) methods:

public interface MessageSource {
  String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
  String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
  String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

This lightweight interface is implemented by several objects in Spring (especially in context components). However, we are looking specifically for MessageSource implementations and three of them in Spring 4+ particularly deserve to be mentioned.

ResourceBundleMessageSource

This MessageSource implementation accesses the resource bundles using specified basenames. It relies on the underlying JDK's ResourceBundle implementation, in combination with the JDK's standard message-parsing provided by MessageFormat (java.text.MessageFormat).

Both the accessed ResourceBundle instances and the generated MessageFormat are cached for each message. The caching provided by ResourceBundleMessageSource is significantly faster than the built-in caching of the java.util.ResourceBundle class.

With java.util.ResourceBundle, it's not possible to reload a bundle when the JVM is running. Because ResourceBundleMessageSource relies on ResourceBundle, it faces the same limitation.

ReloadableResourceBundleMessageSource

In contrast to ResourceBundleMessageSource, this class uses Properties instances as custom data structure for messages. It loads them via a PropertiesPersister strategy using Spring Resource objects.

This strategy is not only capable of reloading files based on timestamp changes, but also loads properties files with a specific character encoding.

ReloadableResourceBundleMessageSource supports reloading of properties files using the cacheSeconds setting and also supports the programmatic clearing of the properties cache.

The base names for identifying resource files are defined with the basenames property (in the ReloadableResourceBundleMessageSource configuration). The defined base names follow the basic ResourceBundle convention that consists in not specifying the file extension nor the language code. We can refer to any Spring resource location. With a classpath: prefix, resources can still be loaded from the classpath, but cacheSeconds values other than -1 (caching forever) will not work in this case.

StaticMessageSource

The StaticMessageSource is a simple implementation that allows messages to be registered programmatically. It is intended for testing rather than for a use in production.

Our MessageSource bean definition

We have implemented a specific controller that serializes and exposes the whole aggregation of our resource bundle properties-files (errors and message) for a given language passed in as a query parameter.

To achieve this, we have created a custom SerializableResourceBundleMessageSource object, borrowed from Roger Villars, and its bookapp-rest application (https://github.com/rvillars/bookapp-rest).

This custom MessageSource object extends ReloadableResourceBundleMessageSource. We have made a Spring bean of it with the following definition:

<bean id="messageBundle" class="edu.zc.csm.core.i18n.SerializableResourceBundleMessageSource">
<property name="basenames" value="classpath:/META-INF/i18n/messages,classpath:/META-INF/i18n/errors"/>
  <property name="fileEncodings" value="UTF-8" />
  <property name="defaultEncoding" value="UTF-8" />
</bean>

We have specifically specified the paths to our resource files in the classpath. This can be avoided with a global resource bean in our context:

<resources location="/, classpath:/META-INF/i18n" mapping="/resources/**"/>

Note that Spring MVC, by default, expects the i18n resource files to be located in a /WEB-INF/i18n folder.

Using a LocaleResolver

In our application, in order to switch the Locale to another language/country, we pass through the user preferences screen. This means that we somehow persist this information in the database. This makes easy the LocaleResolution that is actually operated on the client side, reading the user data and calling the internationalized messages for the language preference asynchronously.

However, some other applications might want to operate LocaleResolution on the server side. To do so, a LocaleResolver bean must be registered.

LocaleResolver is a Spring Interface (org.springframework.web.servlet. LocaleResolver):

public interface LocaleResolver {
  Locale resolveLocale(HttpServletRequest request);
  void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}

There are four concrete implementations in Spring MVC (version four and above):

AcceptHeaderLocaleResolver

The AcceptHeaderLocaleResolver makes use of the Accept-Language header of the HTTP request. It extracts the first Locale that the value contains. This value is usually set by the client’s web browser that reads it from OS configuration.

FixedLocaleResolver

This resolver always returns a fixed default Locale with optionally a time zone. The default Locale is the current JVM's default Locale.

SessionLocaleResolver

This resolver is the most appropriate when the application actually uses user sessions. It reads and sets a session attribute whose name is only intended for internal use:

public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE";

By default, it sets the value from the default Locale or from the Accept-Language header. The session may also optionally contain an associated time zone attribute. Alternatively, we may specify a default time zone.

The practice in these cases is to create an extra and specific web filter.

CookieLocaleResolver

CookieLocaleResolver is a resolver that is well suited to stateless applications like ours. The cookie name can be customized with the cookieName property. If the Locale is not found in an internally defined request parameter, it tries to read the cookie value and falls back to the Accept-Language header.

The cookie may optionally contain an associated time zone value as well. We can still specify a default time zone as well.

There's more…

Translating client-side with angular-translate.js

We used angular-translate.js to handle translations and to switch the user Locale from the client side. angular-translate.js library is very complete and well documented. As a dependency, it turns out to be extremely useful.

The main points of this product are to provide:

  • Components (filters/directives) to translate contents
  • Asynchronous loading of i18n data
  • Pluralization support using MessageFormat.js
  • Expandability through easy to use interfaces

A quick overview of angular-translate is shown in this figure:

Translating client-side with angular-translate.js

International resources are pulled down either dynamically from an API endpoint (as we did), either from static resource files published on the web application path. These resources for their specific Locale are stored on the client-side using LocalStorage or using cookies.

The stored data corresponds to a variable (UrlLanguageStorage in our case) that is accessible and injectable in whatever module that may require translation capabilities.

As shown in the following examples, the translate directive can be used to actually render translated messages:

  <span translate>i18n.key.message</span> or  
  <span translate=" i18n.key.message" >fallBack translation in English (better for Google indexes) </span>

Alternatively, we can use a predefined translate filter to translate our translation keys in the DOM, without letting any controller or service know of them:

{{data.type.type == 'BUY' ? 'screen.stock.detail.transaction.bought' : 'screen.stock.detail.transaction.sold' | translate}}

You can read more about angular-translate on their very well done documentation:

https://angular-translate.github.io

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset