Thymeleaf Teil 2 mit Internationalisierung in Spring

Im 2. Teil meines Spring MVC mit Thymeleaf Artikels geht es um: html Formulare und JavaScript in Templates, Wiederverwendung gleicher html-Blöcke in Thymeleaf und der Internationalisierung (I18N) von Spring Web-Anwendungen.

Spring Model-View-Controller mit Thymeleaf und POJOs

Den 1. Teil meines Spring MVC mit Thymeleaf Artikels findet ihr hier und auf YouTube:


Den kompletten Code zu Teil 1 & 2 gibt es hier bei GitHub:
https://github.com/elmar-brauch/thymeleaf

Wiederverwendung von gemeinsamen html Blöcken

Das berühmte Clean Code Prinzip (Clean Code von Robert Martin) "Don't repeat yourself" gilt nicht nur für Java-Code. Es gilt für alle selbst geschriebenen Artefakte.
Wenn wir für 10 verschiedene html Seiten Thymeleaf Templates erstellt haben, sollten wir unbedingt vermeiden per Copy & Paste den Header, Footer oder ähnliches überall zu duplizieren. Damit wir es nicht an 10 Stellen anpassen müssen, wenn entsprechend neue Anforderungen kommen.

Thymeleaf bietet zur Vermeidung von Copy & Paste das Inkludieren von Fragmenten an. Fragmente werden genauso wie andere Thymeleaf Templates in html-Form abgespeichert. Sie befinden sich zusammen mit den anderen html-Dateien im src/main/resources/templates Verzeichnis. In der Fragment-Datei common_head.html habe ich das html head-Tag definiert, weil es für alle Seiten gleich ist:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,
            initial-scale=1, shrink-to-fit=no" />
    <title th:text="#{title.head}" />
    <link rel="stylesheet" th:href="@{/css/main.css}" />
    <script type="text/javascript" th:src="@{/js/main.js}"></script>
</head>
  • Damit eine Gruppe html-Tags von anderen Thymeleaf Templates als Fragment inkludiert werden kann, muss das Fragment in der Datei (hier common_head.html) definiert werden. Dazu habe ich das head-Tag mit dem Thymeleaf Ausdruck th:fragment="head" als Fragment gekennzeichnet. "head" ist der Name des Fragments. Alle Tags innerhalb des head-Tags sind damit Teil des wiederverwendbaren Fragments.
  • Alle anderen hier verwendeten Thymeleaf Ausdrücke wurden schon in meinem ersten Blog-Post zu Thymeleaf vorgestellt, siehe dazu: spring-mvc-thymeleaf.html
Die Inkludierung eines Fragments in ein anderes Template geht so (Ausschnitt der Datei item-create.html):

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="~{common_head :: head}"> />
    <body>...

th:replace ist der Thymeleaf Ausdruck, um ein Tag durch ein Fragment zu ersetzen. "~{common_head :: head}" gibt den Namen der Template Datei (common_head) an, welche das Fragment head enthält. ~{...} ist die zu verwendende Klammer für Thymeleaf Fragment-Referenzen.

Eine Alternative zu Fragmenten sind Templates. Diese sind aber eine Erweiterung bzw. ein Dialekt für Thymeleaf und benötigen daher zusätzliche Maven Dependencies. Ich möchte sie daher hier erst mal nicht weiter vorstellen und verweise euch daher auf:
https://github.com/ultraq/thymeleaf-layout-dialect 

Mit Thymeleaf ins MVC Modell schreiben

Wie man Attribute des Modells im html-Template ausliest, haben wir in meinem ersten Blog-Post zu Thymeleaf gesehen. Jetzt zeige ich wie man mittels html-Formularen Attribute ins Modell schreibt.
Dazu habe ich im Demo-Code die Datei item-create.html erstellt, der folgende html-Ausschnitt ist auf das Formular fokusiert:

<form th:action="@{/item}" method="post">
<fieldset>
    <div class="form-group">
        <input type="text" name="itemname"
            placeholder="Lego" required autofocus/>
    </div>
    
    <div class="form-group">
        <input type="text" name="itemid"
    placeholder="123" required/>
    </div>
    <div class="row">
        <input type="submit" value="Erstellen" />
    </div>
</fieldset>
</form> 

Das Thymeleaf Formular sieht bis auf wenige Ausnahmen wie ein klassisches Formular aus. In dieser einfachen Form wird tatsächlich nur ein einziger Thymeleaf Ausdruck benötigt:
th:action="@{/item}"

th:action stellt die Verbindung zwischen dem Thymeleaf html-Formular und Springs RequestDataValueProcessor her. Durch den RequestDataValueProcessor kommen die beiden input-Tag Parameter mit name="itemname" und name="itemid" automatisch am ItemController in der mit @PostMapping annotierten Methode als @RequestParam annotierter Parameter an. Dabei ist es wichtig, dass die Namen übereinstimmen - ansonsten muss die Annotation ergänzt werden. 

@Controller
public class ItemController {

    @PostMapping("/item") 
    public String createNewItem(Model model,
            @RequestParam String itemname, 
            @RequestParam String itemid) {
        ...

Der Rest des Formulars ist Standard html und kann bei Bedarf hier detaillierter angeschaut werden:
https://wiki.selfhtml.org/wiki/HTML 

Ich möchte euch nicht die Schönheit meines Formulars mit Button-Design von 1990 vorenthalten. 😁 So sieht es im Browser aus:

Thymeleaf html Formular im Browser
 

Thymeleaf und JavaScript

Wie eine separate JavaScript Datei geladen wird, haben wir im 1. Teil meines Thymeleaf Artikels gesehen: spring-mvc-thymeleaf.html. Nun zeige ich euch, wie inline JavaScript geschrieben wird, welches auf das MVC Modell zugreifen kann. Das sieht dann z.B. so aus:

    <script th:inline="javascript">
        var first = [[${items[0].name}]];
        popup('Erstes Element', first);
    </script>

  • Mit th:inline="javascript" wird dem Thymeleaf Template mitgeteilt, dass sich im script-Tag inline JavaScript Code befindet.
  • In diesem inline JavaScript Code können wir nun mit den bekannten Thymeleaf Ausdrücken auf das Modell zugreifen. ${items[0].name} liest das Attribute name aus dem ersten Element der items Liste. Damit der Wert des Attributes name richtig escaped in den JavaScript Code geschrieben wird, werden die eckigen Klammern doppelt um den Ausdruck geschrieben, also [[...]].
  • Die JavaScript Methode popup wurde in der separaten JavaScript Datei des 1. Teils definiert. Sie enthält keine Besonderheiten, also nur normales JavaScript.

Texte in Konfiguration auslagern

Konfigurierbare Texte werden in Spring standardmäßig in messages.properties Dateien gespeichert. Diese erwartet Spring standardmäßig in src/main/resources. Existiert also die Datei src/main/resources/messages.properties, so können wir sie in unseren Thymeleaf Templates auslesen.

messages.properties sind wie andere properties-Dateien einfache Key-Value Dateien. Zum Beispiel kann die Button-Beschriftung im Formular wie folgt in der messages.properties Datei gespeichert werden (1. Zeile im folgenden Ausschnitt):

    button.create-item=Erstellen
    title.item-list=Ding Sammlung
    title.item-create=Ding Erzeugung
    ...
 
Im Thymeleaf Template html können die so konfigurierten Texte mittels Thymeleaf Ausdrücken einfach ausgelesen werden. Für den Button sieht das so aus:

    <div class="row">
        <input type="submit" th:value="#{button.create-item}" />
    </div> 

#{button.create-item} ist ein Thymeleaf Message Ausdruck, der mit messages.properties Dateien verbunden ist. Der Wert in der #{...} Klammer, muss dem Key in der messages.properties Datei entsprechen. 

I18N per Spring Konvention

Wenn unsere Web Anwendung verschiedene Sprachen unterstützen soll, müssen wir unsere Anwendung internationalisieren. Dazu erstellen wir pro Sprache eine eigene messages.properties Datei mit Sprachkürzel. Das sieht dann so für Deutsch und Englisch aus:
  • src/main/resources/messages_de.properties

    button.create-item=Erstellen
    title.item-list=Ding Sammlung
    title.item-create=Ding Erzeugung
    ...

  • src/main/resources/messages_en.properties

    button.create-item=Create
    title.item-list=Item Collection
    title.item-create=Item Creation
    ...


Da in Spring Boot "Convention over Configuration" gilt, können wir auch schon aufgrund der Standards die Seite in der Sprache unseres Browser sehen. Dazu nach dem Start der Anwendung einfach http://localhost:8080 aufrufen.

Schafft man es im Browser die Sprache so umzustellen, dass im HTTP Request ein passender Accept-Language Header mitgeschickt wird, ändert sich die Sprache entsprechend. Leider ist diese Browser-Einstellungen, genau eine Sprache zu akzeptieren, nicht trivial. Browser schicken nämlich häufig die beiden Sprachen Deutsch und Englisch zusammen im Accept-Language Header zum Server.

Einfacher kann man das ganze mit curl in der Kommandozeile testen. Dadurch das wir den Accept-Language Header explizit setzen (mit -H), stellen wir sicher, dass nur eine Sprache akzeptiert und abgefragt wird. Hier die beiden curl-Kommandos:
  • Deutsche Sprache:
    curl -H "Accept-Language: de" http://localhost:8080
  • Englische Sprache:
    curl -H "Accept-Language: en" http://localhost:8080
Alle anderen Sprache funktionieren, wenig überraschend, analog. 😉
In den Thymeleaf Templates müssen dazu keine Anpassungen gemacht werden.

Sprachauswahl durch den Client 

Wenn dem Benutzer ein Schalter oder ähnliches zum Ändern der Sprache angeboten werden soll, so können wir dafür unterstützende Spring Beans erstellen. Diese Spring Beans speichern die ausgewählte Sprache z.B. in der Session oder in Cookies, so dass sie dann für alle folgenden Seitenaufrufe gilt.
Konkret könnte das so gemacht werden:

    @Configuration
    public class I18nConfig implements WebMvcConfigurer {
        @Bean
        public LocaleResolver localeResolver() {
         var resolver = new SessionLocaleResolver();
    resolver.setDefaultLocale(Locale.GERMAN);
         return resolver;
        }
        @Bean
        public LocaleChangeInterceptor localeChangeInterceptor() {
            var lci = new LocaleChangeInterceptor();
            lci.setParamName("lang");
            return lci;
        }

        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(localeChangeInterceptor());
        }
    } 

  • Bean Definitionen mit @Bean und @Configuration hatte ich schon vorgestellt, siehe kernkonzepte-von-spring-beans-und.html
  • Die LocaleResolver Bean speichert den vom Benutzer ausgewählten Locale, also die Sprache und den Ländercode (in der Demo arbeite ich aber nur mit der Sprache). Konkret wird hier ein SessionLocaleResolver instanziiert, der die Sprachauswahl in der Browser Session zwischen Client und Server hält.
  • Die LocaleChangeInterceptor Bean ist ein Interceptor, welcher bei jedem Request die Sprache in der LocaleResolver Bean ändern kann. Das tut der Interceptor genau dann, wenn der Request einen Request Parameter mit dem Namen "lang" hat. Dann wird der Wert des Parameters als neue Locale im LocaleResolver hinterlegt.
    Was ein Interceptor genau ist, könnt ihr z.B. hier nachlesen Pro Spring 5 Buch
  • Damit der Interceptor sich in die Spring Request Verarbeitung einklinkt, muss er als Interceptor in der InterceptorRegistry registiert werden. Das machen wir, indem wir unsere @Configuration Klasse das Interface WebMvcConfigurer implementieren lassen und die Registrierung in der überschriebenen Methode addInterceptors vornehmen.
Nun können wir jede Seite unserer Web-Anwendung in jede konfigurierte Sprache übersetzen, indem wir die Anwendung mit folgender URL aufrufen:
  • Deutsche Sprache: http://localhost:8080?lang=de
  • Englische Sprache: http://localhost:8080?lang=en
Danach muss der Request Parameter nicht mehr mit geschickt werden, da die Sprache in der Session gespeichert ist. Wird die Spring Boot Application neu gestartet, ist die Session und damit die Sprachauswahl wieder vergessen.

Wie man in der Web-Seite einen Schalter zum Wechseln der Sprache macht, zeige ich nicht, da es eine schöne Übung für euch ist. 😎 
Ihr könnt z.B. die zuvor gezeigten Elemente verwenden, um einen Button zu erstellen, der einen Request mit Parameter "lang=de" oder "lang=en" an den Server schickt. 

Fazit & Ausblick

Im 2. Teil meines Thymeleaf Blog-Artikels haben wir einige weiterführende Techniken in Thymeleaf kennengelernt. Die Basics zu Spring MVC und Thymeleaf findet ihr im 1. Teil:

Wenn ihr tiefer in Thymeleaf eintauchen wollt, schaut euch die Thymeleaf Webseite an:
https://www.thymeleaf.org/

Kommentare

Anonym hat gesagt…
Bedeutet das, dass du für alle 110 Sprachen, die du auf deinem Blog hier anbietest, eine eigene messages_xx.properties erstellt hast, die extra eigene Übersetzungen enthalten?
Danke für die Frage.

Mein Blog ist nicht mit Spring und Thymeleaf gemacht.
Ich verwende den Blog-Service von Goolge, siehe https://de.wikipedia.org/wiki/Blogger.com.
Die 110 Sprachen kommen von Google Translate. Google Translate ist als Plugin einfach eingebunden.

Im Enterprise Kontext will man aber meist eigene Übersetzungen selbst formulieren und nicht automatisch generierte Übersetzungen von Google Translate verwenden. Das kann man dann mit Spring und den message.properties Dateien machen, so wie es hier gezeigt ist.

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

CronJobs mit Spring

Kernkonzepte von Spring: Beans und Dependency Injection