mpopp_0

Internationalisierung mit der GWT RequestFactory

Für moderne Web-Anwendungen ist es mittlerweile Standard Internationalisierung anzubieten, und in weiterer Folge das User Interface in mehrere Sprachen bzw. für mehrere Länder zu übersetzen. In diesem Artikel widmen wir uns der Aufgabe, wie man die GWT RequestFactory und Spring geschickt miteinander kombiniert, damit dem Entwickler sowohl server- als auch clientseitig eine einfache Möglichkeit der Internationalisierung geboten wird. Das Google Web Toolkit (GWT) bietet bereits Möglichkeiten an, um Internationalisierung in eigene Anwendungen einzubauen. Diese Internationalisierung von GWT ist aber rein clientseitig. Möchte man zum Beispiel serverseitig aufgetretene Exceptions am Client abfangen, und dem Benutzer eine passende Fehlermeldung anzeigen, so ist zusätzliche Arbeit nötig. Folgend wird ein konkretes Beispiel vorgestellt, wie man eben solche Exceptions im serverseitigen Code als lokalisierte Nachricht für den Benutzer zum Client überträgt.

Bei der GWT RequestFactory wird in der onFailure-Methode des Receivers bei serverseitig aufgetretenen Exceptions ein ServerFailure-Objekt zurückgegeben. Da die Klassen der am Server aufgetretenen Exceptions nicht zwingend am GWT-Client existieren müssen, sind im ServerFailure-Objekt nur der Exception-Klassenname, eine Message, und ein Stacktrace enthalten. In eigenen Exceptions definierte Felder, wie z.B. für einen Message-Bundle-Key, werden somit nicht zum Client durchgereicht. Per default wird die Exception-Message übertragen, welche oft nicht für eine Benutzeranzeige geeignet und meist auch immer in derselben Sprache ist.

receiver = new Receiver<Void>() {
  @Override
  public void onSuccess(Void v) {
    // successfully returned from server
  }
  @Override
  public void onFailure(ServerFailure error) {
    Window.alert(error.getMessage());
  }
}

Da die Übertragung des Message-Bundle-Keys zum Client somit nicht einfach möglich ist, kann man diesen schon serverseitig in einen lokalisierten Text konvertieren, und bei der Erzeugung des ServerFailure-Objekt als Message setzen. Hierfür wird ein eigener ExceptionHandler wie im nachfolgenden Beispiel implementiert und dem RequestFactoryServlet übergeben. Im ExceptionHandler wird anstelle der Exception-Message der Message-Bundle-Key aus der Exception verwendet um eine lokalisierte Nachricht zu erzeugen, die dann im ServerFailure-Objekt zum Client gesendet wird.

Somit wird dann dem GWT-Client anstelle der Exception-Message eine passende Nachricht übergeben, die auch dem Benutzer direkt angezeigt werden kann. Die eigentliche Exception-Message kann unabhängig von der Benutzer-Nachricht auch technisch ausführlicher und immer in derselben Sprache sein, so wie sie zum Beispiel auch im Server-Log aufscheinen soll.

/**
 * an Exception to be displayed to the user
 */
public class AppException extends Exception {
  private String key;
  private Object[] args;
  public AppException(String message, Throwable cause,
                      String key, Object... args) {
    super(message, cause);
    this.key = key;
    this.args = args;
  }
  public String getKey() {
    return key;
  }
  public Object[] getArgs() {
    return args;
  }
}

public class MyRequestFactoryServlet extends RequestFactoryServlet {
  public PlatformRequestFactoryServlet() {
    super(new MyExceptionHandler());
  }

  @Configurable
  public static class MyExceptionHandler implements ExceptionHandler {
    @Autowired
    private MessageSource messageSource;

    @Override
    public ServerFailure createServerFailure(Throwable throwable) {
      Locale clientLocale = LocaleContextHolder.getLocale(); // determine the local to use on client-side
      if (throwable instanceof AppException) {
        AppException ae = (AppException) throwable;
        return new ServerFailure(
            messageSource.getMessage(ae.getKey(), ae.getArgs(), clientLocale),
            throwable.getClass().getName(), null, true);
      }
      return new ServerFailure(
          "Server Error: " + (throwable == null ? null : throwable.getMessage()),
          throwable == null ? null : throwable.getClass().getName(), null, true);
    }
  }
}

Jetzt haben wir also die Möglichkeit, serverseitig Exceptions zu definieren, für welche eine für den Benutzer lesbare Fehlernachricht direkt zum Client übergeben werden kann. Was uns aber noch fehlt ist das Ermitteln der clientseitig verwendeten Sprache. Hierfür gibt es in Spring verschiedene Implementierungen von LocaleResolver. Unter anderem folgende:

  • AcceptHeaderLocaleResolver – verwendet den Accept-Language-Header vom HTTP-Request (ist meistens in den Browser-Einstellungen konfigurierbar)
  • SessionLocaleResolver – setzt das Locale in die Session
  • CookieLocaleResolver – speichert das Locale in einem Cookie

Einer dieser LocaleResolver kann dann als Bean im ApplicationContext definiert werden, um damit das zu verwendende Locale für die Abarbeitung eines HTTP-Requests zu ermitteln. In Kombination mit GWT ist die komfortabelste Variante, um das vom Client gewünschte Locale auch serverseitig auszulesen, der CookieLocaleResolver. Damit kann nämlich auf ein im GWT-Client gesetztes Cookie mit Spring ohne zusätzliche Client-Server-Kommunikation zugegriffen werden.

Im ExceptionHandler im obigen Beispiel wird aber kein LocaleResolver verwendet, sondern das Locale wird vom LocaleContextHolder geladen. Üblicherweise kümmert sich in Spring das DispatcherServlet darum, dass der LocaleContextHolder korrekt gesetzt wird. Für von GWT kommende Anfragen wird jedoch das von Spring unabhängige RequestFactoryServlet verwendet, wo dieser LocaleContextHolder nicht gesetzt wird. Dafür ist also zusätzliche Konfiguration nötig, sodass der LocaleContext mithilfe des LocaleResolvers erzeugt und in den LocaleContextHolder gesetzt wird, bevor der Request vom RequestFactoryServlet abgearbeitet wird. Mit einem eigenen Filter, kann dies erreicht werden. Dieser Filter muss für das entsprechende Servlet im web.xml konfiguriert werden.

public class LocaleFilter extends OncePerRequestFilter {
  private LocaleResolver localeResolver;

  @Override
  protected void initFilterBean() throws ServletException {
    WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    localeResolver = context.getBean("localeResolver", LocaleResolver.class);
  }

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws ServletException, IOException {
    Locale locale = localeResolver.resolveLocale(request);
    LocaleContextHolder.setLocale(locale);

    chain.doFilter(request, response);

    LocaleContextHolder.resetLocaleContext();
  }
}

Zusammenfassend sei noch zu erwähnen, dass diese Möglichkeit, schon serverseitig eine lokalisierte Nachricht zu generieren, nicht nur für Exceptions verwendet werden kann. Über den LocaleContextHolder kann nämlich an jeder beliebigen Stelle im Code die am Client verwendete Sprache ermittelt werden.

Generell ist aber aufgrund der Vorteile der clientseitigen GWT-Internationalisierung, wie zum Beispiel die Tatsache, dass diese schon vom GWT-Compiler aufgelöst wird, diese zu bevorzugen. Daher empfehlen wir den Umweg über die Internationalisierung am Server nur für Spezialfälle, wo dies in GWT zu mühsam wäre.

Neben dem Beispiel mit den Exception-Messages wäre eine serverseitige Eingabevalidierung (z.B. unter Einsatz von BeanValidation) ein weiterer Anwendungsfall, wo es günstig ist Nachrichten bereits serverseitig zu übersetzen und lokalisiert zum Client zu übertragen. Fallen dir vielleicht noch weitere Beispiele ein, wo die clientseitige Internationalisierung von GWT zu aufwändig wäre und serverseitige Internationalisierung komfortabler anzuwenden ist? Ich freue mich, wenn du uns deine Meinung zu diesem Thema und unserem Lösungsvorschlag mitteilst.

Markus Popp (Software Developer)