Messages and 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 Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra
4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. Faces 4.0 reads the same on every point below as far as
the specification text goes (nothing in the 4.1 changes touches messages or locales), but the examples were not run on
4.0. Faces 5.0 is still a milestone (5.0.0-M9 on 2026-09-18) and is described only as upcoming. Every example and
every statement about the output was run in a scratch Bookshelf WAR served by Tomcat 11.0.26 with Weld 6.0.4.Final
(Java 26 as the runtime), once with Mojarra and once with MyFaces, using curl with different Accept-Language
headers (checked on 2026-10-08). Where the two implementations disagree, the text says which one does what.
A web application talks to people in two ways: it reports what happened (a field is missing, a review was saved), and
it presents its text, numbers and dates in the reader’s language. Faces covers both with the same machinery. A
FacesMessage carries a report from the Java code to the page; a ResourceBundle carries the text of the page
itself; and one active locale, stored on the view, decides which language and which number and date conventions both
use. This page walks through each piece with the Bookshelf review form in English and Spanish.
Where Messages and Locales Sit in the Lifecycle
Nothing here has a phase of its own; it hangs off the phases described on the request processing lifecycle page:
-
Restore View (or the initial build) fixes the view’s locale. The
ViewHandlerpicks it when it creates the view, from the browser’s preferences and the locales the application supports, and thelocaleattribute off:viewcan override it. -
Process Validations is where most messages are born. A converter that cannot convert, a
requiredinput that is empty or a validator that rejects the value queues aFacesMessageon theFacesContext, and the lifecycle then jumps straight to Render Response without touching the model. -
Invoke Application is where your action method adds its own messages (
Review saved,Already reviewed). -
Render Response turns the queued messages into markup (
h:message,h:messages), pulls view text out of the resource bundle for the view’s locale, and formats numbers and dates with that same locale.
FacesMessage: Severity, Summary and Detail
A jakarta.faces.application.FacesMessage has three parts: a severity, a summary (the short text) and a detail
(the longer explanation). The severity is one of four constants in ascending order: SEVERITY_INFO,
SEVERITY_WARN, SEVERITY_ERROR and SEVERITY_FATAL. Constructing a message without a severity gives
INFO, and getDetail() returns the summary when no detail was set, so a message built with null as the detail
still has something to show in places that display the detail.
Since Faces 4.1 a message can be compared. FacesMessage gained equals(), hashCode() and toString() (issue 1823 in the
specification’s change log; the Javadoc marks the three methods Since: 4.1), and FacesMessage.VALUES and
FacesMessage.VALUES_MAP became generic (issue 1789): a List<FacesMessage.Severity> in ascending order and a
Map<String, FacesMessage.Severity> keyed by the severity names. The first is useful to code that queues messages:
it can look for an equal message before adding another one, and a Set<FacesMessage> now behaves as a set.
import jakarta.faces.application.FacesMessage;
import java.util.*;
public class F { public static void main(String[] a){
FacesMessage x=new FacesMessage(FacesMessage.SEVERITY_ERROR,"Bad","detail");
FacesMessage y=new FacesMessage(FacesMessage.SEVERITY_ERROR,"Bad","detail");
FacesMessage z=new FacesMessage(FacesMessage.SEVERITY_WARN,"Bad","detail");
System.out.println("equals same fields: "+x.equals(y)+" hash equal: "+(x.hashCode()==y.hashCode()));
System.out.println("equals other severity: "+x.equals(z));
System.out.println("toString: "+x);
System.out.println("set size: "+new HashSet<>(List.of(x,y,z)).size());
List<FacesMessage.Severity> v=FacesMessage.VALUES; Map<String,FacesMessage.Severity> m=FacesMessage.VALUES_MAP;
System.out.println("VALUES: "+v+" MAP keys: "+m.keySet());
FacesMessage.Severity s=m.get("ERROR"); System.out.println("typed lookup: "+s);
}}
Source: Jakarta Faces 4.1 API: FacesMessage and Jakarta Faces 4.1 specification, Changes between 4.1 and 4.0
Compiled against the Mojarra 4.1.17 jar and, separately, the MyFaces API 4.1.4 jar, the program printed on both
equals same fields: true hash equal: true, equals other severity: false and set size: 2 (three messages, two of them
equal), and typed lookup for "ERROR" returned the severity (the first attempt used the key "Error" and got null:
the keys are the upper-case names). VALUES printed [INFO 0, WARN 1, ERROR 2, FATAL 3] on Mojarra and
[INFO, WARN, ERROR, FATAL] on MyFaces. The one visible difference is toString():
FacesMessage[severity='ERROR 2', summary='Bad', detail='detail'] on Mojarra and
FacesMessage[severity='ERROR', summary='Bad', detail='detail'] on MyFaces, so do not parse the string. What the equality
is compared on beyond severity, summary and detail was not tested, and nothing here shows Faces itself dropping duplicates
from the queue; the removal of a duplicate is code you write. Faces 5.0 is planned to add a fifth severity, SUCCESS
(issue 1592, listed on the 5.0 specification page); it is not in 4.1, so keep using INFO for a success text and style it
yourself (see the versions page).
The client id is not part of the message; it is the first argument of FacesContext.addMessage(String, FacesMessage).
A message added with a client id ("review:title") belongs to that component and is shown by an h:message whose
for names it. A message added with a null client id is a global message: it belongs to the page and is shown
by an h:messages. The Bookshelf review bean uses both:
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.annotation.ManagedProperty;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.text.MessageFormat;
import java.util.ResourceBundle;
@Named
@RequestScoped
public class ReviewBean {
@Inject
private FacesContext context;
@Inject
@ManagedProperty("#{msg}")
private ResourceBundle msg;
private String title;
private Integer rating;
private String isbn;
private String text;
public String save() {
if ("Dune".equals(title)) {
// a message that belongs to one component
String summary = msg.getString("review.duplicate");
context.addMessage("review:title",
new FacesMessage(FacesMessage.SEVERITY_WARN, summary, null));
return null;
}
// a global message: no client id
String summary = MessageFormat.format(msg.getString("review.saved"), "Ana", title);
context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, summary, null));
return null;
}
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Integer getRating() { return rating; }
public void setRating(Integer rating) { this.rating = rating; }
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
}
Three details of that bean are worth a sentence each:
-
@Inject FacesContextworks because Faces provides a CDI producer for the current context; the olderFacesContext.getCurrentInstance()call does the same and is still what you use outside a CDI bean. -
@Inject @ManagedProperty("#{msg}")injects themsgresource bundle declared infaces-config.xml(see below).jakarta.faces.annotation.ManagedPropertyis a CDI qualifier that evaluates an EL expression when the bean is injected, so use it in request-scoped beans. It is not the removedjavax.faces.bean.ManagedPropertyof the books. Beans that live longer ask the application for the bundle on each use instead, as shown next. -
MessageFormat.format(...)is needed becauseResourceBundle.getString()returns the raw pattern. Substitution of{0}and{1}is done by you in Java and byh:outputFormatin a view.
A bean that lives longer than a request, or code that has no injected bundle, asks the application, which returns the bundle for the locale of the current view:
ResourceBundle bundle = context.getApplication().getResourceBundle(context, "msg");
String summary = bundle.getString("review.duplicate");
Showing Messages: h:message and h:messages
h:message renders one message for one component, the first message queued for the client id in its for
attribute. h:messages renders all queued messages, as an HTML list by default or as a table with
layout="table". Their defaults differ on purpose: h:message shows the detail (showDetail defaults to
true, showSummary to false), because it sits beside one field and has room for the explanation;
h:messages shows the summary (showSummary defaults to true, showDetail to false), because it lists
many.
The Bookshelf layout (a template, see the templating page) shows global messages once, so every page gets them:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<f:view locale="#{localeBean.locale}">
<h:head>
<title>#{msg['app.name']}</title>
</h:head>
<h:body>
<header>
<h:form id="lang">
#{msg['nav.language']}:
<h:commandLink id="en" value="English" action="#{localeBean.switchTo('en')}"/> |
<h:commandLink id="es" value="Español" action="#{localeBean.switchTo('es')}"/>
</h:form>
</header>
<main>
<h:messages id="global" globalOnly="true"
infoClass="info" warnClass="warn" errorClass="error" fatalClass="fatal"/>
<ui:insert name="content"/>
</main>
</h:body>
</f:view>
</html>
And a review page puts one h:message beside each field:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
<ui:define name="content">
<h1>#{msg['review.title']}</h1>
<h:form id="review">
<h:outputLabel for="title" value="#{msg['review.field.title']}"/>
<h:inputText id="title" value="#{reviewBean.title}" required="true"
label="#{msg['review.field.title']}">
<f:validateLength minimum="3"/>
</h:inputText>
<h:message for="title" errorClass="error" warnClass="warn"/>
<h:outputLabel for="rating" value="#{msg['review.field.rating']}"/>
<h:inputText id="rating" value="#{reviewBean.rating}"
label="#{msg['review.field.rating']}"/>
<h:message for="rating" errorClass="error"/>
<h:outputLabel for="isbn" value="ISBN"/>
<h:inputText id="isbn" value="#{reviewBean.isbn}">
<f:validator validatorId="bookshelf.isbnLength"/>
</h:inputText>
<h:message for="isbn" errorClass="error"/>
<h:outputLabel for="text" value="#{msg['review.field.text']}"/>
<h:inputTextarea id="text" value="#{reviewBean.text}" required="true"
requiredMessage="#{msg['review.text.required']}"/>
<h:message for="text" errorClass="error"/>
<h:commandButton id="send" value="#{msg['review.submit']}" action="#{reviewBean.save}"/>
</h:form>
</ui:define>
</ui:composition>
</html>
Source: VDL documentation: h:message, VDL documentation: h:messages and Jakarta EE Tutorial: Adding Components to a Page Using HTML Tag Library Tags
Submitting that form with a two-letter title, the text abc as the rating and an empty review produced, in Spanish
(Accept-Language: es), the following text beside the fields on both Mojarra and MyFaces:
| Field | Text shown |
|---|---|
Título |
Título: al menos 3 caracteres, por favor. |
Valoración |
Escribe solo dígitos, por ejemplo 9346. |
ISBN |
El ISBN debe tener 13 dígitos, pero "123" tiene 3. |
Reseña |
Escribe algo en tu reseña. |
(The example number inside the rating message differs between implementations; see the comparison table below.)
The attributes both tags share, from the VDL documentation:
| Attribute | Meaning |
|---|---|
|
Required on |
|
|
|
Whether to render the summary and the detail text. The defaults differ between the two tags, as described above. |
|
|
|
CSS class applied per severity. |
|
When |
|
The usual generic attributes. |
A few behaviours you will meet quickly:
-
Messages live for one request. They are queued on the
FacesContextof the current request. When the response is a redirect, they are lost unless you ask the flash to keep them.h:messageswith the idglobalin the layout did not show the message after a Post/Redirect/Get until the action calledcontext.getExternalContext().getFlash().setKeepMessages(true); with it, the message appeared on the page the browser was redirected to, on both implementations. Without the call the redirected page showed nothing.Keeping a message across a redirectpublic String saveAndRedirect() { context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, msg.getString("review.saved"), null)); context.getExternalContext().getFlash().setKeepMessages(true); return "index?faces-redirect=true"; } -
A message nobody displays is not lost silently while you develop. The VDL documentation says that when a form has no
h:messagesand the project stage isDevelopment, the runtime adds one for debugging. In the scratch run an element with the idjakarta_faces_developmentstage_messageswas appended to every page, empty when there was nothing unhandled. Do not style or rely on it; put your ownh:messageswhere users should look. -
In an Ajax request the message component must be in the
renderlist. A message added during an Ajax postback only reaches the screen if theh:messages(orh:message) is re-rendered. The snippet below renders the form and, by absolute id, the global list from the layout. The Ajax event model itself is covered on the Ajax page of this section.An Ajax submit that refreshes field and global messages<h:form id="review"> <h:inputText id="title" value="#{reviewBean.title}" required="true" label="#{msg['review.field.title']}"/> <h:message id="titleMsg" for="title" errorClass="error"/> <h:commandButton id="send" value="#{msg['review.submit']}" action="#{reviewBean.save}"> <f:ajax execute="@form" render="@form :global"/> </h:commandButton> </h:form>In the scratch run, with
Accept-Language: es, an empty title returned a partial response whose form update carried the Spanish detail text under the field, and a valid title returned an update of thegloballist carrying the saved message. -
Other component libraries have their own renderers for the same queue. PrimeFaces provides its own message components (
p:messagesandp:growl, among others) that read the sameFacesMessagelist; they are covered with PrimeFaces. The OmniFaces utility classes wrap the boilerplate of creating and adding messages.
The Messages Faces Itself Raises
The messages in the table above, such as Título: al menos 3 caracteres, were not written in Java by the
Bookshelf. They are standard messages: when a converter, a validator or the required check fails, the
implementation creates a FacesMessage from a message key. The specification requires every implementation to ship a
resource bundle named jakarta.faces.Messages with the same keys, in several languages. Each key looks up the
summary, and the same key with _detail appended looks up the detail. The values are java.text.MessageFormat
patterns.
Some of the keys in the Mojarra 4.1.17 bundle (the specification lists the full set):
| Key | Raised by | Placeholders |
|---|---|---|
|
|
|
|
a converter threw and gave no message |
|
|
text that is not an integer |
|
|
text that is not a date |
|
|
|
|
|
|
|
|
|
the detail text receives the pattern as |
|
Bean Validation constraint violations |
|
The last placeholder of the converter and validator messages is the label of the input: the value of its
label attribute, or, when the attribute is absent, its client id. That is why the review form above sets
label="#{msg['review.field.title']}": without it a failure starts with the bare id, for example j_idt22 or review:title, which
means nothing to a reader. In the scratch run a converter on an h:outputText without a label reported the generated
id j_idt22 in its message.
Three attributes on input components replace a standard message for that one component, without any bundle:
requiredMessage, converterMessage and validatorMessage. The values are ordinary EL, so they come from the resource bundle and follow the view
locale. The review form above uses requiredMessage="#{msg['review.text.required']}", and the empty review showed
Escribe algo en tu reseña. in Spanish and Please write something in your review. in English. Note that
validatorMessage replaces the message of every validator attached to the input, including Bean Validation’s,
which is why it is better for a single, generic sentence and not for per-constraint texts. How a Bean Validation violation becomes a FacesMessage, how the constraint messages are localised with ValidationMessages bundles and how the label placeholder is filled are on the Bean Validation page; validatorMessage versus Bean Validation messages was not run on this page.
Overriding the Standard Messages: the Message Bundle
To change the wording of the standard messages, supply your own bundle in faces-config.xml with the
message-bundle element. Faces then follows this algorithm, taken from the specification, every time it needs a
message for a key:
Only the keys you override need to be present; everything else falls back to the implementation’s bundle, in the language of the view. The Bookshelf declares its bundle for view text and its bundle for standard messages side by side:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
version="4.1">
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>es</supported-locale>
</locale-config>
<resource-bundle>
<base-name>org.example.bookshelf.messages</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>org.example.bookshelf.FacesMessages</message-bundle>
</application>
</faces-config>
jakarta.faces.component.UIInput.REQUIRED={0} is required.
jakarta.faces.component.UIInput.REQUIRED_detail=Fill in the field "{0}" before sending the form.
jakarta.faces.converter.IntegerConverter.INTEGER={2}: "{0}" is not a whole number.
jakarta.faces.converter.IntegerConverter.INTEGER_detail=Type digits only, for example {1}.
jakarta.faces.validator.LengthValidator.MINIMUM={1}: at least {0} characters, please.
jakarta.faces.component.UIInput.REQUIRED={0} es obligatorio.
jakarta.faces.component.UIInput.REQUIRED_detail=Rellena el campo "{0}" antes de enviar el formulario.
jakarta.faces.converter.IntegerConverter.INTEGER={2}: "{0}" no es un número entero.
jakarta.faces.converter.IntegerConverter.INTEGER_detail=Escribe solo dígitos, por ejemplo {1}.
jakarta.faces.validator.LengthValidator.MINIMUM={1}: al menos {0} caracteres, por favor.
# Intentionally empty: every key is inherited from FacesMessages.properties.
# See "The JVM default locale" below for why this file exists.
Source: Jakarta Faces 4.1 specification, Localized Application Messages and Jakarta Faces 4.1 specification, 11.3.5 Application Configuration Resource Format
Posting the review form with an empty title in English and with the title ab and the rating abc in Spanish
produced, on both implementations:
| Request | Message text |
|---|---|
English, empty title |
Fill in the field "Title" before sending the form. (the detail of |
Spanish, short title |
Título: al menos 3 caracteres, por favor. ( |
Spanish, rating |
Escribe solo dígitos, por ejemplo 9346. (the detail of |
Two things stand out. The MINIMUM key has no _detail partner in the bundle, and the page still shows the
text: FacesMessage.getDetail() falls back to the summary, so a field with an h:message (which shows the detail)
reads correctly even when you only translated the summary. And the rating example was 9346 on Mojarra and
21 on MyFaces: the {1} example value is the implementation’s choice, which is one more reason not to quote it in
tests.
Messages from your own validators and converters
A custom Validator or Converter reports failure by throwing ValidatorException or ConverterException
carrying a FacesMessage. Build that message from the application’s resource bundle for the view’s locale, so the
text follows the user’s language. The Bookshelf ISBN validator (id bookshelf.isbnLength):
package org.example.bookshelf.web;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;
import java.text.MessageFormat;
import java.util.ResourceBundle;
@FacesValidator("bookshelf.isbnLength")
public class IsbnLengthValidator implements Validator<String> {
private static final int LENGTH = 13;
@Override
public void validate(FacesContext context, UIComponent component, String value) {
if (value == null || value.length() == LENGTH) {
return;
}
ResourceBundle bundle = context.getApplication().getResourceBundle(context, "msg");
String text = MessageFormat.format(bundle.getString("isbn.length"),
LENGTH, value, value.length());
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, text, null));
}
}
Source: Jakarta EE Tutorial: Creating and Using a Custom Converter and ValidatorException API documentation
With isbn.length=ISBN must have {0} digits, but "{1}" has {2}. in the base bundle and its Spanish version in
messages_es.properties, an ISBN 12345 gave ISBN must have 13 digits, but "12345" has 5. and El ISBN debe tener
13 dígitos, pero "12345" tiene 5. Validators and converters are explored on the validators and converters pages of this section. Bean
Validation has its own localisation: the specification (3.5.6.4) asks Faces to supply a message interpolator that
resolves the locale the Faces way, so constraint messages follow the view locale and the
jakarta.faces.validator.BeanValidator.MESSAGE key wraps the result.
Resource Bundles for the Text of Your Pages
The message bundle above is only for Faces' own messages. The text of your pages (headings, labels, button captions,
your own messages) goes in a second bundle, a normal java.util.ResourceBundle, and the views read it through an EL
variable. There are two ways to expose it.
Declaring the bundle once: resource-bundle
The <resource-bundle> element in faces-config.xml (shown above; the file itself is introduced on the project setup page) names the bundle (base-name) and the EL
variable (var). Every view and every bean in the application can then use #{msg['key']}, and the variable always
resolves to the bundle for the current view’s locale. The base name is a fully qualified resource name without extension, so
org.example.bookshelf.messages means org/example/bookshelf/messages.properties on the class path, with
messages_es.properties as the Spanish sibling:
app.name=Bookshelf
nav.language=Language
catalogue.title=Catalogue
catalogue.count={0,choice,0#No books|1#One book|1<{0} books} in the catalogue
review.title=Write a review
review.field.title=Title
review.field.rating=Rating
review.field.text=Review
review.submit=Send review
review.text.required=Please write something in your review.
review.saved=Thank you, {0}: your review of "{1}" was saved.
review.duplicate=You already reviewed this book.
isbn.length=ISBN must have {0} digits, but "{1}" has {2}.
app.name=Estantería
nav.language=Idioma
catalogue.title=Catálogo
catalogue.count={0,choice,0#Ningún libro|1#Un libro|1<{0} libros} en el catálogo
review.title=Escribe una reseña
review.field.title=Título
review.field.rating=Valoración
review.field.text=Reseña
review.submit=Enviar reseña
review.text.required=Escribe algo en tu reseña.
review.saved=Gracias, {0}: tu reseña de "{1}" se ha guardado.
review.duplicate=Ya has reseñado este libro.
isbn.length=El ISBN debe tener {0} dígitos, pero "{1}" tiene {2}.
# Intentionally empty: every key is inherited from messages.properties.
# See "The JVM default locale" below for why this file exists.
Source: Jakarta EE Tutorial: Registering Application Messages and Jakarta Faces 4.1 specification, Localized Text
A view then reads the text with #{msg['catalogue.title']}. The bracket form works for every key; the dotted form
#{msg.catalogue_title} works only for keys that are valid Java identifiers, which is why keys with dots, such as
the ones above, are always written msg['...']. A key that does not exist renders as ???key??? and does
not throw: in the scratch run #{msg['no.such.key']} printed ???no.such.key??? on both implementations, which is
the behaviour the VDL documentation of f:loadBundle requires of its map and the one <resource-bundle> shows too.
A missing bundle is a different matter and is treated as an error.
Loading a bundle in one view: f:loadBundle
f:loadBundle loads a bundle into a map stored as a request attribute under the name in var, for the locale of the
current view. It needs no faces-config.xml entry, which makes it handy for a bundle used by a single page:
<f:view locale="en" xmlns:f="jakarta.faces.core" xmlns:h="jakarta.faces.html">
<f:loadBundle basename="org.example.bookshelf.messages" var="lb"/>
<h:outputText value="#{lb['nav.language']}"/>
</f:view>
Source: VDL documentation: f:loadBundle
The two can be compared directly:
<resource-bundle> in faces-config.xml |
f:loadBundle in a view |
|
|---|---|---|
Scope of the variable |
The whole application, in views and in beans |
The request, in the views that contain the tag |
Available from Java |
Yes: |
No: only the view’s EL can see the request attribute |
Must be repeated |
No |
In every view that needs it |
Missing key |
|
|
Missing bundle |
Not exercised in the scratch run |
A |
Use it for |
The application’s text |
A bundle used by one page, or a library’s own bundle |
Both gave identical output for the same bundle in the scratch run. Prefer <resource-bundle> for the application’s
text, because one declaration serves views and beans alike.
The JVM default locale: a trap in the bundle lookup
ResourceBundle.getBundle does not go straight from messages_en to messages. When the requested locale has
no bundle of its own, Java first tries the bundle for the JVM’s default locale, and only then the base bundle. In the
scratch run the server JVM was started with -Duser.language=es. With only messages.properties and
messages_es.properties on the class path, a request with Accept-Language: en rendered Spanish: there was no
messages_en.properties, so Java fell back to messages_es (the JVM default) before reaching the English base. The
same behaviour was visible on both implementations, because both go through ResourceBundle.
The fix is an English bundle that exists, even when it is empty: with messages_en.properties (a comment is enough)
present, the en request found its own bundle and inherited every key from the base. The messages_en.properties
and FacesMessages_en.properties files shown above exist for exactly this reason. Developers on a Spanish-locale
laptop and servers in a Spanish-locale container hit this; a teammate in an English-locale JVM does not, which is why it
survives code review.
Resource bundle files are UTF-8
Since Java 9, PropertyResourceBundle reads a .properties stream as UTF-8 and falls back to ISO-8859-1 only when
the bytes are not valid UTF-8. The Bookshelf files above contain á, é, í, ñ and ó written directly
and rendered correctly on both implementations under Java 26. The \uXXXX escapes and the native2ascii step of
Java 8 and earlier, which the older books show, are no longer needed.
Parameterised and Plural Messages
Bundle values are java.text.MessageFormat patterns. In a view, h:outputFormat fills them in from nested
f:param elements. The Bookshelf catalogue header needs singular and plural, which a choice format expresses in
the pattern itself, so the translator controls the grammar for each language:
<h1>#{msg['catalogue.title']}</h1>
<p>
<h:outputFormat value="#{msg['catalogue.count']}">
<f:param value="#{catalogueBean.size}"/>
</h:outputFormat>
</p>
<p>
<h:outputText value="#{catalogueBean.published}">
<f:convertDateTime type="localDate" dateStyle="long"/>
</h:outputText>
</p>
<p>
<h:outputText value="#{catalogueBean.price}">
<f:convertNumber type="currency" currencyCode="EUR"/>
</h:outputText>
</p>
With a catalogue of 3 books, the three counts 0, 1 and 3 rendered, in English and then Spanish:
| Count | English | Spanish |
|---|---|---|
|
No books in the catalogue |
Ningún libro en el catálogo |
|
One book in the catalogue |
Un libro en el catálogo |
|
3 books in the catalogue |
3 libros en el catálogo |
Four rules for these patterns:
-
The parameter must be a number for a
choiceornumberelement.<f:param value="3"/>passes the string"3"and the page failed withCannot format given Object as a Number;<f:param value="#{3}"/>or a bean property of typeintworks. -
An apostrophe in a pattern that has placeholders must be doubled. In
MessageFormata single quote starts a quoted section. With the English textDon't panic: {0} booksand one parameter, Mojarra and MyFaces both renderedDont panic: {0} books: the quote vanished and the placeholder was not replaced. WrittenDon''t panic: {0} booksit renders correctly. This is why the standard Faces messages contain''{0}''. -
The doubling is only right where
MessageFormatruns.#{msg['key']}printed directly, orResourceBundle.getString()without aMessageFormatcall, shows the pattern verbatim, soDon''tappears with both quotes. The two cases need two versions of the text, or you always format. -
Dates passed as parameters use the JVM’s time zone.
MessageFormatformats aDateargument itself, in the JVM default zone (the epoch showed1:00on a server in Spain). Format the date withf:convertDateTime, wheretimeZoneis explicit, and put the formatted string in the message.
Mojarra and MyFaces differ on one more point. With no f:param children, the VDL documentation says the value is
rendered unmodified, which is what Mojarra did (Don't panic stayed intact). MyFaces applied MessageFormat anyway
and printed Dont panic. Do not depend on either: a message that contains an apostrophe should be doubled, or never
sent through h:outputFormat without parameters.
Choosing the Locale
Faces keeps one active locale per view, on UIViewRoot. Everything locale-aware reads it: the resource bundle
variable, converters and validators, the messages above and f:convertNumber. Declare what the application supports
in faces-config.xml (the block shown earlier):
<locale-config>
<default-locale>en</default-locale>
<supported-locale>es</supported-locale>
</locale-config>
When a view is created, the ViewHandler calculates a locale: it compares the browser’s preferences, the
Accept-Language header, with the supported locales and uses the default when none matches. The specification
leaves the details to the Javadoc, so they were observed. Both Mojarra and MyFaces, with a JVM whose default locale was
English, gave the same answers:
Accept-Language |
View locale | Why |
|---|---|---|
|
|
exact match with a supported locale |
|
|
no |
|
|
nothing supported matches, so the default locale is used |
|
|
the browser’s preference list is walked in order until a supported locale matches |
|
|
the first supported entry, |
no header |
the JVM default, when supported, else the default |
a request without the header gets the server’s own default locale from the Servlet API |
The last row hides the trap again: the "default" for a request without the header is the server’s, not the
application’s default-locale. On a JVM whose default was es, a header-less request rendered in Spanish because
es was a supported locale. If you want the application’s default for header-less clients, set the JVM default
locale deliberately (-Duser.language=en) or keep an explicit choice in the session, as below.
The active locale can also be set by hand. UIViewRoot.getLocale() follows an algorithm that is spelled out in its
Javadoc:
Two consequences follow from it. A null from the expression is harmless: Faces falls back to the calculated locale. And
an explicit UIViewRoot.setLocale() call wins over a f:view expression for that view; use one mechanism, not both.
In a page, f:view takes a locale as a java.util.Locale or as a string in the form
language[{-|_}country[{-|_}variant]], for example es or ja_JP.
A Language Switcher
The browser’s preference is a good start, but users want to override it. The pieces are a place to remember the choice
(a session-scoped bean), f:view locale to apply it, and a control that changes it. The bean starts from the locale
Faces would have chosen:
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.application.Application;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@Named
@SessionScoped
public class LocaleBean implements Serializable {
private Locale locale;
@PostConstruct
void init() {
FacesContext context = FacesContext.getCurrentInstance();
locale = context.getApplication().getViewHandler().calculateLocale(context);
}
public Locale getLocale() {
return locale;
}
public String getLanguage() {
return locale.getLanguage();
}
public void setLanguage(String language) {
this.locale = Locale.forLanguageTag(language);
}
public List<Locale> getSupportedLocales() {
Application application = FacesContext.getCurrentInstance().getApplication();
List<Locale> locales = new ArrayList<>();
locales.add(application.getDefaultLocale());
application.getSupportedLocales().forEachRemaining(locales::add);
return locales;
}
public String switchTo(String language) {
setLanguage(language);
return reload();
}
public String reload() {
String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId();
return viewId + "?faces-redirect=true&includeViewParams=true";
}
}
The layout shown earlier wraps the whole document in <f:view locale="#{localeBean.locale}"> and its language
links call #{localeBean.switchTo('es')}. Those links, the bundle and the formats on the catalogue page produced the
following, one request per row (JVM default locale es, so the _en bundles matter):
Accept-Language |
Title | Count (3) | Date | Price |
|---|---|---|---|---|
|
Bookshelf |
3 books in the catalogue |
December 23, 1815 |
€12.50 |
|
Estantería |
3 libros en el catálogo |
23 de diciembre de 1815 |
12,50 € |
|
Bookshelf |
3 books in the catalogue |
December 23, 1815 |
€12.50 |
After a click on Español the same session received Spanish on the redirected page and on the next request, on both implementations, whatever the browser’s header said.
Several details of the bean deserve attention:
-
It asks
ViewHandler.calculateLocale()instead of readingUIViewRootin@PostConstruct. The first version of the scratch bean readcontext.getViewRoot().getLocale(). It gave the right answer on pages that use the layout as a template client, and the wrong one (English for anesbrowser) on a page that hadf:viewas its own root and touched the bean from there. The cause was not isolated;calculateLocalegave the right answer in both cases and does not depend on the view being in any particular state. -
f:viewmust wrap the content. The layout puts it aroundh:headandh:body. Because thelocaleattribute is an EL expression, the bundle variable and the converters on every page that uses the layout followlocaleBean.localewithout any page repeating it. -
It redirects after switching, on purpose. With a plain action that sets the locale and returns
null, Mojarra re-rendered the same postback in Spanish, while MyFaces answered the postback in the old language and showed the new one only on the next request (as if it had evaluated thef:viewexpression while rebuilding the view, before the action ran). Returning an outcome withfaces-redirect=truemakes both implementations behave identically, since the next request is a GET that sees the new locale from the start.includeViewParams=truekeeps the view parameters on the redirect URL, a topic of the view parameters page in this section. -
A direct
UIViewRoot.setLocale()call also works, as the Javadoc algorithm predicts, but remember that it takes precedence over the expression for that view.
A drop-down is a small variation on the same bean. Render the supported locales as select items, bind the selection to
language, and let the button reload the view:
<h:form id="menuform">
<h:selectOneMenu id="menu" value="#{localeBean.language}">
<f:selectItems value="#{localeBean.supportedLocales}" var="l"
itemValue="#{l.language}" itemLabel="#{l.getDisplayLanguage(l)}"/>
</h:selectOneMenu>
<h:commandButton id="apply" value="OK" action="#{localeBean.reload}"/>
</h:form>
l.getDisplayLanguage(l) asks each locale to name itself in its own language, so the menu showed English and
español whatever the page language. Posting the form with es selected switched the application to Spanish.
Where a choice should survive the session, store it where it fits your application: a cookie read in the bean’s
@PostConstruct, a user-profile column, or a locale segment in the URL. The session-scoped bean is the minimum, and
the part of the page that depends on it, f:view locale, does not change.
Formatting Numbers and Dates by Locale
Converters read the active locale unless told otherwise. The catalogue page above shows the effect without extra work:
f:convertDateTime type="localDate" dateStyle="long" printed December 23, 1815 in English and 23 de diciembre de 1815
in Spanish, and f:convertNumber type="currency" currencyCode="EUR" printed €12.50 and 12,50 €. The
type="localDate" form is the way to format java.time.LocalDate; the older date and time types
expect a java.util.Date.
Both converters accept a locale attribute, a java.util.Locale or a language string, that overrides the view
locale for one value. That is the right tool when a number must be written one way regardless of the reader, for
instance with the invoice’s own convention or a machine-readable value:
<h:outputText value="#{catalogueBean.price}">
<f:convertNumber minFractionDigits="2" locale="en"/>
</h:outputText>
That printed 12.50 on the Spanish page, where the surrounding currency converter printed 12,50 €. For dates,
timeZone is the other attribute you will want: a date is formatted in the JVM’s time zone unless the converter
names one, and an international audience seldom shares the server’s zone.
Character Encoding and the Response Locale
At the start of Render Response the ViewHandler sets the response locale to the view’s locale, which can influence the
response’s character encoding according to the Servlet specification. f:view also has encoding and
contentType attributes to set both explicitly. In the scratch run every page, English or Spanish, rendered the
accented characters correctly with no extra configuration other than the UTF-8 resource files. Keep your sources,
templates and bundles in UTF-8 and the three agree.
Mojarra and MyFaces at a Glance
Everything above behaved the same on both implementations except these points, observed with Mojarra 4.1.17 and MyFaces Core 4.1.4:
| Behaviour | Mojarra 4.1.17 | MyFaces Core 4.1.4 |
|---|---|---|
|
Value rendered unmodified, as the VDL documentation says |
|
|
The same response is already in the new locale |
The new locale shows from the next request |
|
|
|
Quotes in message text |
Written as they are |
Written as |
Supported-locale matching, standard message override, |
Identical |
Identical |
When you write a test that posts a form to MyFaces without a browser, remember that it also expects the hidden
<formId>_SUBMIT field and identifies a link by <formId>:_idcl, while Mojarra accepts the link’s client id as a
parameter. A real browser sends both correctly.
Common Mistakes
-
No
labelon inputs. The message ends with the client id (review:title, or a generatedj_idt22). Setlabelfrom the resource bundle on every input that can fail. -
Hard-coded English in
FacesMessage. A literal summary ignores the locale. Always build it from the bundle for the view’s locale, as in the examples above. -
Missing
messages_en.properties. On a server whose JVM locale is not English, the base bundle is skipped. Keep a bundle for every supported locale, even an empty one. -
Old
javax.faceskeys in a custom message bundle. In the scratch run aFacesMessages.propertieswhose keys started withjavax.faces.was silently ignored and the default text Title: Validation Error: Value is required. appeared. Faces 3.0 renamed the namespace; the keys arejakarta.faces.*(the Faces 3.0 version number follows the Jakarta EE 9 rename and was not separately checked). -
Apostrophes in patterns that have placeholders. Double them, or the placeholder and the quote disappear.
-
f:paramwith a literal for a numeric format. Pass a number through EL. -
Messages not rendered by Ajax. Add the
h:messagesto therenderattribute, or the user sees nothing. -
Messages lost on redirect. Use
setKeepMessages(true)(or the flash) when an action both adds a message and redirects. -
escape="false"on a message component with user input in the text. A message that echoes what the user typed is output that must stay escaped. Do not turnescapeoff to show formatting; do it in the bundle text with plain characters or style classes. -
Bundle text with a secret or a path. Bundles are readable by anyone who can reach the class path or the generated page; keep credentials and server paths out of them.
What Changed Between Versions
-
Faces 1.2: the
resource-bundleapplication element andApplication.getResourceBundle()(the Javadoc of the method carriesSince: 1.2), the alternative tof:loadBundle. -
Faces 2.3: the
java.timetypes inf:convertDateTime(type="localDate"and friends; the 4.1 Javadoc lists them, but the change log does not say in which release they arrived, and 2.3 is the usual attribution), and the CDI producers andjakarta.faces.annotation.ManagedPropertyqualifier that let a bean inject the bundle or theFacesContext. The CDI replacement for@ManagedPropertyis issue 1418 in the 4.1 specification’s change log (a 2.3 item); the exact version of the producers was not re-checked on the 2026 Javadoc. -
Faces 3.0 and 4.0: the
javax.facestojakarta.facesrename (Faces 3.0, Jakarta EE 9; the 4.1 specification lists the keys only in theirjakarta.faces.*form) moved the standard bundle tojakarta.faces.Messages, the message keys to thejakarta.faces.*prefix and the Facelets namespaces to URNs such asjakarta.faces.core(f:viewas written above). Thejavax.faces.bean.ManagedPropertyannotation of the books was removed in 4.0. -
Faces 4.1:
FacesMessagegainsequals(),hashCode()andtoString(), andVALUESandVALUES_MAPbecome generic (see FacesMessage: Severity, Summary and Detail); nothing else about messages or locales that this page depends on changed, and every behaviour above was observed on 4.1 implementations. -
Faces 5.0 (upcoming): still a milestone. A
SUCCESSseverity forFacesMessage(issue 1592) is planned and not available in 4.1; nothing on this page is announced as removed, but the planned removal of the pre-4.0 Facelets namespaces would affect the tag libraries the older books use.
Where the Books Are Outdated
-
The JSF 2.0 books (Burns and Schalk, Leonard) and the PrimeFaces cookbook show
javax.faces.application.FacesMessage, thejavax.faces.Messagesbundle and thehttp://java.sun.com/jsf/corenamespaces. The classes and tags are the same today with thejakarta.facesnames and the URN namespacesjakarta.faces.coreandjakarta.faces.html, but a message bundle copied with thejavax.faces.*keys is ignored, as shown above. -
They keep view text in a
@ManagedBeanor<managed-bean>bean and often load it withf:loadBundle. Declare the bundle once with<resource-bundle>and inject it with CDI instead. -
Their locale switchers use
@ManagedBean @SessionScopedandjavax.faces.bean.ManagedProperty. The CDI version above (@Named @SessionScoped) is the current one, and it redirects after the switch to behave the same on Mojarra and MyFaces. -
Properties files written with
\uXXXXescapes or converted withnative2asciiwere necessary when Java read them as ISO-8859-1. Java 9 and later read them as UTF-8, so write the characters directly. -
The books' PrimeFaces chapters show
RequestContextand legacy message components; PrimeFaces 16 usesPrimeFaces.current(). Thep:messagesandp:growlcomponents are discussed on the PrimeFaces Ajax and forms page.
Use the books for the reasoning about why messages carry a severity, a summary and a detail, and why locales belong to the view. Use this page and the specification for the keys and attributes to type today.
Source: Burns and Schalk, JavaServer Faces 2.0: The Complete Reference (McGraw-Hill, 2010, ISBN 978-0-07-162509-8), Leonard, JSF 2.0 Cookbook (Packt, 2010, ISBN 978-1-847199-52-2) and Jakarta Faces 4.1 specification, Localization and Internationalization
References
-
Jakarta Faces 4.1 specification, Localization and Internationalization
-
Jakarta Faces 4.1 specification, Localized Application Messages
-
Jakarta Faces 4.1 specification, Localization of Bean Validation Messages
-
Jakarta Faces 4.1 specification, 11.3.5 Application Configuration Resource Format
-
Jakarta Faces 4.1 specification, Changes between 4.1 and 4.0
-
Burns, Schalk, Griffin: JavaServer Faces 2.0: The Complete Reference, McGraw-Hill, 2010, publisher page
-
Leonard: JSF 2.0 Cookbook, Packt, 2010, publisher page