11

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?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Rasmus Franke
  • 4,434
  • 8
  • 45
  • 62

2 Answers2

22

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}" />
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Thanks for the answer, this definetly looks interesting. One question about bundles though: what scope do they have? Lets say one user changes the name of a competence and calls newBundle to refresh his changes. Will this reflect the bundles of ALL users, or just his? – Rasmus Franke Dec 21 '10 at 15:23
  • The bundles are basically applicationwide cached by basename and locale. So, any change will be reflected to all users. – BalusC Dec 21 '10 at 15:25
  • 1
    I have implemented this now, and it feels a lot better than having the model ask the controllers of language details. I'm having some trouble when renaming a competence though, calling CompetenceBundle.clearCache(); clears the cache when i read the bundle from java, but the web site still shows the old value. Does the application server have its own cache somehow? – Rasmus Franke Dec 22 '10 at 09:27
  • I guess that this is to be handled at JPA level. – BalusC Dec 22 '10 at 11:08
  • @BalusC To this work, I also need to create several subclasses with the language and country suffix, like _pt_BR. Otherwise the custom DBControl is not set. Thank you. – John John Pichler Sep 28 '17 at 13:37
0

I would move the language specific part from your model into recource bundles. Just model Competence, Language and User. If a user requests a page, you display his competence and lookup the language specific competence (CompetenceName) from the recource bundle.

I searched for sample code to get you started and found this, see Listing 19.16 customerDetails.jsp.

Something like:

<fmt:message key="${competence}" />
remipod
  • 11,269
  • 1
  • 22
  • 25
  • The problem is that Competences are not static, an administrator can at any time add new competences or competenceNames for existing competences to the system. From my knowledge, a bundle cannot be written to programmaticly. – Rasmus Franke Dec 21 '10 at 14:15
  • jdk 6 allows to reload resource bundles: http://stackoverflow.com/questions/156586/in-java-how-to-reload-dynamically-resources-bundles-in-a-web-application – remipod Dec 21 '10 at 16:09