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.

flowchart LR iface["AppMessages.java
@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"]

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 setLocale call; the page changes language by reloading with a different query parameter, cookie or meta value (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.

Table 1. Internationalization techniques at a glance
Technique Use it for Checked Cost

Constants

Typed values loaded from properties: String, String[], int, float, double, boolean and Map. Labels, menu texts, configuration such as a page size.

Compile time (a value of the wrong type is a compile error).

Smallest: unused accessors are removed.

ConstantsWithLookup

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 MissingResourceException.

Larger: the compiler cannot discard unused accessors and the lookup cannot be resolved at compile time.

Messages

Text with parameters, plural forms, select forms, number, date and list formatting, SafeHtml results. Everything a user reads that is built from data.

Compile time (number and use of arguments).

Small; the compiler can inline a message like a string concatenation.

Dictionary

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.

Localizable classes

A whole algorithm or class that differs per locale, selected by name (Type_es).

Compile time (it is ordinary Java).

Only the matching class is in each permutation.

ui:msg in UiBinder

Text that is part of a UiBinder template (see [uibinder-i18n]).

Compile time.

The same as Messages: the generator writes a hidden Messages interface.

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 Messages for text and Constants for 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 Constants with 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:

  1. extend-property adds the values. values takes a comma-separated list, so one element can add several locales.

  2. set-property with the same list restricts the permutations to the listed values, which removes the default permutation.

  3. set-property-fallback names 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:

Table 2. Which properties file answers for an interface org.example.foo.Intf
Compiled locale File that is tried, in order

unspecified or default

org/example/foo/Intf.properties

x (for example es)

Intf_es.properties, then Intf.properties

x_Y (for example es_AR)

Intf_es_AR.properties, then Intf_es.properties, then Intf.properties

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:

  1. the properties files are saved as UTF-8 (set the encoding in the IDE, and project.build.sourceEncoding to UTF-8 in the parent POM, as the Bookshelf does, so resource copying does not re-encode them);

  2. the Java sources are UTF-8 for the same reason (the compiler plugin takes the same property);

  3. the host page declares <meta charset="utf-8"> in its head and 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.

Table 3. Constants accessors, annotations and property syntax
Return type Default-value annotation Property value is read as

String

@DefaultStringValue("text")

a plain string

String[]

@DefaultStringArrayValue({"a", "b"})

comma-separated; a comma inside a value is written \\, (two backslashes, see the pitfalls)

int

@DefaultIntValue(25)

an int, checked at compile time

float

@DefaultFloatValue(1.5f)

a float, checked at compile time

double

@DefaultDoubleValue(0.21)

a double, checked at compile time

boolean

@DefaultBooleanValue(true)

true or false, checked at compile time

Map<String, String> (or a raw Map)

@DefaultStringMapValue({"k1", "v1", "k2", "v2"}) (an even number of strings)

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:

Table 4. Output of the BookConstants probe
Accessor ?locale=en ?locale=es

appName()

Bookshelf

Estantería

pageSize(), vatRate()

25, 0.21

10, 0.1

showCovers()

true

true (no override in the Spanish file)

categories()

3 elements: Fiction, Science, History

3 elements: Ficción, Ciencia and Historia, política

languages()

{en=English, es=Spanish}

{en=Inglés, es=Español}

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. for pageSize=diez.

  • A Map accessor 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 key in 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.

Table 5. Message placeholder formats
Placeholder Meaning Output in the probe (en / es)

{0}

the argument converted to a string with Java concatenation (null becomes null)

Dune

{0,number}

the locale’s decimal format

1,234 / 1.234

{0,number,currency}

the locale’s default currency format

$1,234.50 / 1.234,50 €

{0,number:curcode=EUR,currency}

currency format with an explicit currency code (an extra argument after a colon)

€9.50 / 9,50 €

{0,number,#,##0.00}

a custom pattern (you are now responsible for localizing it)

as for NumberFormat.getFormat with the same pattern (see the formatting section)

{0,date,medium} (and date/time with short, medium, long, full)

the JRE-style predefined date and time formats

Oct 9, 2026 / 9 oct. 2026

{0,localdatetime,predef:DATE_MEDIUM}

a locale-specific predefined format by DateTimeFormat.PredefinedFormat name

Oct 9, 2026 / 9 oct. 2026

{0,localdatetime,yMMMMd}

a skeleton: the fields you want, in any order; the locale supplies the pattern

October 9, 2026 / 9 de octubre de 2026

{0,list}

a List or array formatted with the locale’s separators; {0,list,number} formats each element as a number

a, b, and c / a, b y c

{startBold,<b>}

a static argument: shown to translators as the placeholder startBold, emitted as the text after the comma

<b> in both locales

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 a TimeZone tz parameter. 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 printed currency:curcode=EUR10 for 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>}") printed Welcome back, <b>Ana</b>. The result is still a String; if it is shown in an HTML context, use a SafeHtml message.

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.

Table 6. Annotations on the interface
Annotation Purpose

@DefaultLocale("es")

The locale of the text in the source. The default is en. Use it when the application’s main language is not English; the English strings then live in _en files.

@GenerateKeys

Generate the keys of all methods with a key generator: the default is the MD5 generator com.google.gwt.i18n.server.keygen.MD5KeyGenerator; also available are MethodNameKeyGenerator and FullyQualifiedMethodNameKeyGenerator in the same package. Without the annotation the key is the method name.

@Generate(format = ..., fileName = ..., locales = ...)

Write a message catalog for translators during the build (see [tools]). format is the class name of a MessageCatalogFactory, for example com.google.gwt.i18n.server.PropertyCatalogFactory. The older rebind.format.MessageCatalogFormat still works but is deprecated.

@GeneratedFrom("file")

Marks an interface as generated from a file (documentation only; the name is not checked).

Table 7. Annotations on methods
Annotation Purpose

@DefaultMessage("text")

The text for the default locale. With @AlternateMessage present it is the text for every form that has no specific entry (in English, the plural).

@AlternateMessage({"form", "text", ...})

Pairs of a form name and its text for the default locale: plural forms (one, =0) or select values (MALE).

@Key("reviews.header")

The key in the external format. Without it, the key comes from @GenerateKeys or the method name.

@Description("...")

A note for the translator. It is not part of an MD5 key, and depending on the format the translator may not see it.

@Meaning("the fruit")

Tells identical texts apart for translation (two methods with the same text and different meanings should have different keys).

@PluralText

Deprecated: use @AlternateMessage.

Table 8. Annotations on parameters
Annotation Purpose

@PluralCount

Marks the parameter that selects the plural form. It must be int, short, an array or a List (the length is used). Anything else is an error (see the pitfalls table).

@Offset(n)

With @PluralCount: subtract n before the plural rule is applied (exact =N matches are tested before the offset).

@Select

Marks the parameter that selects an alternate message: an integral primitive, a String, a boolean or an enum.

@Example("John")

An example value, shown to translators instead of {0}.

@Optional

This parameter need not appear in every translation. Without it, a translation that omits a parameter is a compile error.

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:

Table 9. Plural form output of the reviews message
Count English Spanish

0

No reviews yet

Aún no hay reseñas

1

1 review

1 reseña

2

2 reviews

2 reseñas

1234

1,234 reviews

1.234 reseñas

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>&lt;404&gt; error</strong> has occurred: <em>missing</em>.

Rules of the 2.13.1 generator, observed in the probe:

  • a method with a SafeHtml argument must return SafeHtml; a String result 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&quot; onclick=&quot;alert(1)">tag</span> for the argument a" 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 errorHtml entry keeps the <strong> tag), so review translations of SafeHtml messages like code. The SafeHtml & XSS page describes how to build SafeHtml values.

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();

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:

Table 10. Substitution rule for a class Type that implements Localizable
Locale Class that GWT.create(Type.class) returns

unspecified

Type itself, or Type_ if Type is an interface

x

Type_x if it exists, otherwise as if the locale were unspecified

x_Y

Type_x_Y if it exists, otherwise as if the locale were x

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

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.

flowchart LR qp["1. query string
?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.

Table 11. Configuration properties of the locale provider
Property Default Meaning

locale.queryparam

locale

Name of the query-string parameter. The name must match [A-Za-z][A-Za-z0-9_]*; an empty value disables the source.

locale.cookie

empty (disabled)

Name of the cookie that holds the locale, with the same name rule. Set it to enable the cookie source.

locale.usemeta

Y

Look for <meta name="gwt:property" content="locale=x_Y"> in the host page. Accepts y, yes, true, on (any case); anything else is false.

locale.useragent

N

Use the browser’s language (navigator.language, with - turned into _ and the region upper-cased). Off by default.

locale.searchorder

queryparam,cookie,meta,useragent

Comma-separated list of the sources to try, in order. An unknown word is ignored with a warning.

runtime.locales

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):

Table 12. What the property provider resolved
Page LocaleInfo name Why

index.html?locale=es

es

the query string

index.html?locale=es_AR

es

es_AR is not compiled, so the suffix is stripped and es matches

index.html?locale=fr_FR

en

neither fr_FR nor fr exists: the fallback applies

index.html?locale=es-ES

en

a hyphen is not a separator in the query string: es-ES is looked up literally and does not match

a script sets the cookie bookshelf_locale=ar before the bootstrap script

ar

the cookie

<meta name="gwt:property" content="locale=ar">

ar

the meta tag

a meta tag with en and the cookie ar

ar

the cookie is searched before the meta tag

the page with the meta tag en and the cookie ar, opened with ?locale=es

es

the query string beats both

a page whose meta tag says ar, opened with ?locale=zz

en

the query value zz is consulted first, matches nothing and the fallback answers; the meta tag is never looked at

no value anywhere, on a machine whose browser language is es-ES

es

the user-agent source turned es-ES into es_ES and the suffix was stripped

a script sets window.__gwt_Locale = 'es' before the bootstrap script

es

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:

Table 13. Runtime locales over the compile-time locale es
?locale= Default currency NumberFormat.getDecimalFormat() of 1234567.891 Currency format of 1234.5

es

EUR

1.234.567,891

1.234,50 €

es_AR

ARS

1.234.567,891

1.234,50 $

es_MX

MXN

1,234,567.891

$1,234.50

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.

Table 14. LocaleInfo API
Member Returns

LocaleInfo.getCurrentLocale()

the instance for the current locale (the only one available)

getLocaleName()

the name, such as es or es_AR (default for the language-less locale)

isRTL()

true if the current locale is written right to left

static getAvailableLocaleNames()

the compiled locale names, always including default (the probe printed [default, ar, en, es])

static getLocaleNativeDisplayName(String)

the name of a locale in its own language: español, English, العربية; null for unknown names (the probe printed undefined for default, so skip it)

static getLocaleQueryParam(), static getLocaleCookieName()

the values of locale.queryparam and locale.cookie (locale and bookshelf_locale in the probe)

static hasAnyRTL()

true if any locale compiled into the application is right to left

getDateTimeFormatInfo(), getNumberConstants(), getLocalizedNames()

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:

sequenceDiagram participant U as User participant S as LocaleSwitcher participant B as Browser participant N as nocache.js U->>S: picks Español S->>B: Cookies.setCookie(bookshelf_locale, es) S->>B: Window.Location.reload() B->>N: loads the host page again N->>N: query string, then cookie finds es N->>B: loads the es permutation B->>U: Bookshelf in Spanish

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.

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):

Table 15. NumberFormat factory methods
Method Purpose 1234567.891 / 1234.5 in en

getDecimalFormat()

the locale’s decimal format

1,234,567.891

getPercentFormat()

percent (0.256 becomes a percentage)

26%

getScientificFormat()

scientific notation

1E4 for 12345.6789

getCurrencyFormat(), getCurrencyFormat(String code), getCurrencyFormat(CurrencyData)

the default currency or the given one

$1,234.50; getCurrencyFormat("EUR") gives €1,234.50

getSimpleCurrencyFormat(...)

the currency symbol only, without disambiguation

€1,234.50

getGlobalCurrencyFormat(...)

the locale’s "global" currency pattern, which in en appends the code, for text that mixes currencies

€1,234.50 EUR

getFormat(String pattern), getFormat(String pattern, String currencyCode)

a custom pattern

#,##0.00 gives 1,234.50

setForcedLatinDigits(boolean)

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:

Table 16. NumberFormat.getDecimalFormat().parse(text)
Text en es ar

1,234.5

1234.5

NumberFormatException: 1,234.5

NumberFormatException: 1,234.5

1.234,5

NumberFormatException: 1.234,5

1234.5

NumberFormatException: 1.234,5

1234,5

12345 (the comma is a grouping separator)

1234.5

NumberFormatException: 1234,5

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.

Table 17. CurrencyData accessors
Accessor Meaning

getCurrencyCode()

the ISO code, such as EUR

getCurrencySymbol(), getSimpleCurrencySymbol(), getPortableCurrencySymbol()

the symbol as the locale writes it, the short symbol, and one that is unambiguous across locales

getDefaultFractionDigits()

the number of decimals: 2 for EUR, 0 for JPY

isSymbolPrefix(), isSymbolPositionFixed(), isSpaceForced(), isSpacingFixed()

where the symbol goes and whether a space is used

isDeprecated()

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.

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);
Table 18. The same Date in three locales (UTC)
Format en es ar

DATE_FULL

Friday, October 9, 2026

viernes, 9 de octubre de 2026

الجمعة، 9 أكتوبر 2026

DATE_MEDIUM

Oct 9, 2026

9 oct. 2026

09‏/10‏/2026

TIME_SHORT

2:05 PM

14:05

2:05 م

DATE_TIME_MEDIUM

Oct 9, 2026, 2:05:07 PM

9 oct. 2026 14:05:07

09‏/10‏/2026 2:05:07 م

YEAR_MONTH_ABBR

Oct 2026

oct. 2026

أكتوبر 2026

pattern yyyy-MM-dd

2026-10-09

2026-10-09

2026-10-09

pattern EEEE d MMMM

Friday 9 October

viernes 9 octubre

الجمعة 9 أكتوبر

ISO_8601

2026-10-09T14:05:07.000+00:00

the same

the same

Three practical points:

  • Month and weekday names follow the locale, but a custom pattern keeps your word order: EEEE d MMMM printed viernes 9 octubre in Spanish, without the "de" that the predefined format has. Use a predefined format, or a skeleton in a message ({0,localdatetime,yMMMMd}, which printed 9 de octubre de 2026).

  • The pattern letters follow the CLDR/ICU table (the Javadoc lists them): y year, M month, d day, E weekday, H hour 0-23, h hour 1-12, m minute, s second, a AM/PM, z time zone.

  • format(date) uses the browser’s time zone; format(date, TimeZone) takes a com.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 locale ar (and false for en and es); LocaleInfo.hasAnyRTL() tells whether any compiled locale is RTL. BidiPolicy.isBidiEnabled() is true when at least one compiled locale is RTL, and widgets consult it; the module property gwt.forceBidi (default false) can force it on.

  • Direction of elements and widgets: HasDirection.Direction is the enum RTL, LTR, DEFAULT (inherit). com.google.gwt.i18n.client.BidiUtils reads and writes the dir property of a DOM element with getDirectionOnElement and setDirectionOnElement. Label implements HasDirection and HasDirectionalText, so it has setDirection, getDirection, getTextDirection and setText(String, Direction).

  • Direction estimation: for text of unknown direction, setDirectionEstimator(true) on a widget makes it guess from the content (WordCountDirectionEstimator and FirstStrongDirectionEstimator are in com.google.gwt.i18n.shared), and BidiFormatter wraps strings in span or 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);

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.

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>
  1. Compile with the -extra directory (the plugin’s extra parameter). The compiler writes extra/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.

  2. 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}.
  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:

Table 19. The texts of the compiled Bookshelf (header and navigation from the DOM, counts from the tests)
URL Header and navigation copies(0) and copies(1)

index.html

Bookshelf, Catalogue, Admin

No copies, 1 copy

index.html?locale=es

Estantería, Catálogo, Administración

Sin ejemplares, 1 ejemplar

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

[WARN] Plural form 'one' unknown in com.google.gwt.i18n.client.impl.plurals.DefaultRule: ignoring, then 1 reviews at run time (test: expected: <1 review>, actual: <1 reviews>)

The English plural forms are in the annotations only. Without _en.properties the English class is generated with the language-less rule, which has no one.

Add AppMessages_en.properties (even with only a comment). Remove the default locale from the module.

[ERROR] Argument 2 beyond range of arguments: {0} de {1} ({2})

A translation uses more placeholders than the method has parameters.

Fix the translation or add the parameter.

[ERROR] Required argument 1 not present: {0} de autor

A translation omits a parameter that the method declares.

Add the placeholder, or mark the parameter @Optional.

[ERROR] No resource found for key 'reviews.header' followed by a MissingResourceException stack trace

A method has no @DefaultMessage (or default value annotation) and the properties files do not define its key.

Add the default annotation or the key to the suffix-less file.

[ERROR] diez could not be parsed as a number.

A Constants property is not a valid int, float or double.

Correct the value in the properties file.

[ERROR] Error parsing 'It's {0}'

A single apostrophe in a message.

Write It''s {0}.

[ERROR] reviews: PluralCount parameter must be int, short, array, or List

@PluralCount on a double or another type.

Use int, short, an array or a List.

[ERROR] Message methods with SafeHtml arguments can only have SafeHtml return type

A Messages method takes a SafeHtml parameter and returns String.

Return SafeHtml.

[WARN] Plural form 'few' unknown in com.google.gwt.i18n.client.impl.plurals.DefaultRule_es: ignoring

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).

[WARN] In locale 'ar', required plural forms are missing: [few, none, many, two]

A locale has no translation for a message with plural forms.

Add the forms to the _ar file, or drop the locale.

java.util.MissingResourceException: Cannot find constant 'poetry'; expecting a method name

ConstantsWithLookup.getString with a name that is not an accessor.

Check the name, or catch the exception for data-driven keys.

Cannot find 'nope' in Dictionary bookshelfStrings and 'noSuchDict' is not a JavaScript object and cannot be used as a Dictionary

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.

Uncaught Error: java.lang.NumberFormatException: 1.234,5 (console, in the en permutation)

NumberFormat.parse with separators of another locale.

Parse with the user’s locale, catch NumberFormatException, and exchange numbers with the server as plain double/ISO text.

No message at all; Spanish shows English

The _es.properties file is in another package than the interface, or its suffix is wrong (it must be Interface_es.properties in the same package).

Move it; the matching is silent.

No message at all; Estanter�a

The properties file is saved as ISO-8859-1 but read as UTF-8.

Save as UTF-8, or use \u00ed escapes; keep project.build.sourceEncoding at UTF-8.

A String[] with a comma in a value has one element too many (Historia\, política gave four elements, not three)

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: Historia\\, política.

?locale=es-ES shows English

The query value uses a hyphen; GWT locales use underscores.

?locale=es_ES. The browser-language source converts the hyphen for you; a query parameter is not converted.

?locale=es_AR shows the number and currency formats of es (euro, not peso)

es_AR is not compiled, so the suffix is stripped to es.

Declare runtime.locales for country-specific number, currency and date data (see the runtime locales section), or compile the locale.

?locale=zz ignores the cookie

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 1234.5

ar formats digits as Arabic-Indic.

NumberFormat.setForcedLatinDigits(true) for formatting, and send numbers to the server as double or ISO text.

Recent changes and the outdated material

Table 20. What changed since the older 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 DateTimeFormat getters (getShortDateFormat() and the like) are @Deprecated in favour of getFormat(PredefinedFormat), @PluralText is deprecated in favour of @AlternateMessage, and MessageCatalogFormat is deprecated in favour of MessageCatalogFactory.

  • The Manning book (Hanson and Tacy, 2007, GWT 1.3 and 1.4) covers Constants and the locale property in its third chapter and Constants, ConstantsWithLookup, Messages, Localizable and Dictionary in 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, no SafeHtml messages, no runtime locales, no locale.cookie, no LocaleInfo-based switching and no UiBinder i18n. Its project tooling (applicationCreator, Eclipse .launch files, -i18n scripts with GWTCompiler) is gone: use Maven and I18NSync from the classpath.

  • The official pages. The Developer Guide describes @Generate(String[] formatFQCN, ...) where the element is format; 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 the ui: prefix on attributes and the older placeholder names. The compiled code on this page is the reference.

  • Packages. com.google.gwt.i18n.shared contains the types that are safe to use on the server and in shared code (DateTimeFormat, Localizable, BidiFormatter, the direction estimators, GwtLocale), while com.google.gwt.i18n.client is client-only. The org.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 Messages and the locale property.

  • ASP.NET Core globalization and localization: IStringLocalizer, .resx conventions 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.