Internationalization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario and the org.gwtproject:gwt BOM, checked on 2026-10-09.
Internationalization is mostly client and build knowledge: the interfaces and formatters live in the com.google.gwt.i18n packages, the translations are properties files and annotations that the GWT compiler turns into Java classes during deferred binding, and the choice of language is made in the browser by the bootstrap script.
The server takes part only through the host page (it can write a meta tag, set a cookie or print a JavaScript object) and through whatever it uses for its own messages.
Every listing on this page was compiled by the 2.13.1 compiler and run: a probe module com.example.i18n.Probe with the locales en, es and ar was loaded in headless Chrome 155 and inspected through its DOM, the Bookshelf additions ran as GWTTestCase classes in HtmlUnit, and the compiler messages and outputs are quoted as they were printed.
GWT does almost all locale work at compile time: a locale is a value of a compile-time property, every supported locale is compiled into its own JavaScript file, and the browser downloads only the one that matches.
That makes translations type-safe (a missing argument is a compile error), makes unused messages disappear from the output and makes lookups as fast as a string literal, but it also means that adding a language is a rebuild, not a data change.
The page walks through the whole model: the techniques and when to pick each, how a locale is declared, found and inherited, Constants and Messages with plural and select forms, the dynamic Dictionary alternative, locale-sensitive classes and resources, number, date and currency formatting, bidirectional text, the command-line tools and properties-file encoding, and a complete English and Spanish Bookshelf with plural review counts.
The model: one locale, one permutation
In GWT locale is a client property, exactly like user.agent.
A client property can have any number of values, but all of them must be known when the compiler runs, because the compiler produces one optimized JavaScript program (a permutation) for every combination of property values.
The bootstrap script <module>.nocache.js evaluates the properties in the browser, maps them to one permutation and loads that *.cache.js file only.
The generators that write the Constants and Messages classes are deferred-binding generators, described on the deferred binding page.
The Bookshelf declares two locales and the default two user.agent values, so the compiler printed Compiling 4 permutations and wrote this mapping (file compilation-mappings.txt):
0497484FB46F7E126EE9B187A2DD9739.cache.js
locale en
user.agent safari
19EF293A60C1F77300B093380C8B5E99.cache.js
locale en
user.agent gecko1_8
342AE3F7D9EDD9BB635FB4F8BFDC783C.cache.js
locale es
user.agent safari
47F4948768F26929DC32C5E89F8678BE.cache.js
locale es
user.agent gecko1_8
Each of the four files was about 263 to 264 KB in the production build, so a Spanish user never downloads the English strings and vice versa. The next diagram shows what happens to a messages interface and its properties files on the way to those four files.
@DefaultMessage, @AlternateMessage"] --> gen["LocalizableGenerator
runs once per compiled locale"] props["AppMessages_en.properties
AppMessages_es.properties"] --> gen gen --> impls["Generated classes
AppMessages_en, AppMessages_es"] impls --> perms["4 permutations
locale x user.agent"] perms --> browser["The browser downloads one *.cache.js
chosen by nocache.js from the locale value"]
Source: GWT Developer Guide — Static String Internationalization and GWT Developer Guide — i18n Locale: Overview
Two consequences shape everything below.
-
Locales multiply permutations. Every locale you add multiplies the number of compiles and the build time (see the compiler page for the cost of permutations), so list only the languages you really translate and use runtime locales (see [runtime-locales]) for country variants.
-
The locale of a running page is fixed. There is no
setLocalecall; the page changes language by reloading with a different query parameter, cookie ormetavalue (see [switching]), after which the module restarts.
Choosing a technique
GWT offers several ways to internationalize, and a real application mixes them. The Developer Guide starts with a decision list; this table puts it in one place, with the cost of each approach from the guide and the probe.
| Technique | Use it for | Checked | Cost |
|---|---|---|---|
|
Typed values loaded from properties: |
Compile time (a value of the wrong type is a compile error). |
Smallest: unused accessors are removed. |
|
Constants that must also be read by name at run time (data-driven screens). |
Compile time for the accessors; the by-name lookup fails at run time with |
Larger: the compiler cannot discard unused accessors and the lookup cannot be resolved at compile time. |
|
Text with parameters, plural forms, select forms, number, date and list formatting, |
Compile time (number and use of arguments). |
Small; the compiler can inline a message like a string concatenation. |
|
Strings printed by the server into the host page as a JavaScript object, when the web server already owns the translations. |
Not at all (keys are strings; a wrong key throws at run time). |
Larger and slower; no recompile needed when the strings change. |
|
A whole algorithm or class that differs per locale, selected by name ( |
Compile time (it is ordinary Java). |
Only the matching class is in each permutation. |
|
Text that is part of a UiBinder template (see [uibinder-i18n]). |
Compile time. |
The same as |
Locale-sensitive resources |
Images, text and style files that differ per locale (see [resources]). |
Compile time. |
One copy per permutation. |
The rest of the Developer Guide advice reduces to four rules of thumb:
-
writing code from scratch: start with
Messagesfor text andConstantsfor typed values; -
you have existing properties files: let
I18NSync(see [tools]) generate the interfaces from them; -
you add GWT to a page that already has a localization system on the server: use
Dictionary; -
you only want properties files delivered to the client regardless of language: use
Constantswith no locale-specific files.
Setting up a module
A module that uses internationalization inherits the I18N module.
com.google.gwt.user.User already inherits it (the probe compiled without the explicit line), so the inherits element is optional but harmless and makes the dependency visible.
Locales are added with extend-property; the only locale that exists without any declaration is the literal value default.
<module>
<inherits name="com.google.gwt.user.User" />
<inherits name="com.google.gwt.i18n.I18N" />
<!-- 1. the full list of supported locales -->
<extend-property name="locale" values="en,es" />
<!-- 2. remove the 'default' locale permutation -->
<set-property name="locale" value="en,es" />
<!-- 3. use English wherever no locale is requested or matched -->
<set-property-fallback name="locale" value="en" />
</module>
The three lines are the recipe from the Developer Guide and they are what the Bookshelf module uses:
-
extend-propertyadds the values.valuestakes a comma-separated list, so one element can add several locales. -
set-propertywith the same list restricts the permutations to the listed values, which removes thedefaultpermutation. -
set-property-fallbacknames the real language that answers when the requested locale is unknown (?locale=zz) or absent.
Why bother? The default locale has no language, so the Developer Guide warns that "many things will produce surprising results" in it: only a small set of currencies is supported and no plural forms at all.
The same gap shows up as a compiler warning about an unknown plural form (see the plural forms section and the pitfalls table).
The probe module with three locales and the three-line recipe printed Compiling 6 permutations (3 locales x 2 user agents); leaving default in would have added two more.
Properties files: names and lookup
Translations live in properties files that sit in the same package as the interface, are named after it and carry a locale suffix. The best match is looked up separately for every accessor, so a regional file can redefine a handful of keys and rely on the language file for the rest:
| Compiled locale | File that is tried, in order |
|---|---|
unspecified or |
|
|
|
|
|
Values given in @Default... annotations take precedence over the default (suffix-less) properties file when both exist.
The package of the interface must be on the module’s source path, and the properties files must be on the compiler classpath in the same package; the Bookshelf keeps them next to the interface’s package in bookshelf-client/src/main/resources/com/example/bookshelf/client/i18n/, which the Maven plugin puts on the compiler classpath.
Encoding
The Java properties format is ISO-8859-1 by default, but GWT reads these files as UTF-8, so accented and non-Latin characters can be written directly and native2ascii is no longer needed.
Three places must agree:
-
the properties files are saved as UTF-8 (set the encoding in the IDE, and
project.build.sourceEncodingtoUTF-8in the parent POM, as the Bookshelf does, so resource copying does not re-encode them); -
the Java sources are UTF-8 for the same reason (the compiler plugin takes the same property);
-
the host page declares
<meta charset="utf-8">in itsheadand is served as UTF-8, otherwise correctly compiled text appears garbled.
The probe checked what happens when this goes wrong.
A Spanish file saved as ISO-8859-1 compiled without an error or a warning and displayed the replacement character: appName=Estanter�a.
Unicode escapes also work, as in any Java properties file: Estanter\u00eda in the file displayed Estantería.
The Developer Guide tutorial still writes Ä and ü HTML entities in its German properties files from the time when UTF-8 was not available; with UTF-8 files and a UTF-8 host page, write the characters themselves, because an entity in a String message that goes through setText would be shown literally.
Constants
Constants is the interface for typed constants.
Every method is a constant accessor with no parameters, its key is the method name (or @Key) and its return type decides how the property value is parsed.
The default value comes from a @Default...Value annotation, from the suffix-less properties file, or from both.
| Return type | Default-value annotation | Property value is read as |
|---|---|---|
|
|
a plain string |
|
|
comma-separated; a comma inside a value is written |
|
|
an |
|
|
a |
|
|
a |
|
|
|
|
|
a comma-separated list of property names, each of which is a key whose value is the map value; iteration follows declaration order |
The Bookshelf needs configuration values and labels that are not parameterized. This interface, with a Spanish file that overrides only some of the values, was compiled and run in the probe:
package com.example.i18n.client;
import com.google.gwt.i18n.client.Constants;
import java.util.Map;
public interface BookConstants extends Constants {
@DefaultStringValue("Bookshelf")
String appName();
@DefaultIntValue(25)
int pageSize();
@DefaultBooleanValue(true)
boolean showCovers();
@DefaultDoubleValue(0.21)
double vatRate();
@DefaultStringArrayValue({"Fiction", "Science", "History"})
String[] categories();
@DefaultStringMapValue({"en", "English", "es", "Spanish"})
Map<String, String> languages();
}
# BookConstants_es.properties (UTF-8). Only the keys that differ from the annotations.
appName=Estantería
pageSize=10
vatRate=0.1
categories=Ficción,Ciencia,Historia\\, política
languages=en,es
en=Inglés
es=Español
The class is created with GWT.create, never with new: it is an interface, and the generator supplies the implementation for the current locale.
BookConstants c = GWT.create(BookConstants.class);
String name = c.appName(); // "Bookshelf" or "Estantería"
int rows = c.pageSize(); // 25 or 10, parsed at compile time
Map<String, String> languages = c.languages();
The same call printed these values in the two compiled locales:
| Accessor | ?locale=en |
?locale=es |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
3 elements: |
3 elements: |
|
|
|
Points worth knowing:
-
In a properties file
#starts a comment, so a value that contains#must escape it; annotation defaults do not need any escaping, which is why the Developer Guide recommends annotations: you can also use compile-time Java constants in them, and the value stays next to the method. -
A properties file with a wrong value type fails the build, naming the value:
[ERROR] diez could not be parsed as a number.forpageSize=diez. -
A
Mapaccessor reads the key list from its own property, then one property per key; the keys of the map therefore also exist as properties and must not collide with other accessor names. -
A missing key with no annotation default is a compile error (see
No resource found for keyin the pitfalls table).
ConstantsWithLookup
ConstantsWithLookup extends Constants with by-name lookup methods: getString, getStringArray, getInt, getFloat, getDouble, getBoolean and getMap, each taking the method name and each declared to throw MissingResourceException.
It is meant for highly data-driven code, for example a menu whose item names come from the server and are looked up as keys.
public interface BookLookup extends ConstantsWithLookup {
@DefaultStringValue("Fiction")
String fiction();
@DefaultStringValue("Science")
String science();
}
BookLookup lookup = GWT.create(BookLookup.class);
String shelf = lookup.getString("fiction"); // "Fiction", or "Ficción" in the es permutation
String typo = lookup.getString("poetry"); // throws MissingResourceException
The failing call threw java.util.MissingResourceException with the message Cannot find constant 'poetry'; expecting a method name.
The price of this flexibility is size: the compiler cannot prove which accessors are unused when they can be reached by name, so every accessor of the interface stays in the output.
Prefer plain Constants unless the key really is data.
Messages
Messages is the interface for text with parameters.
Each method returns a String (or SafeHtml, see [safehtml-messages]), its parameters match the {0}, {1} placeholders of the message, and the compiler checks that the message uses exactly the arguments the method declares.
The message syntax follows java.text.MessageFormat with GWT extensions (the choice format type is not supported; plural and select forms replace it), and the compiler removes what it can: unused methods vanish and the localized strings are inlined.
# ErrorMessages.properties
permissionDenied = Error {0}: User {1} Permission denied.
public interface ErrorMessages extends Messages {
String permissionDenied(int errorCode, String username);
}
ErrorMessages msgs = GWT.create(ErrorMessages.class);
Window.alert(msgs.permissionDenied(errorVal, loginId));
Usually the default text goes into the source with @DefaultMessage, and properties files add the other languages.
The Bookshelf’s catalogue line is the smallest realistic message:
@DefaultMessage("{0} by {1}")
@Description("A book line in the catalogue: title, then author")
String bookLine(String title, String author);
which printed Dune by Frank Herbert in English and Dune de Frank Herbert with the Spanish entry bookLine={0} de {1}.
If a key is missing in a translation, the compiler falls back to the @DefaultMessage text silently, so an untranslated message appears in the default language, not as an error.
Quoting rules
MessageFormat quoting is the first thing that bites.
A single quote starts a quoted section, so the apostrophe in an English default is written twice, and text that must not be treated as a placeholder is quoted:
@DefaultMessage("It''s {0} and ''{0}''")
String apostrophe(String x); // "It's x and 'x'"
@DefaultMessage("Saved \"{0}\"")
String saved(String title); // Java string escapes for double quotes are ordinary Java
A single apostrophe is not silently swallowed as it is in plain MessageFormat: the 2.13.1 generator refuses it with [ERROR] Error parsing 'It's {0}'.
The official tutorial states the same rule ("replace them with two consecutive single quotes in the properties files").
The GWT message formats
Beyond {0}, a placeholder can carry a format that is applied in the current locale.
| Placeholder | Meaning | Output in the probe (en / es) |
|---|---|---|
|
the argument converted to a string with Java concatenation ( |
|
|
the locale’s decimal format |
|
|
the locale’s default currency format |
|
|
currency format with an explicit currency code (an extra argument after a colon) |
|
|
a custom pattern (you are now responsible for localizing it) |
as for |
|
the JRE-style predefined date and time formats |
|
|
a locale-specific predefined format by |
|
|
a skeleton: the fields you want, in any order; the locale supplies the pattern |
|
|
a |
|
|
a static argument: shown to translators as the placeholder |
|
Notes on the table:
-
With a custom pattern such as
{0,date,MM/dd/yy}every locale gets the same pattern and many users misread it; use a predefined format or a skeleton so the order of day and month follows the locale. -
An extra argument is written after the format name and a colon,
{0,number:curcode=EUR,currency}, and a dynamic argument can be a method parameter, for example{0,localdatetime:tz=$tz,predef:DATE_FULL}with aTimeZone tzparameter. The spelling{0,number,currency:curcode=EUR}also compiles, but the text after the second comma is read as a custom number pattern and the message printedcurrency:curcode=EUR10for 9.5. -
The list format knows only the locale’s default separators and the logical "and"; there is no way to ask for "a, b, or c".
-
Static arguments keep markup out of the translator’s text:
@DefaultMessage("Welcome back, {startBold,<b>}{0}{endBold,</b>}")printedWelcome back, <b>Ana</b>. The result is still aString; if it is shown in an HTML context, use aSafeHtmlmessage.
Annotations
The annotations below are nested in com.google.gwt.i18n.client.LocalizableResource (shared by Constants and Messages) and in Messages (message-only).
Inside the body of a Messages or Constants interface the nested names are in scope, so @DefaultMessage and @Description need no import.
On the interface declaration itself, import them: com.google.gwt.i18n.client.LocalizableResource.Generate.
| Annotation | Purpose |
|---|---|
|
The locale of the text in the source. The default is |
|
Generate the keys of all methods with a key generator: the default is the MD5 generator |
|
Write a message catalog for translators during the build (see [tools]). |
|
Marks an interface as generated from a file (documentation only; the name is not checked). |
| Annotation | Purpose |
|---|---|
|
The text for the default locale. With |
|
Pairs of a form name and its text for the default locale: plural forms ( |
|
The key in the external format. Without it, the key comes from |
|
A note for the translator. It is not part of an MD5 key, and depending on the format the translator may not see it. |
|
Tells identical texts apart for translation (two methods with the same text and different meanings should have different keys). |
|
Deprecated: use |
| Annotation | Purpose |
|---|---|
|
Marks the parameter that selects the plural form. It must be |
|
With |
|
Marks the parameter that selects an alternate message: an integral primitive, a |
|
An example value, shown to translators instead of |
|
This parameter need not appear in every translation. Without it, a translation that omits a parameter is a compile error. |
Source: GWT Developer Guide — i18n: Java Annotations and GWT Developer Guide — i18n Messages: Using Annotations
The Developer Guide writes the signature of @Generate as @Generate(String[] formatFQCN, ...); the real element is named format, as in the Javadoc and the 2.13.1 source.
A method that uses most of them, as the Bookshelf catalogue could declare it:
@DefaultMessage("Reviews")
@Key("reviews.header")
@Meaning("column header")
@Description("Header of the review count column")
String reviewsHeader();
@DefaultMessage("Hello {0}")
String hello(@Example("Alice") String name);
@DefaultMessage("Static text only")
String optional(@Optional String ignored);
Plural forms
Most languages change the word for a counted thing with the count, and the rules differ: English separates 1 from the rest, French uses the singular for 0 and 1, Arabic has five special forms besides the default, and some languages have no plural at all.
GWT ships plural rules for hundreds of languages and applies them to the parameter marked @PluralCount.
The Bookshelf shows review counts under each title. The default (English) text sits in the annotations:
@DefaultMessage("{0,number} reviews")
@AlternateMessage({"=0", "No reviews yet", "one", "{0,number} review"})
@Description("Number of reader reviews of a book, shown under its title")
String reviews(@PluralCount int count);
@DefaultMessage is the text for every count that has no entry (the English plural), "one" is the singular and "=0" is an exact value that wins over the rules: it is nicer to say "No reviews yet" than "0 reviews".
Other languages put their forms in the properties file with a bracket syntax that GWT invented because properties files have no plural support:
# AppMessages_es.properties
reviews={0,number} reseñas
reviews[\=0]=Aún no hay reseñas
reviews[one]={0,number} reseña
The = of an exact value is escaped as \= because it also separates a key from its value.
These strings printed in the two locales:
| Count | English | Spanish |
|---|---|---|
0 |
|
|
1 |
|
|
2 |
|
|
1234 |
|
|
The plural form names that the rules can produce include none, one, two, few and many, plus the default.
They do not have to match English grammar: the Developer Guide’s French example puts the singular on one for both 0 and 1, so never hard-code the count in the one text of a language other than English.
The Arabic rule that GWT uses is: none for 0, one for 1, two for 2, few when the last two digits are 03 to 10, many when they are 11 to 99, and the default form for everything else.
A probe file that supplied all six forms for ar (with placeholder English text) printed:
reviews(0)=no reviews (none) reviews(1)=one review (one)
reviews(2)=two reviews (two) reviews(3)=٣ reviews (few)
reviews(11)=١١ reviews (many) reviews(101)=١٠١ reviews (default)
reviews(102)=١٠٢ reviews (default) reviews(1234)=١٬٢٣٤ reviews (many)
The digits are Arabic-Indic because the ar number format is used for {0,number} (see [number-format]).
English needs its own properties file. Plural forms are not inherited between locales, because locales may have different plural rules (even pt and pt_BR differ), and the default locale has no plural rule.
The English text lives in the annotations, so a project with only _es files looks complete, but the English class generated for locale en then uses the rule of the language-less locale, and the compiler says:
[WARN] Plural form 'one' unknown in com.google.gwt.i18n.client.impl.plurals.DefaultRule: ignoring
The one entry is dropped and the count 1 prints 1 reviews; the Bookshelf test then fails with expected: <1 review>, actual: <1 reviews>.
The fix is a file AppMessages_en.properties next to the interface, even an almost empty one, so that GWT generates a real en class:
# AppMessages_en.properties
# English uses the defaults from @DefaultMessage; this file makes GWT generate an "en" implementation with the English plural rule.
The Bookshelf ships exactly this file.
The probe showed two sibling warnings for the same reason: [WARN] Plural form 'few' unknown in com.google.gwt.i18n.client.impl.plurals.DefaultRule_es: ignoring when a Spanish file used a form Spanish does not have, and [WARN] In locale 'ar', required plural forms are missing: [few, none, many, two] when a locale has no translation for a plural message.
Exact values, offsets and lists
"=N" forms (exact values), @Offset and list formatting handle the cases where grammar rules are not enough.
Both are demonstrated by a message that shows who liked a book; the offset subtracts the two names that are shown before the plural rule decides between "and one other" and "and N others":
@DefaultMessage("{1}, {2} and {0} others like this")
@AlternateMessage({"=0", "Nobody likes this yet", "=1", "{1} likes this", "=2", "{1} and {2} like this",
"one", "{1}, {2} and one other like this"})
String likes(@PluralCount @Offset(2) String[] names, String first, String second);
likes={1}, {2} y {0} más les gusta esto
likes[\=0]=A nadie le gusta esto todavía
likes[\=1]=A {1} le gusta esto
likes[\=2]=A {1} y {2} les gusta esto
likes[one]={1}, {2} y otra persona más les gusta esto
With five names the English message printed Ana, Luis and 3 others like this: the exact forms are compared with the raw count (0, 1 and 2), and the offset of 2 is applied afterwards, so a count of 3 selects one and a count of 5 the default form with {0} equal to 3.
The Spanish output was Ana, Luis y 3 más les gusta esto.
A caller passes the array and the names it wants shown, which the Developer Guide admits is clumsy:
String[] names = {"Ana", "Luis", "Eva"};
msg.likes(names, names.length > 0 ? names[0] : null, names.length > 1 ? names[1] : null);
Lists are formatted by {0,list} with the locale’s separators, and a List can also be the @PluralCount argument.
@DefaultMessage("Orders {0,list,number} are ready for pickup.")
@AlternateMessage({"=0", "No orders are ready for pickup.", "one", "Order {0,list,number} is ready for pickup."})
String ordersReady(@PluralCount List<Integer> orders);
The English results were No orders are ready for pickup., Order 14 is ready for pickup., Orders 14 and 17 are ready for pickup. and Orders 14, 17, and 21 are ready for pickup. for lists of 0, 1, 2 and 3 orders.
Select forms
A @Select parameter chooses between messages by value instead of by count: a gender, a state, a boolean.
The Bookshelf uses it for the availability of a copy:
public enum Availability { AVAILABLE, ON_LOAN, RESERVED }
@DefaultMessage("{0} is not available right now")
@AlternateMessage({"AVAILABLE", "{0} is on the shelf", "ON_LOAN", "{0} is on loan"})
String availability(String title, @Select Availability state);
availability={0} no está disponible ahora
availability[AVAILABLE]={0} está en la estantería
availability[ON_LOAN]={0} está prestado
The default text is used for every value without an entry (here RESERVED, and null).
The probe printed Dune is on the shelf, Dune is on loan and Dune is not available right now for the three states, and Dune está en la estantería, Dune está prestado and Dune no está disponible ahora in Spanish.
In the properties file the form is the key plus the value in brackets, with the enum constant name written exactly.
SafeHtml messages
When a message contains markup and its result goes into an HTML context (the content of an HTML or InlineHTML widget), declare the method with the return type com.google.gwt.safehtml.shared.SafeHtml.
The generator then HTML-escapes every String argument before substitution and leaves SafeHtml arguments alone, which is how cross-site scripting through message parameters is prevented:
@DefaultMessage("A <strong>{0} error</strong> has occurred: {1}.")
SafeHtml errorHtml(String error, SafeHtml details);
SafeHtml html = msgs.errorHtml("<404>", SafeHtmlUtils.fromSafeConstant("<em>missing</em>"));
errorBar.setHTML(html); // html.asString():
// A <strong><404> error</strong> has occurred: <em>missing</em>.
Rules of the 2.13.1 generator, observed in the probe:
-
a method with a
SafeHtmlargument must returnSafeHtml; aStringresult is refused with[ERROR] Message methods with SafeHtml arguments can only have SafeHtml return type; -
the Developer Guide says that a parameter inside a tag, such as
<span class="{0}">, is not allowed. The 2.13.1 generator accepted it and wrote<span class="a" onclick="alert(1)">tag</span>for the argumenta" onclick="alert(1), so the value was escaped rather than rejected. Do not rely on that: put dynamic values in text positions and choose attribute values from a fixed set; -
translators may change the markup in the properties file (the Spanish
errorHtmlentry keeps the<strong>tag), so review translations ofSafeHtmlmessages like code. The SafeHtml & XSS page describes how to buildSafeHtmlvalues.
For user-supplied data that is not markup, never concatenate it into HTML; use setText or a SafeHtml message.
Dynamic strings: Dictionary
com.google.gwt.i18n.client.Dictionary reads strings that the host page defines as a JavaScript object.
It ignores the locale property completely: the server renders the right strings into the page, and the same compiled application works with any set of translations that exists on the server.
It is meant for adding a GWT module to an application that already has a server-side localization system.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script>
var bookshelfStrings = { title: "Bookshelf", greeting: "Hello" }; // printed by the server, in the user's language
</script>
</head>
<body>
<script src="bookshelf/bookshelf.nocache.js"></script>
</body>
</html>
Dictionary dict = Dictionary.getDictionary("bookshelfStrings");
Window.setTitle(dict.get("title")); // "Bookshelf"
Set<String> keys = dict.keySet(); // [greeting, title]
Collection<String> values = dict.values();
Source: Javadoc — Dictionary
The probe produced the two failure messages (both are `java.util.MissingResourceException`s):
-
an unknown key:
Cannot find 'nope' in Dictionary bookshelfStrings; -
an unknown object:
'noSuchDict' is not a JavaScript object and cannot be used as a Dictionary.
Everything is looked up at run time: there is no type checking, keys cannot be validated by the compiler, the compiler cannot remove unused values and the code is larger and slower than static internationalization.
In return, adding a language does not need a rebuild.
Security notes: the server must emit the object as proper JavaScript (escape quotes, </script> and line separators, or print the strings through a JSON encoder), and the application must treat dictionary values as plain text (setText, a Label) unless the server guarantees they are safe HTML.
Localizable classes
Localizable is the base of Constants and Messages and can also be used directly for locale-sensitive type substitution.
A class that implements com.google.gwt.i18n.client.Localizable (the shared interface of the same name is the one it extends) is replaced at GWT.create time by a subclass chosen by the locale:
| Locale | Class that GWT.create(Type.class) returns |
|---|---|
unspecified |
|
|
|
|
|
public class Salutation implements Localizable {
public String greet(String name) { return "Hello, " + name; }
}
public class Salutation_es extends Salutation {
@Override
public String greet(String name) { return "Hola, " + name; }
}
Salutation s = GWT.create(Salutation.class);
s.greet("Ana"); // "Hello, Ana" in en and ar, "Hola, Ana" in es
Source: Javadoc — Localizable
This is an advanced technique, useful for libraries with complex locale-specific algorithms (a collation, a name formatter, a phone-number format).
Use Messages and the formatters for text and numbers, and reach for Localizable classes only when the logic itself differs per locale.
The substitution is by class name, so the subclass must be on the module’s source path and be named exactly Type_x.
The locale property
The value of locale is chosen in the browser by the property provider that the I18N module registers (LocalePropertyProviderGenerator).
The code it generates looks for a value in a fixed order, takes the first one it finds and then makes it match a compiled locale.
?locale=es_AR"] --> ck["2. cookie
locale.cookie"] ck --> mt["3. meta tag
gwt:property"] mt --> ua["4. browser language
locale.useragent"] ua --> gl["window.__gwt_Locale"] gl --> inh["strip the suffix after the last _
until a compiled locale matches"] inh -- "match" --> perm["load that permutation"] inh -- "no match" --> fb["set-property-fallback locale"] fb --> perm
The first source that yields a value wins, and later sources are not consulted, even if the value turns out to be unknown (a bad query parameter does not fall through to the cookie). The order is configurable and every source can be switched off.
| Property | Default | Meaning |
|---|---|---|
|
|
Name of the query-string parameter. The name must match |
|
empty (disabled) |
Name of the cookie that holds the locale, with the same name rule. Set it to enable the cookie source. |
|
|
Look for |
|
|
Use the browser’s language ( |
|
|
Comma-separated list of the sources to try, in order. An unknown word is ignored with a warning. |
|
none |
Extra locales that are selectable without a new permutation (see [runtime-locales]). |
They are set in the module XML with set-configuration-property:
<set-configuration-property name="locale.cookie" value="bookshelf_locale" />
<set-configuration-property name="locale.useragent" value="Y" />
<set-configuration-property name="locale.searchorder" value="queryparam,cookie,useragent" />
The Bookshelf host page pins English with a meta tag, so a visitor without a cookie or query parameter gets English; every other source overrides it.
All of the following were loaded in headless Chrome against the probe (locales en, es, ar, fallback en, cookie name bookshelf_locale, locale.useragent on):
| Page | LocaleInfo name |
Why |
|---|---|---|
|
|
the query string |
|
|
|
|
|
neither |
|
|
a hyphen is not a separator in the query string: |
a script sets the cookie |
|
the cookie |
|
|
the meta tag |
a meta tag with |
|
the cookie is searched before the meta tag |
the page with the meta tag |
|
the query string beats both |
a page whose meta tag says |
|
the query value |
no value anywhere, on a machine whose browser language is |
|
the user-agent source turned |
a script sets |
|
the last source consulted before the fallback |
The user-agent source is convenient for a first visit and risky as the only source: the browser language is not the language the visitor wants for your site, so store an explicit choice in a cookie and put cookie before useragent in the search order (the default order already does).
Writing the locale from the host page
A server-rendered host page can choose the locale itself, for instance from the user’s profile or from the Accept-Language header, and print it as the meta tag:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bookshelf</title>
<meta name="gwt:property" content="locale=es">
<script src="bookshelf/bookshelf.nocache.js"></script>
</head>
<body>
<noscript>Bookshelf needs JavaScript.</noscript>
</body>
</html>
A meta or cookie value is plain text that the visitor controls, so the server should validate it against the list of supported locales before printing it into the page (and never print it unescaped).
If the module is embedded in an existing page that decides the locale in some other way, write a property provider in the module XML: a JavaScript fragment that returns the value.
The I18N.gwt.xml file shows the standard one (property-provider name="locale" generator=...), and the Developer Guide points at it as the example to follow.
Runtime locales
Sometimes the translations are the same but country-specific details differ: one Spanish translation for all of Latin America, yet Argentinian pesos and Mexican number separators. Compiling a permutation per country would multiply the build, so GWT lets you declare runtime locales: locales that extend a compile-time locale, are chosen at run time and affect only currency data, number formats and date/time formats. Everything else (messages, constants, resources) keeps using the compile-time locale.
<extend-configuration-property name="runtime.locales" value="es_AR" />
<extend-configuration-property name="runtime.locales" value="es_MX" />
<inherits name="com.google.gwt.i18n.CldrLocales"/> adds every locale that GWT knows and that inherits from one of your compile-time locales, which the Showcase sample does, at the price of larger permutations.
With the two lines above (and the compile-time locale es the only translation) the probe printed, for the same code:
?locale= |
Default currency | NumberFormat.getDecimalFormat() of 1234567.891 |
Currency format of 1234.5 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
LocaleInfo.getCurrentLocale().getLocaleName() returned es_AR and es_MX, while the translated message reviews(1234) printed 1.234 reseñas for es_AR and 1,234 reseñas for es_MX: the number inside the message follows the runtime locale, the words follow es.
The Developer Guide lists the caveats: the tables for every runtime locale are included in each permutation (download size grows), the tables for non-obvious inheritance and aliases are too large for the selection script (so control the set of locales you offer, or let the server choose using GwtLocaleFactoryImpl), and only currency, number and date data is affected.
LocaleInfo
com.google.gwt.i18n.client.LocaleInfo answers questions about the running locale and the compiled ones.
| Member | Returns |
|---|---|
|
the instance for the current locale (the only one available) |
|
the name, such as |
|
|
|
the compiled locale names, always including |
|
the name of a locale in its own language: |
|
the values of |
|
|
|
locale data objects for formatting and for names of languages and regions |
Switching language at run time
A language selector therefore stores a choice and reloads the page.
The Bookshelf’s LocaleSwitcher builds its items from LocaleInfo, writes the choice into the cookie named by locale.cookie and reloads; the cookie is read by the bootstrap script on the next load:
public class LocaleSwitcher extends ListBox {
public LocaleSwitcher() {
String current = LocaleInfo.getCurrentLocale().getLocaleName();
for (String name : LocaleInfo.getAvailableLocaleNames()) {
if ("default".equals(name)) {
continue;
}
addItem(LocaleInfo.getLocaleNativeDisplayName(name), name);
if (name.equals(current)) {
setSelectedIndex(getItemCount() - 1);
}
}
addChangeHandler(event -> {
String cookie = LocaleInfo.getLocaleCookieName();
Date expires = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
Cookies.setCookie(cookie, getSelectedValue(), expires, null, "/", false);
Window.Location.reload();
});
}
}
A GWTTestCase in the en permutation confirmed the items en=English and es=español, the selected item en and the cookie name bookshelf_locale.
The sequence in the browser is:
The cookie is not HttpOnly (the page script writes it), so any script in the page can change it; treat it as a preference, never as authorization.
Do not store anything secret in it.
Also pass false as the secure flag only on plain HTTP development servers; in production serve the site over HTTPS and set the flag to true.
The alternative that needs no cookie is a link with ?locale=es (the UrlBuilder from Window.Location.createUrlBuilder() replaces the parameter and keeps the rest), which is also shareable and bookmarkable.
Source: Javadoc — Cookies
Numbers, dates and currencies
GWT does not emulate java.text.DecimalFormat, java.text.DateFormat or java.util.Currency.
It offers NumberFormat, DateTimeFormat, CurrencyList and CurrencyData in com.google.gwt.i18n.client, whose big difference from the JRE is that the locale-specific logic is selected by deferred binding, so only the code for the compiled locale is in each permutation.
The data comes from the Unicode CLDR.
The 2.9.0 release notes (2020-05-13) record "Updates CLDR to version 34" as the latest update; later releases do not mention a new CLDR version, so expect names and patterns of that vintage and use the browser’s Intl object through JsInterop when you need newer data.
The Bookshelf scenario binds Intl.NumberFormat with a few lines (see the JsInterop page).
NumberFormat
NumberFormat is never created with new; a factory method returns a formatter for the current locale (its static get... methods are cached):
| Method | Purpose | 1234567.891 / 1234.5 in en |
|---|---|---|
|
the locale’s decimal format |
|
|
percent ( |
|
|
scientific notation |
|
|
the default currency or the given one |
|
|
the currency symbol only, without disambiguation |
|
|
the locale’s "global" currency pattern, which in |
|
|
a custom pattern |
|
|
static switch: use Latin digits in all new formats |
(see below) |
A pattern uses 0 for a digit that is always shown, # for an optional digit, . for the decimal separator, , for grouping and - for the minus sign; the Developer Guide gives NumberFormat.getFormat("000000.000000").format(12345.6789) as 012345.678900.
An invalid pattern throws IllegalArgumentException.
A pattern formatter is a normal object, so cache it in a static field if you use it repeatedly.
The patterns are not localized: #,##0.00 printed 1,234.50 in en and 1.234,50 in es because the separators are taken from the locale, but the layout stays what you wrote.
parse converts text back to a double using the same locale rules, and it is strict about separators:
| Text | en |
es |
ar |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The silent case in the last row, English parsing 1234,5 as 12345, makes the worst bug: validate user input before trusting it, and catch NumberFormatException.
The Arabic locale formats digits as Arabic-Indic (١٬٢٣٤٫٥٠), so Arabic input must use them too; NumberFormat.setForcedLatinDigits(true) switches all new formats to Latin digits, but in the probe’s ar permutation it printed 1 234,5 (Latin digits, a no-break space as the group separator and a comma as the decimal separator), so check the separators for each target locale.
The Developer Guide page on formatting shows 1,2345.6789 (a misplaced grouping separator) and 1.2345E4 as default outputs; the 2.13.1 en formats printed 1,234,567.891 for 1234567.891 and 1E4 for the scientific format of 12345.6789, so run your own pattern before documenting expected strings.
CurrencyList and CurrencyData
CurrencyList.get() is the list of currencies known to the compiled locale (iterable), getDefault() is the locale’s own currency, and lookup(String code) finds one by ISO 4217 code.
A CurrencyData describes a currency in that locale.
| Accessor | Meaning |
|---|---|
|
the ISO code, such as |
|
the symbol as the locale writes it, the short symbol, and one that is unambiguous across locales |
|
the number of decimals: |
|
where the symbol goes and whether a space is used |
|
the currency is no longer in use |
The probe printed the code, symbol and fraction digits USD $ 2 for the default currency in en, EUR € 2 in es and EGP ج.م. 2 in ar, and 0 digits for JPY.
Currency formatting uses the fraction digits of the currency, not of the locale: getCurrencyFormat("JPY") printed ¥1,235 for 1234.5, while the euro keeps two decimals.
Currency names and symbols are tied to the compiled locale, and with the default locale only a small set of currencies exists, so a code that is not in the list makes the NumberFormat factories throw IllegalArgumentException (the reason the fallback should be a real language).
Never use a double for money in business logic: format amounts for display only, and keep amounts as integers (cents) or BigDecimal on the server.
Source: Javadoc — CurrencyData and Javadoc — CurrencyList
DateTimeFormat
DateTimeFormat formats and parses java.util.Date.
Since the individual getters (getShortDateFormat(), getLongDateFormat(), getMediumDateTimeFormat(), and so on) are deprecated in 2.13.1, use DateTimeFormat.getFormat(PredefinedFormat); the enum covers the full, long, medium and short date, time and date-time formats, ISO_8601, RFC_2822, and fields such as DAY, MONTH_ABBR_DAY, YEAR_MONTH_ABBR and YEAR_QUARTER.
Date d = new Date(1791554707000L); // 2026-10-09T14:05:07Z
TimeZone utc = TimeZone.createTimeZone(0);
DateTimeFormat.getFormat(PredefinedFormat.DATE_FULL).format(d, utc);
DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_MEDIUM).format(d, utc);
DateTimeFormat.getFormat("yyyy-MM-dd").format(d, utc);
DateTimeFormat.getFormat("EEEE d MMMM").format(d, utc);
| Format | en |
es |
ar |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pattern |
|
|
|
pattern |
|
|
|
|
|
the same |
the same |
Three practical points:
-
Month and weekday names follow the locale, but a custom pattern keeps your word order:
EEEE d MMMMprintedviernes 9 octubrein Spanish, without the "de" that the predefined format has. Use a predefined format, or a skeleton in a message ({0,localdatetime,yMMMMd}, which printed9 de octubre de 2026). -
The pattern letters follow the CLDR/ICU table (the Javadoc lists them):
yyear,Mmonth,dday,Eweekday,Hhour 0-23,hhour 1-12,mminute,ssecond,aAM/PM,ztime zone. -
format(date)uses the browser’s time zone;format(date, TimeZone)takes acom.google.gwt.i18n.client.TimeZone, which is what makes the output reproducible in tests (above, UTC). The ISO-8601 format is fixed across locales and is the one to use in text that machines read.
A date written 12/04/07 is December 4th in one locale and April 12th in another; show the user a predefined format and exchange dates between client and server as ISO-8601 strings or epoch milliseconds.
Bidirectional text
Arabic, Hebrew, Persian and Urdu are written right to left (RTL), and a mix of RTL and left-to-right (LTR) text in one paragraph needs care. GWT has three layers:
-
Locale awareness:
LocaleInfo.getCurrentLocale().isRTL()is true for the compiled localear(and false forenandes);LocaleInfo.hasAnyRTL()tells whether any compiled locale is RTL.BidiPolicy.isBidiEnabled()istruewhen at least one compiled locale is RTL, and widgets consult it; the module propertygwt.forceBidi(defaultfalse) can force it on. -
Direction of elements and widgets:
HasDirection.Directionis the enumRTL,LTR,DEFAULT(inherit).com.google.gwt.i18n.client.BidiUtilsreads and writes thedirproperty of a DOM element withgetDirectionOnElementandsetDirectionOnElement.LabelimplementsHasDirectionandHasDirectionalText, so it hassetDirection,getDirection,getTextDirectionandsetText(String, Direction). -
Direction estimation: for text of unknown direction,
setDirectionEstimator(true)on a widget makes it guess from the content (WordCountDirectionEstimatorandFirstStrongDirectionEstimatorare incom.google.gwt.i18n.shared), andBidiFormatterwraps strings inspanor Unicode marks so that, for example, an English title inside an Arabic sentence does not reorder the punctuation.
// Direction of the whole page follows the locale
Direction dir = LocaleInfo.getCurrentLocale().isRTL() ? Direction.RTL : Direction.LTR;
BidiUtils.setDirectionOnElement(Document.get().getBody(), dir);
// A label that works out its own direction from its text
Label title = new Label("שלום עולם");
title.setDirectionEstimator(true); // getTextDirection() is RTL
// Force a direction
Label latin = new Label("Hello");
latin.setDirection(Direction.RTL);
Source: Javadoc — BidiUtils and Javadoc — HasDirection
The probe printed body dir=rtl for ?locale=ar and body dir=ltr for en, and the Hebrew label reported RTL from getTextDirection() and BidiUtils.getDirectionOnElement.
Styles and graphics follow the direction too: CssResource flips left and right properties for RTL locales unless a rule is marked as not flippable (GSS has @noflip, see Styling: CSS & GSS), and @ImageOptions(flipRtl = true) on an ImageResource mirrors arrows and disclosure icons.
LocaleInfo.isRTL() is the decision point for all of them.
Locale-sensitive resources
A ClientBundle method can find a locale-specific file by the same suffix rule as properties: the resource is searched with the compiled locale removed from the right until a match is found.
A bundle method @Source("terms.txt") TextResource terms() with terms.txt and terms_es.txt next to the interface printed Terms of use (default) in en and ar and Condiciones de uso (es) in es.
This works for images (a logo.png and a logo_es.png), text and style files, and it costs one copy per permutation.
The ClientBundle & Images page covers bundles; the point here is that translated artwork needs no code.
Source: Javadoc — ClientBundle
UiBinder templates
UiBinder templates are translated with ui:msg, ui:ph and ui:attribute, and the generator writes a hidden Messages interface per template (everything on this page applies to it except plural forms).
The full syntax and the unclobberable parts of a message are on the UiBinder page (UiBinder), so here only the build workflow, verified on a probe template:
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
ui:generateFormat="com.google.gwt.i18n.server.PropertyCatalogFactory"
ui:generateKeys="com.google.gwt.i18n.server.keygen.MD5KeyGenerator"
ui:generateLocales="default">
<g:HTMLPanel>
<h3 title="Your shelf"><ui:attribute name="title" description="Tooltip of the shelf heading"/>
<ui:msg description="Heading of the shelf card">My <b>shelf</b></ui:msg></h3>
<p><ui:msg description="Counter line">You own <span ui:field="count"/> books on <span ui:ph="brand" class="brand">Bookshelf</span>.</ui:msg></p>
</g:HTMLPanel>
</ui:UiBinder>
-
Compile with the
-extradirectory (the plugin’sextraparameter). The compiler writesextra/probe/com.example.i18n.client.BookCardBinderImplGenMessages.properties, in a folder named after the module’s rename-to name (probe), not the fully-qualified module name the Developer Guide shows. -
Send that file to the translator. It lists the messages under their MD5 keys:
# Description: Counter line # 0 - htmlElement1Begin, Example: <tag> # 1 - htmlElement1End, Example: </tag> # 2 - brandBegin, Example: <tag> # 3 - brandEnd, Example: </tag> 2DCCFF8F065C4909BE4FBF0A5E139574=You own {0}{1} books on {2}Bookshelf{3}. -
Put each translated file next to the
ui.xml, named after the generated class without its package:BookCardBinderImplGenMessages_es.properties.
With the Spanish file in place, the probe rendered <h3 title="Tu estantería"> Mi <b>estantería</b></h3> <p>Tienes <span>3</span> libros en <span class="brand">Bookshelf</span>.</p> for ?locale=es and the English text otherwise.
Besides the ui: attributes the page uses the unprefixed name and description on ui:attribute; the prefixed spelling still compiles but warns Deprecated prefix "ui:" on "ui:name". Use "name" instead..
Bookshelf templates that only display text use the simpler route: declare the application’s Messages with ui:with and read it in an expression, as the shell does with <g:Label text="{msg.appTitle}" styleName="{res.css.header}" />.
Tools: i18nCreator and I18NSync
Two command-line tools help when the translations already exist as properties files, or when you want a first properties file for a new interface.
i18nCreator is the command documented in the Developer Guide (a script of the SDK distribution); its main class is com.google.gwt.i18n.tools.I18NCreator.
It creates a sample properties file and a script, and I18NSync (com.google.gwt.i18n.tools.I18NSync) generates or refreshes the Java interface from the properties file, so you only maintain the properties.
The Developer Guide lists i18nCreator among the command-line tools.
The classes are in gwt-dev, so with Maven you run them with the same classpath as the compiler.
The command below, copied from the probe, created a Constants interface scaffold:
$ java -cp "$CP" com.google.gwt.i18n.tools.I18NCreator -out . com.example.foo.client.FooConstants
Created directory ./src
Created directory ./src/com/example/foo/client
Created file ./src/com/example/foo/client/FooConstants.properties
Created file ./FooConstants-i18n
Its options are -eclipse projectName (also writes an Eclipse launch configuration), -out dir, -[no]overwriteFiles, -[no]ignoreExistingFiles, -[no]createConstantsWithLookup and -[no]createMessages, and the interface name must be fully qualified and not nested.
The generated script FooConstants-i18n is a shell script that runs I18NSync with hard-coded paths gwt-user.jar and gwt-dev.jar; those names are those of the SDK zip, so the script does not work against the versioned jars of a Maven repository (gwt-user-2.13.1.jar). Run I18NSync yourself instead.
After the properties file FooConstants.properties contained helloWorld=hello, world and goodbyeWorld=goodbye, world:
$ java -cp "src:$CP" com.google.gwt.i18n.tools.I18NSync -out src com.example.foo.client.FooConstants
public interface FooConstants extends com.google.gwt.i18n.client.Constants {
@DefaultStringValue("goodbye, world")
@Key("goodbyeWorld")
String goodbyeWorld();
@DefaultStringValue("hello, world")
@Key("helloWorld")
String helloWorld();
}
With -createMessages, the properties file greeting=Hello {0}, you have {1} new books produced @DefaultMessage("Hello {0}, you have {1} new books") @Key("greeting") String greeting(String arg0, String arg1);.
Notice the generic arg0 parameter names and the alphabetical order; rename the parameters after generation (or keep the generator in the loop and never edit the file).
Run I18NSync again whenever the properties file gets a new key.
The tool is old and its template mentions Eclipse launch files, so for new code the annotations are the maintained workflow: write the interface with @DefaultMessage, and let the compiler emit a catalog for translators with @Generate:
@Generate(format = "com.google.gwt.i18n.server.PropertyCatalogFactory", locales = {"default"})
@GenerateKeys("com.google.gwt.i18n.server.keygen.MD5KeyGenerator")
public interface ShelfMessages extends Messages {
@DefaultMessage("Add to shelf")
@Description("Label of the button that adds a book to the shelf")
String addToShelf();
@DefaultMessage("{0} of {1}")
@Meaning("pagination")
String pageOf(int page, int total);
}
The compile with -extra wrote extra/probe/com.example.i18n.client.ShelfMessages.properties:
# Messages from com.example.i18n.client.ShelfMessages
# Source locale en
# Description: Label of the button that adds a book to the shelf
A26F76642A10E4777E724D98AFA3CB0C=Add to shelf
# Meaning: pagination
# 0 - page
# 1 - total
E9D60F341E9E0A36DC5F7817690CCB0D={0} of {1}
If you generate keys with MD5, the translations survive moving a message between classes, and identical texts share one translation; if you need stable human-readable keys, use @Key or the default method-name keys.
The tutorial of the Developer Guide (StockWatcher) uses the same interfaces and properties and adds one thing worth copying: it moves the page heading from the static host page into a Label so that it can be translated.
Worked example: the Bookshelf in English and Spanish
The scenario’s catalogue shows the number of copies and of reviews per book, labels, a language selector and formatted numbers and dates.
Everything below was compiled in the three-module Bookshelf (bookshelf-shared, bookshelf-client, bookshelf-server) with mvn -B package on the client module, which printed Compiling 4 permutations, and the English and Spanish messages were checked by GWTTestCase classes.
1. The module. Two locales, no default, English as the fallback, the cookie enabled:
<extend-property name="locale" values="en,es" />
<set-property name="locale" value="en,es" />
<set-property-fallback name="locale" value="en" />
<set-configuration-property name="locale.cookie" value="bookshelf_locale" />
2. The interface. Defaults are the English text, so a reader of the code sees the real strings. It keeps the Bookshelf’s existing messages and adds the review count:
public interface AppMessages extends Messages {
@DefaultMessage("Bookshelf")
String appTitle();
@DefaultMessage("Catalogue")
String catalogue();
@DefaultMessage("{0} copies")
@AlternateMessage({"=0", "No copies", "one", "{0} copy"})
String copies(@PluralCount int count);
@DefaultMessage("{0,number} reviews")
@AlternateMessage({"=0", "No reviews yet", "one", "{0,number} review"})
@Description("Number of reader reviews of a book, shown under its title")
String reviews(@PluralCount int count);
@DefaultMessage("{0} by {1}")
String bookLine(String title, String author);
@DefaultMessage("Saved \"{0}\"")
String saved(String title);
}
3. The properties files, both UTF-8, in bookshelf-client/src/main/resources/com/example/bookshelf/client/i18n/:
# AppMessages_en.properties
# English uses the defaults from @DefaultMessage; this file makes GWT generate an "en" implementation with the English plural rule.
# AppMessages_es.properties
appTitle=Estantería
catalogue=Catálogo
copies={0} ejemplares
copies[\=0]=Sin ejemplares
copies[one]={0} ejemplar
reviews={0,number} reseñas
reviews[\=0]=Aún no hay reseñas
reviews[one]={0,number} reseña
bookLine={0} de {1}
saved=Guardado «{0}»
4. The host page declares UTF-8 and pins English as the server default:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Bookshelf</title>
<meta name="gwt:property" content="locale=en">
<script src="bookshelf/bookshelf.nocache.js"></script>
</head>
<body>
<noscript>Bookshelf needs JavaScript.</noscript>
</body>
</html>
5. Use. The catalogue column and the shell read the same interface; UiBinder reads it as {msg.appTitle} (a ui:with field of the type AppMessages), Java code uses GWT.create:
private final AppMessages msg = GWT.create(AppMessages.class);
new TextColumn<Book>() {
@Override
public String getValue(Book b) { return msg.copies(b.getCopies()); }
};
6. Tests. A test class per language, the Spanish module pinning locale to es (the AppEsTest module of the scenario sets set-property locale es and a single user.agent, which keeps the test compile to one permutation):
public class EnglishMessagesGwtTest extends GWTTestCase {
@Override
public String getModuleName() { return "com.example.bookshelf.AppTest"; }
public void testEnglishPlurals() {
assertEquals("en", LocaleInfo.getCurrentLocale().getLocaleName());
AppMessages msg = GWT.create(AppMessages.class);
assertEquals("No reviews yet", msg.reviews(0));
assertEquals("1 review", msg.reviews(1));
assertEquals("2 reviews", msg.reviews(2));
assertEquals("1,234 reviews", msg.reviews(1234));
}
}
public class SpanishMessagesGwtTest extends GWTTestCase {
@Override
public String getModuleName() { return "com.example.bookshelf.AppEsTest"; }
public void testSpanishPlurals() {
AppMessages msg = GWT.create(AppMessages.class);
assertEquals("Aún no hay reseñas", msg.reviews(0));
assertEquals("1 reseña", msg.reviews(1));
assertEquals("1.234 reseñas", msg.reviews(1234));
}
}
Both ran green with mvn -B -o -pl bookshelf-client -am test -Dtest='EnglishMessagesGwtTest,SpanishMessagesGwtTest'.
Removing AppMessages_en.properties made the English test fail with expected: <1 review>, actual: <1 reviews> and the compiler print the plural warning of the pitfalls table.
7. Running it. The compiled application, served from a static directory, rendered the header, navigation and catalogue labels in English without parameters and in Spanish with ?locale=es:
| URL | Header and navigation | copies(0) and copies(1) |
|---|---|---|
|
|
|
|
|
|
The server part of the Bookshelf is untouched by any of this: the REST and RPC services return data (ids, titles, copies, review counts), never translated text, and the client formats it.
The one place where the server must care is text that it produces for users (e-mails, PDF exports, error messages), which needs its own localization, for instance a ResourceBundle or Spring’s MessageSource.
Pitfalls
The messages below were printed by the 2.13.1 compiler for the probe module with one deliberately broken change per row, or by the JavaScript the compiler produced.
Compiler errors of the generator are normally followed by a second, generic line that is the real failure of the build: [ERROR] Line 48: Failed to resolve 'com.example.i18n.client.BookMessages' via deferred binding, because the GWT.create could not complete.
Search the log for the first [ERROR] above it.
| Symptom (message as printed) | Cause | Fix |
|---|---|---|
|
The English plural forms are in the annotations only. Without |
Add |
|
A translation uses more placeholders than the method has parameters. |
Fix the translation or add the parameter. |
|
A translation omits a parameter that the method declares. |
Add the placeholder, or mark the parameter |
|
A method has no |
Add the default annotation or the key to the suffix-less file. |
|
A |
Correct the value in the properties file. |
|
A single apostrophe in a message. |
Write |
|
|
Use |
|
A |
Return |
|
A translation uses a plural form the language’s rule does not have. |
Use only the forms of that language (see the Plural Forms section). |
|
A locale has no translation for a message with plural forms. |
Add the forms to the |
|
|
Check the name, or catch the exception for data-driven keys. |
|
A key that the host page object lacks, or a missing/typo’d host variable. |
Define the object before the bootstrap script and validate keys. |
|
|
Parse with the user’s locale, catch |
No message at all; Spanish shows English |
The |
Move it; the matching is silent. |
No message at all; |
The properties file is saved as ISO-8859-1 but read as UTF-8. |
Save as UTF-8, or use |
A |
The Developer Guide shows a single backslash, but the properties loader consumes one, so the value reaches GWT as a bare comma. |
Write two backslashes in the file: |
|
The query value uses a hyphen; GWT locales use underscores. |
|
|
|
Declare |
|
The first source with a value wins, even if it matches nothing. |
Validate the value in the page that links to the application. |
Arabic numbers in an Arabic-locale page whose server expects |
|
|
Recent changes and the outdated material
| Version | Change |
|---|---|
2.9.0 (2020-05-13) |
CLDR data updated to version 34, the latest CLDR update in the release notes. |
2.10 to 2.13.1 |
No internationalization entry in the release notes; the generator, the formatters and the locale provider described on this page are those of the 2.13.1 tag. |
2.13.1 source |
The individual |
-
The Manning book (Hanson and Tacy, 2007, GWT 1.3 and 1.4) covers
Constantsand thelocaleproperty in its third chapter andConstants,ConstantsWithLookup,Messages,LocalizableandDictionaryin its fifteenth. Its techniques still exist and work the same way, but there are no annotations (the book keeps every default in a properties file), no plural or select forms, noSafeHtmlmessages, no runtime locales, nolocale.cookie, noLocaleInfo-based switching and no UiBinder i18n. Its project tooling (applicationCreator, Eclipse.launchfiles,-i18nscripts withGWTCompiler) is gone: use Maven andI18NSyncfrom the classpath. -
The official pages. The Developer Guide describes
@Generate(String[] formatFQCN, ...)where the element isformat; it shows a single-backslash comma escape that does not survive the properties loader (use\\,); it prints formatting examples (1,2345.6789,1.2345E4,Dec 18, 2007) that no longer match what the formatters print; its tutorial uses HTML entities in properties files; and some UiBinder i18n samples use theui:prefix on attributes and the older placeholder names. The compiled code on this page is the reference. -
Packages.
com.google.gwt.i18n.sharedcontains the types that are safe to use on the server and in shared code (DateTimeFormat,Localizable,BidiFormatter, the direction estimators,GwtLocale), whilecom.google.gwt.i18n.clientis client-only. Theorg.gwtproject.*modules offer J2CL-ready replacements for parts of this API; see J2CL & the Future.
The same idea in other stacks
Every web stack externalizes text into per-language resources, selects a locale per request or per user, and formats numbers and dates by locale; GWT’s twist is that all of it is resolved at compile time.
-
Thymeleaf internationalization: message expressions, where the texts come from and how the locale is resolved, the server-rendered counterpart of
Messagesand the locale property. -
ASP.NET Core globalization and localization:
IStringLocalizer,.resxconventions and the request-localization culture providers, comparable to the query string, cookie and browser-language sources of the GWT locale provider. -
Next.js internationalization: locale routing strategies, dictionaries per locale in server components and
next-intl, the JavaScript-framework counterpart.