Search code examples
databasejsfjpainternationalizationresourcebundle

internationalization in JSF with ResourceBundle entries which are loaded from database


I'm working on a Java EE6 project using JPA/EJB/JSF and I'm having some trouble designing multiple language support for entities. There are three relevant entities:

Language (has id)
Competence (has id)
CompetenceName (has Competence reference, Language reference and a String)

Competence has a one-to-many reference to CompetenceName implemented with a Map, containing one object for every Language that there exists a name for a Competence. Note that competences are created dynamically and their names can thus not exist in a resource bundle.

When listing the Competences on a web page, I want them to show with the language of the currently logged in user, this is stored in a Session Scoped Managed Bean.

Is there any good way to accomplish this without breaking good MVC design? My first idea was to get the session scoped bean directly from a "getName" method in the Competence entity via FacesContext, and look in the map of CompetenceNames for it as following:

public class Competence
{
...
@MapKey(name="language")
@OneToMany(mappedBy="competence", cascade=CascadeType.ALL, orphanRemoval=true)
private Map<Language, CompetenceName> competenceNames;

public String getName(String controller){
    FacesContext context = FacesContext.getCurrentInstance();
    ELResolver resolver = context.getApplication().getELResolver();
    SessionController sc = (SessionController)resolver.getValue(context.getELContext(), null, "sessionController");
    Language language = sc.getLoggedInUser().getLanguage();
    if(competenceNames.get(language) != null)
        return competenceNames.get(language).getName();
    else
        return "resource missing";
}

This solution feels extremly crude since the entity relies on the Controller layer, and have to fetch a session controller every time I want its name. A more MVC compliant solution would be to take a Language parameter, but this means that every single call from JSF will have to include the language fetched from the session scoped managed bean which does not feel like a good solution either.

Does anyone have any thoughts or design patterns for this issue?


Solution

  • Internationalization/localization should preferably be entirely done in the view side. The model shouldn't be aware of this.

    In JSF, the <resource-bundle> entry in faces-config.xml and the <f:loadBundle> in XHTML can also point to a fullworthy ResourceBundle class instead of basename of .properties files. In Java SE 6 there's a new ResourceBundle.Control API available which allows full control over loading and filling the bundle.

    Knowing those facts, it should be possible to load the bundle messages from the DB with a custom ResourceBundle and Control. Here's a kickoff example:

    public class CompetenceBundle extends ResourceBundle {
    
        protected static final String BASE_NAME = "Competence.messages"; // Can be name of @NamedQuery
        protected static final Control DB_CONTROL = new DBControl();
    
        private Map<String, String> messages;
    
        public CompetenceBundle() {
            setParent(ResourceBundle.getBundle(BASE_NAME, 
                FacesContext.getCurrentInstance().getViewRoot().getLocale(), DB_CONTROL));
        }
    
        protected CompetenceBundle(Map<String, String> messages) {
            this.messages = messages;
        }
    
        @Override
        protected Object handleGetObject(String key) {
            return messages != null ? messages.get(key) : parent.getObject(key);
        }
    
        @Override
        public Enumeration<String> getKeys() {
            return messages != null ? Collections.enumeration(messages.keySet()) : parent.getKeys();
        }
    
        protected static class DBControl extends Control {
    
            @Override
            public ResourceBundle newBundle
                (String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
                    throws IllegalAccessException, InstantiationException, IOException
            {
                String language = locale.getLanguage();
                Map<String, String> messages = getItSomehow(baseName, language); // Do your JPA thing. The baseName can be used as @NamedQuery name.
                return new CompetenceBundle(messages);
            }
    
        }
    
    }
    

    This way you can declare it as follows in faces-config.xml:

    <resource-bundle>
        <base-name>com.example.i18n.CompetenceBundle</base-name>
        <var>competenceBundle</var>
    </resource-bundle>
    

    Or as follows in the Facelet:

    <f:loadBundle basename="com.example.i18n.CompetenceBundle" var="competenceBundle" />
    

    Either way, you can use it the usual way:

    <h:outputText value="#{competenceBundle.name}" />