© Bauke Scholtz, Arjan Tijms 2018

Bauke Scholtz and Arjan Tijms, The Definitive Guide to JSF in Java EE 8, https://doi.org/10.1007/978-1-4842-3387-0_14

14. Localization

Bauke Scholtz and Arjan Tijms2

(1)Willemstad, Curaçao

(2)Amsterdam, Noord-Holland, The Netherlands

JSF has always had decent internationalization support. Since JSF 1.0 you can supply java.util.ResourceBundle-based bundle files in different locales, which in turn get dynamically included as text in the web page at the declared places. Also, all JSF converters and validators have their own set of localized default messages which you can easily customize via a message bundle or, since JSF 1.2, via new requiredMessage, converterMessage, and validatorMessage attributes. JSF 2.0 adds, via the new javax.faces.application.ResourceHandler, API (application programing interface) support for localizable assets such as stylesheets, scripts, and images.

The act of internationalization, “I18N,” is distinct from the act of localization, “L10N.” The internationalization part is basically already done by JSF (JavaServer Faces) itself as being a MVC (model-view-controller) framework. All you need to do is to take care of the localization part. Basically, you need to specify the “active locale” in the view, supply the desired resource bundle files, if necessary translated with help of a third-party translation service, and declare references to the bundle file in your JSF page.

In this chapter you will learn how to prepare a JSF web application for different languages and how to develop it in order to make localization easier for yourself as to maintenance.

Hello World, Olá mundo, A454457_1_En_14_Figa_HTML.gif

To start off, create a bunch of new bundle files in main/java/resources folder of the project. The main/java/resources folder of a Maven WAR project is intended for non-class files which are supposed to end up in the /WEB-INF/classes folder of the final build. The bundle files can be in java.util.Properties format, with the .properties extension.

The filename of those files must have a common prefix (e.g., “text”), followed by an underscore and the two-letter ISO 639-1-Alpha-21 language code (e.g., “en” for English, “pt” for Portuguese, and “hi” for Hindi). It can optionally be followed by another underscore and the two-letter ISO 3166-1-Alpha-22 country code (e.g., “GB” for Great Britain, “US” for United States, “BR” for Brazil, “PT” for Portugal).

main/java/resources/com/example/project/i18n/text.properties
title = Localization example
heading = Hello World
paragraph = Welcome to my website!
main/java/resources/com/example/project/i18n/text_pt_BR.properties
title = Exemplo de localização
heading = Olá mundo
paragraph = Bem-vindo ao meu site!
main/java/resources/com/example/project/i18n/text_hi.properties

A454457_1_En_14_Figb_HTML.gif

Do note that all those bundle files have common keys title”, “heading”, and “paragraph”, which are usually in English. It’s basically the lingua franca of the Internet and web developers. It’s considered the best practice to keep the source code entirely in English, particularly if it is open source.

Also note that the English bundle file doesn’t have the “en” language code in the filename as in text_en.properties but is just text.properties. Basically, it has become the fallback bundle file which is supposed to contain every single bundle key used in the entire web application. This way, when a bundle file with a specific language code doesn’t contain the desired bundle entry, then the value will be looked up from the fallback bundle file. This is useful for situations wherein you’d like to gradually upgrade the bundle files, or when you have certain sections in the web application which don’t necessarily need to be localized, such as back-end admin pages.

Configuration

In order to familiarize the JSF application with those bundle files and the desired locales, we need to edit its faces-config. xml file to add the following entries to the <application> element:

<application>
    <locale-config>
        <default-locale>en</default-locale>
        <supported-locale>pt_BR</supported-locale>
        <supported-locale>hi</supported-locale>
    </locale-config>
    <resource-bundle>
        <base-name>com.example.project.i18n.text</base-name>
        <var>text</var>
    </resource-bundle>
</application>

The <base-name> must specify the fully qualified name (FQN) following the same convention as for Java classes and that it doesn’t include the file extension. The <var> basically declares the EL (Expression Language) variable name of the bundle file. This will make the currently loaded resource bundle available as a Map-like object in EL via #{text}. To avoid conflicts, you only need to make sure that this name isn’t already possessed by any managed bean or any implicit EL objects.

Referencing Bundle in JSF Page

It’s relatively simple, just treat #{text} as a Map with the bundle keys as map keys .

<!DOCTYPE html>
<html lang="#{view.locale.toLanguageTag()}"
    xmlns:="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html">
    <h:head>
        <title>#{text['title']}</title>
    </h:head>
    <h:body>
        <h1>#{text['heading']}</h1>
        <p>#{text['paragraph']}</p>
    </h:body>
</html>

JSF will already automatically determine the closest matching active locale based on the HTTP Accept-Language header3 and set it as locale property of UIViewRoot. The Accept-Language header is configurable in browser’s settings. In, for example, Chrome, you can configure it via chrome://settings/languages. If you play around with it, for example, by switching between English, Portuguese, and Hindi as the top-ranked language setting in browser and refresh the JSF page, then you’ll notice that it changes the text to conform the browser-specified language setting. If you check the browser’s developer tools—usually accessible by pressing F12—and inspect the HTTP request headers in the network monitor, then you’ll also notice that the Accept-Language header changes accordingly.

You might have noticed that the lang attribute of the <html> tag references #{view.locale.toLanguageTag()}. Basically, this will print the IETF BCP 47 language tag4 of the locale property of the current UIViewRoot, which is in turn available as an implicit EL object #{view}. The locale property is an instance of java.util.Locale which has actually no getter method for the language tag such as getLanguageTag(), but only a toLanguageTag() method, hence the direct method reference in EL instead of the expected property reference.

The lang attribute of the <html> tag is not mandatory for the functioning of JSF localization feature. Moreover, JSF treats it as template text and does nothing special with it. You can safely leave out it. It is, however, important for search engines. This way a search engine like Google will be informed which language the page’s copy is in. This is not only important in order to end up correctly in localized search results, but also important in case you serve the very same page in different languages. This would otherwise by the average search engine algorithm be penalized as “duplicate content,” which is thus bad for SEO (search engine optimization) ranking.

You’ll also have noticed that the bundle keys are specified in the so-called brace notation #{text['...']}. The string between single quotes basically represents the bundle key. In this specific case you could also have used #{text.title}, #{text.heading}, and #{text.paragraph} instead. This is, however, not the common practice. Using the brace notation not only gives a generally clear meaning to what the EL variable represents (a resource bundle), but it also allows you to use dots in the bundle key name such as #{text['meta.description']}. The EL expression #{text.meta.description} has, namely, an entirely different meaning: “get the description property of the nested meta property of the text object,” which is incorrect.

Changing the Active Locale

You can also change the active locale on the server side. This is best to be done in a single place in a site-wide master template which contains the <f:view> tag. The active locale can be set in the locale attribute of the <f:view> which can accept either a static string representing the language tag or a concrete java.util.Locale instance. The locale attribute accepts an EL expression and can be changed programmatically via a managed bean. This offers you the opportunity to let the user change it via the web page without fiddling around in the browser’s language settings. You could present the available language options to the user in a JSF page and let each selection change the active locale. This can be achieved with the following JSF page:

<!DOCTYPE html>
<html lang="#{activeLocale.languageTag}"
    xmlns:="http://www.w3.org/1999/xhtml"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html">
    <f:view locale="#{activeLocale.current}">
        <h:head>
            <title>#{text['title']}</title>
        </h:head>
        <h:body>
            <h1>#{text['heading']}</h1>
            <p>#{text['paragraph']}</p>
            <h:form>
                <h:selectOneMenu value="#{activeLocale.languageTag}">
                    <f:selectItems
                        value="#{activeLocale.available}" var="l"
                        itemValue="#{l.toLanguageTag()}"
                        itemLabel="#{l.getDisplayLanguage(l)}">
                    </f:selectItems>
                    <f:ajax listener="#{activeLocale.reload()}" />
                </h:selectOneMenu>
            </h:form>
        </h:body>
    </f:view>
</html>

It is slightly adjusted from the previous example; there is now <f:view> around <h:head> and <h:body>. The locale attribute of <f:view> references the currently active locale via the #{activeLocale} managed bean, which is as follows:

@Named @SessionScoped
public class ActiveLocale implements Serializable {


    private Locale current;
    private List<Locale> available;


    @Inject
    private FacesContext context;


    @PostConstruct
    public void init() {
        Application app = context.getApplication();
        current = app.getViewHandler().calculateLocale(context);
        available = new ArrayList<>();
        available.add(app.getDefaultLocale());
        app.getSupportedLocales().forEachRemaining(available::add);
    }


    public void reload() {
        context.getPartialViewContext().getEvalScripts()
            .add("location.replace(location)");
    }


    public Locale getCurrent() {
        return current;
    }


    public String getLanguageTag() {
        return current.toLanguageTag();
    }


    public void setLanguageTag(String languageTag) {
        current = Locale.forLanguageTag(languageTag);
    }


    public List<Locale> getAvailable() {
        return available;
    }


}

To reiterate, @Inject FacesContext works only if you have placed @ FacesConfig on an arbitrary CDI bean somewhere in the web application. Otherwise you have to replace it by inline FacesContext.getCurrentInstance() calls. There’s only one caveat with those inline calls: you need to make absolutely sure that you don’t assign it as a field in, for example, @PostConstruct, because the actual instance is subject to being changed across method calls on the very same bean instance. Injecting as a field via CDI takes transparently care of this, and is therefore safe, but manually assigning is not.

In @ PostConstruct, ViewHandler#calculateLocale() is used to calculate the current locale based on Accept-Language header and the default and supported locales as configured in faces-config.xml. This follows exactly the same JSF-internal behavior as if when there’s no <f:view locale> defined. Finally, the available locales are collected based on the configured default and supported locales.

The available locales are, via <f:selectItems> of <h:selectOneMenu>, presented to the user as drop-down options (see Figure 14-1). The nested <f:ajax> makes sure that the selected option is set in the managed bean as soon as the user changes the option.

A454457_1_En_14_Fig1_HTML.jpg
Figure 14-1 Changing the active locale

The #{activeLocale.languageTag} property delegates internally to the current java.util.Locale instance. This is basically done for convenience so that we don’t necessarily need to add a converter for #{activeLocale.current} in case we want to use it in <h:selectOneMenu>.

<f:ajax listener> basically performs a full page reload with the help of a piece of JavaScript which is executed on completion of the Ajax request. This is done by adding a script to the PartialViewContext#getEvalScripts() method, which is new since JSF 2.3. Any added script will end up ordered in the <eval> section of the JSF Ajax response, which in turn gets executed after JSF Ajax engine has updated the HTML DOM (Document Object Model) tree.

The script itself, location.replace(location), basically instructs JavaScript to reload the current document without keeping the previous document in history. This means that the back button won’t redisplay the same page. You can also use location.reload(true) instead, but this won’t work nicely if a synchronous (non-Ajax) POST request has been fired on the same document beforehand. It would be re-executed and cause a double submit. And, it unnecessarily remembers the previous page in the history. This may end up in confusing behavior, because the back button would then seem to have no effect as it would redisplay exactly the same page as the active locale is stored in the session, not in the request.

Alternatively, instead of invoking <f:ajax listener>, you can in this specific use case also use just <f:ajax render="@all"> without any listener. It has at least one disadvantage: the document’s title won’t be updated. In any case, using @all is generally considered a bad practice. There’s only one legitimate real-world use case for it: displaying a full error page on an Ajax request.

As a completely different alternative, you could make the active locale request scoped instead of session scoped by including the language tag in the URL as in http://example.com/en/page.xhtml , http://example.com/pt/page.xhtml , http://example.com/hi/page.xhtml . This way you can change the active locale by simply following a link. This only involves a servlet filter which extracts the java.util.Locale instance from the URL and forwards it to the desired JSF page, and a custom view handler which includes the language tag in the generated URL of any <h:form>, <h:link>, and <h:button> component. You can find a kickoff example in the Java EE Kickoff Application.5

Organizing Bundle Keys

When the web application grows, you may notice that bundle files start to become unmaintainable. The key is to organize the bundle keys following a very strict convention. Reusable site-wide entries, usually those used as input labels, button labels, link labels, table header labels, etc., should be keyed using a general prefix (e.g., “label.save=Save”). Page-specific entries should be keyed using a page-specific prefix (e.g., “foldername_pagename.title=Some Page Title”). Following is an elaborate example of a localized Facelets template , /WEB-INF/templates/page.xhtml:

<!DOCTYPE html>
<html lang="#{activeLocale.language}"
    xmlns:="http://www.w3.org/1999/xhtml"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
    xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
>
    <c:set var="page" value="page#{fn:replace(
        fn:split(view.viewId, '.')[0], '/', '_')}" scope="view" />
    <f:view locale="#{activeLocale.current}">
        <h:head>
            <title>#{text[page += '.title']}</title>
            <meta name="description"
                content="#{text[page += '.meta.description']}" />
        </h:head>
        <h:body id="#{page}">
            <header>
                <nav>
                    <h:link outcome="/home"
                        value="#{text['label.home']}" />
                    <h:link outcome="/login"
                        value="#{text['label.login']}" />
                    <h:link outcome="/signup"
                        value="#{text['label.signup']}" />
                </nav>
                <h:form>
                    <h:selectOneMenu
                            value="#{activeLocale.languageTag}">
                        <f:selectItems
                            value="#{activeLocale.available}" var="l"
                            itemValue="#{l.toLanguageTag()}"
                            itemLabel="#{l.getDisplayLanguage(l)}">
                        </f:selectItems>
                        <f:ajax listener="#{activeLocale.reload()}" />
                    </h:selectOneMenu>
                </h:form>
            </header>
            <main>
                <h1>#{text[page += '.title']}</h1>
                <ui:insert name="content" />
            </main>
            <footer>
                © #{text['page_home.title']}
            </footer>
        </h:body>
    </f:view>
</html>

The JSTL <c:set> basically converts the UIViewRoot#getViewId() to a string which is suitable as a page-specific prefix . The JSF view ID basically represents the absolute server-side path to the physical file representing the JSF page (e.g., “/user/account.xhtml”). This needs to be manipulated to a format suitable as a resource bundle key. The fn:split() call extracts the part “/user/account” from it and the fn:replace() call converts the forward slash to underscore so that it becomes “_user_account”. Finally, <c:set> stores it as “page_user_account” in the view scope under the name “page” so that it’s available as #{page} elsewhere in the same view.

You’ll notice that #{page} is in turn being used as, among others, the ID of <h:body>. This makes it easier to select a specific page from a general CSS (Cascading Style Sheets) file just in case that’s needed. #{page} is also being used in several resource bundle references, such as #{text[page += '.title']} which ultimately references in case of “/home.xhtml” the key “page_home.title”. With such a template you can have the following page-specific resource bundle entries:

page_home.title = My Website
page_home.meta.description = A Hello World JSF application.


page_login.title = Log In
page_login.meta.description = Log in to My Website.


page_signup.title = Sign Up
page_signup.meta.description = Sign up to My Website.

Following is an example of a template client which utilizes the previously shown template /WEB-INF/templates/page.xhtml, the /login.xhtml:

<ui:composition template="/WEB-INF/templates/page.xhtml"
    xmlns:="http://www.w3.org/1999/xhtml"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
>
    <ui:define name="content">
        <h:form id="login">
            <fieldset>
                <h:outputLabel for="email"
                    value="#{text['label.email']}" />
                <h:inputText id="email" required="true"
                    value="#{login.email}" />
                <h:message for="email" styleClass="message" />


                <h:outputLabel for="password"
                    value="#{text['label.password']}" />
                <h:inputSecret id="password" required="true"
                    value="#{login.password}" />
                <h:message for="password" styleClass="message" />


                <h:commandButton id="submit" action="#{login.submit}"
                    value="#{text['label.login']}" />
                <h:message for="login" styleClass="message" />
            </fieldset>
        </h:form>
    </ui:define>
</ui:composition>

Following is what the associated resource bundle entries look like:

label.email = Email
label.password = Password
label.login = Log In

Note: in case you find that the page looks crippled, simply add a CSS file with the following rule to start with:

nav a, fieldset label, fieldset input {
    display: block;
}

Localizing Conversion/Validation Messages

In case you have prepared a simple backing bean class Login with two string properties email and password and a method submit(), and submit the above shown login page without filling out the e-mail input field, then you’ll face a validation error in the following format:

login:email: Validation Error: Value is required.

When you switch the language to Portuguese and resubmit the empty form, then you’ll see that it’s also localized. However, when you switch the language further to Hindi, then you’ll notice that there’s no standard Hindi message bundle in the standard JSF implementation. You’d need to provide your own. There are several ways to achieve this.

First, JSF input and select components support three attributes to override the default message : requiredMessage, validatorMessage, and converterMessage. The following example shows how to override the default required message:

<h:inputText ... requiredMessage="#{text['message.required']}" />

This is arguably the easiest approach. The major caveat is that you have to copy/paste it everywhere in case you haven’t wrapped it in a reusable tag file like <my:inputText>. This is not DRY.6

Another way is to supply a custom message bundle which overrides all predefined bundle keys specific for JSF conversion/validation messages and register it as <message-bundle> in faces-config.xml. You can find the predefined bundle keys in chapter 2.5.2.4 “Localized Application Messages” of the JSF specification.7 The bundle key of the default required message “Validation Error: Value is Required” is thus javax.faces.component.UIInput.REQUIRED. We can adjust it in new message bundle files as follows:

main/java/resources/com/example/project/i18n/messages.properties
javax.faces.component.UIInput.REQUIRED = {0} is required.
main/java/resources/com/example/project/i18n/messages_pt_BR.properties
javax.faces.component.UIInput.REQUIRED = {0} é obrigatório.
main/java/resources/com/example/project/i18n/messages_hi.properties

A454457_1_En_14_Figc_HTML.gif

Finally, configure it in the <application> element of the faces-config.xml file:

<application>
    ...
    <message-bundle>com.example.project.i18n.messages</message-bundle>
</application>

You’ll perhaps have noticed the {0} placeholders in the messages. They represent the labels of the associated input and select components. The labels default to the component’s client ID, which is basically the ID of the JSF-generated HTML element as you can find in the browser’s page source. You can override it by explicitly setting the label attribute of the component.

<h:inputText id="email" ... label="#{text['label.email']}" />
<h:inputSecret id="password" ... label="#{text['label.password']}" />

Note that putting the message bundle in a different file than the resource bundle is not strictly necessary. You can also just put the message bundle entries in text.properties files and adjust the <message-bundle> entry to point to the same FQN as <resource-bundle>.

Obtaining Localized Message in a Custom Converter/Validator

The value of the <message-bundle> entry can be obtained programmatically via Application#getMessageBundle(). You can in turn use it to obtain the actual bundle via the java.util.ResourceBundle API, along with UIViewRoot#getLocale(). This allows you to obtain a localized message in a custom converter and validator. Following is an example of such a validator, which checks if the specified e-mail address is already in use:

@FacesValidator(value = "duplicateEmailValidator", managed = true)
public class DuplicateEmailValidator implements Validator<String> {


    @Inject
    private UserService userService;


    @Override
    public void validate
        (FacesContext context, UIComponent component, String value)
            throws ValidatorException
    {
        if (value == null) {
            return;
        }


        Optional<User> user = userService.findByEmail(value);

        if (user.isPresent()) {
            throw new ValidatorException(new FacesMessage(getMessage(
                context, "message.duplicateEmailValidator")));
        }
    }


    public static String getMessage(FacesContext context, String key) {
        return ResourceBundle.getBundle(
            context.getApplication().getMessageBundle(),
            context.getViewRoot().getLocale()).getString(key);
    }
}

You might have noticed the new managed attribute of the @FacesValidator annotation . This will basically turn on CDI support on the validator instance and hence allow you to inject a business service into a validator. The same attribute is also available for @FacesConverter.

The shown validator example assumes that the following entry is present in the resource bundle files as identified by <message-bundle>:

message.duplicateEmailValidator = Email is already in use.

Localizing Enums

The cleanest approach to localize enums is to simply use their own identity as a bundle key. This keeps the enum class free of potential UI-specific clutter such as hard-coded bundle keys. Generally, the combination of the enum’s simple name and the enum value should suffice to represent a site-wide unique identifier (UI). Given the following com.example.project.model.Group enum representing a user group:

public enum Group {
    USER,
    MANAGER,
    ADMINISTRATOR,
    DEVELOPER;
}

and the following resource bundle entries in text.properties:

Group.USER = User
Group.MANAGER = Manager
Group.ADMINISTRATOR = Administrator
Group.DEVELOPER = Developer

you can easily localize them as follows:

<f:metadata>
    <f:importConstants type="com.example.project.model.Group" />
</f:metadata>
...
<h:selectManyCheckbox value="#{editUserBacking.user.groups}">
    <f:selectItems value="#{Group.values()}" var="group"
        itemLabel="#{text['Group.' += group]}" />
</h:selectManyCheckbox>

Note that the <f:importConstants> is new since JSF 2.3. It is required to be placed inside <f:metadata>. Its type attribute must represent the fully qualified name of the enum or any class or interface which contains public constants in flavor of public static final fields. <f:importConstants> will automatically import them into the EL scope as a Map<String, Object> wherein the map key represents the name of the constant as string and the map value represents the actual value of the constant.

With #{Group.values()} you can thus obtain a collection of all constant values and each value is then localized in itemLabel. Also note that itemValue is omitted as it defaults to the value of the var attribute which is already sufficient.

Parameterized Resource Bundle Values

You can also parameterize your resource bundle entries using the {0} placeholders of the java.text.MessageFormat API.8 They can on the JSF side only be substituted with <h:outputFormat> whereby the parameters are provided as <f:param> children, in the same order as the placeholders. Given the following entry:

page_products.table.header = There {0, choice, 0#are no products
                                             | 1#is one product
                                             | 1<are {0} products}.

it can be substituted using <h:outputFormat> as follows:

<h:outputFormat value="#{text['page_products.table.header']}">
    <f:param value="#{bean.products.size()}" />
</h:outputFormat>

Database-Based ResourceBundle

JSF also supports specifying a custom ResourceBundle implementation as <base-name>. This allows you to programmatically fill and supply the desired bundles, for example, from multiple bundle files, or even from a database. In this example we’ll replace the default properties file-based resource bundle by one which loads the entries from a database. This takes us a step further as to organizing the resource bundle keys. This way, you can even edit them via a web-based interface . Following is what the JPA entity looks like:

@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(columnNames = { "locale", "key" })
})
public class Translation {


    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;


    @Column(length = 5, nullable = false)
    private @NotNull Locale locale;


    @Column(length = 255, nullable = false)
    private @NotNull String key;


    @Lob @Column(nullable = false)
    private @NotNull String value;


    // Add/generate getters and setters here.
}

Note that the JPA (Java Persistence API) annotations provide sufficient hints as to what the DDL (Data Definition Language) of the table should look like. When having the property javax.persistence.schema-generation.database.action set to create or drop-and-create in persistence.xml, then it will automatically generate the proper DDL. For sake of completeness, here it is in HSQL/pgSQL flavor.

CREATE TABLE Translation (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    locale VARCHAR(5) NOT NULL,
    key VARCHAR(255) NOT NULL,
    value CLOB NOT NULL
);


ALTER TABLE Translation
    ADD CONSTRAINT UK_Translation_locale_key
    UNIQUE (locale, key);

And here’s what the EJB (Enterprise JavaBeans) service looks like.

@Stateless
public class TranslationService {


    @PersistenceContext
    private EntityManager entityManager;


    @TransactionAttribute(value = REQUIRES_NEW)
    @SuppressWarnings("unchecked")
    public Object[][] getContent
        (Locale locale, Locale fallback)
    {
        List<Object[]> resultList = entityManager.createQuery(
            "SELECT t1.key, COALESCE(t2.value, t1.value)"
                + " FROM Translation t1"
                + " LEFT OUTER JOIN Translation t2"
                    + " ON t2.key = t1.key"
                    + " AND t2.locale = :locale"
                + " WHERE t1.locale = :fallback")
            .setParameter("locale", locale)
            .setParameter("fallback", fallback)
            .getResultList();
        return resultList.toArray(new Object[resultList.size()][]);
    }
}

For JPA we only need an additional converter which converts between java.util. Locale in the model and VARCHAR in the database, which is represented by java.lang.String. You can use the JPA 2.0 AttributeConverter for this. It’s much like a JSF converter but for JPA entities. It’s relatively simple; there’s no additional configuration necessary. See the following:

public class LocaleConverter
    implements AttributeConverter<Locale, String>
{
    @Override
    public String convertToDatabaseColumn(Locale locale) {
        return locale.toLanguageTag();
    }


    @Override
    public Locale convertToEntityAttribute(String languageTag) {
        return Locale.forLanguageTag(languageTag);
    }
}

Now we have the custom ResourceBundle; it’s called Database ResourceBundle. Put it in the package com.example.project.i18n.

public class DatabaseResourceBundle extends ResourceBundle {

    private static final Control CONTROL = new DatabaseControl();

    @Override
    public Object handleGetObject(String key) {
        return getCurrentInstance().getObject(key);
    }


    @Override
    public Enumeration<String> getKeys() {
        return getCurrentInstance().getKeys();
    }


    private ResourceBundle getCurrentInstance() {
        FacesContext context = FacesContext.getCurrentInstance();
        String key = CONTROL.getClass().getName();
        return (ResourceBundle) context.getAttributes()
            .computeIfAbsent(key, k -> ResourceBundle.getBundle(key,
                context.getViewRoot().getLocale(),
                Thread.currentThread().getContextClassLoader(),
                CONTROL));
    }


    private static class DatabaseControl extends Control {
        @Override
        public ResourceBundle newBundle
            (String baseName, Locale locale, String format,
                ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException,
                IOException
        {
            FacesContext context = FacesContext.getCurrentInstance();
            final Object[][] contents = CDI.current()
                .select(TranslationService.class).get()
                .getContent(
                    locale,
                    context.getApplication().getDefaultLocale());
            return new ListResourceBundle() {
                @Override
                protected Object[][] getContents() {
                    return contents;
                }
            };
        }
    }
}

Finally, adjust the <resource-bundle><base-name> entry in faces-config.xml to specify the fully qualified name of the custom ResourceBundle as follows:

<base-name>com.example.project.i18n.DatabaseResourceBundle</base-name>

The actual implementation of this ResourceBundle is frankly somewhat hacky, only and only because of the following limitations:

  1. JSF doesn’t allow defining a custom ResourceBundle.Control via faces-config.xml.

  2. Providing a custom ResourceBundle.Control via SPI (Serial Peripheral Interface) as java.util.spi.ResourceBundleControlProvider doesn’t work from WAR on.

  3. Create multiple separate DatabaseResourceBundle subclasses for each single locale registered in faces-config.xml, such as DatabaseResourceBundle_en, DatabaseResourceBundle_pt_BR, and DataBaseResourceBundle_hi, in order to satisfy the default ResourceBundle.Control behavior is not maintenance friendly in long term.

An additional advantage of this approach is that it allows you to programmatically clear out any database bundles in the cache by simply calling ResourceBundle#clearCache(). Namely, the JSF implementation may in turn cache it in its Application implementation, causing the ResourceBundle#clearCache() to seem to have no effect at all. Mojarra is known to do that.9

HTML in ResourceBundle

This is a bad practice. It adds a maintenance burden. For large sections of content you’d better pick a more lightweight markup language than HTML , such as Markdown.10 This is not only safer as to XSS (cross-site scripting) risks but also easier for the user to edit via a text area in some Content Management System (CMS) screens. This is best to implement in combination with a database-based resource bundle. You could add an extra boolean flag to the Translation model indicating whether the value should be parsed as Markdown.

@Column(nullable = false)
private boolean markdown;

Then, inside TranslationService#getContents(), select it as the third column.

"SELECT t1.key, COALESCE(t2.value, t1.value), t1.markdown"

And, finally, in the DatabaseControl#newBundle() method, after retrieving the contents, you could postprocess them based on the boolean . You could use any Java-based Markdown library for this, such as CommonMark.11

static final Parser PARSER = Parser.builder().build();
static final HtmlRenderer RENDERER = HtmlRenderer.builder().build();
...
for (Object[] translation : contents) {
    if ((boolean) translation[2]) {
        translation[1] = RENDERER.render(PARSER.parse(translation[1]));
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.172.50