Resource Bundle Tricks and Best Practices
Join the DZone community and get the full member experience.
Join For FreeToday is resource bundle day. This is the most well known mechanism for internationalization (i18n) in Java in general. Working with it should be a breeze. But there are many little questions that come up while getting your hands dirty with it. If you are feeling the same, this post is for you.
Basics
The java.util.ResourceBundle defines a standardized way for accessing translations in java. They contain locale-specific resources. Resource bundles belong to families whose members share a common base name, but whose names also have additional components that identify
their locales. Each resource bundle in a family contains the same items, but the items have been translated for the locale represented by that resource bundle. Those are key/value pairs. The keys uniquely identify a locale-specific object in the bundle.
The most basic example uses the following familiy:
Messages.properties
Messages_de.properties
Messages_en.properties
If you need to query a bundle in your application you simple call the
ResourceBundle bundle = ResourceBundle.getBundle("Messages");
method and query the returned bundle:
bundle.getString("welcome.message");
If you are wondering which Locale is going to be used here, you are right. The String constructor implicitly uses Locale.getDefault() to resolve the language. That might not be what you want. So you should ResourceBundle bundle =
ResourceBundle.getBundle("Messages", locale);
You cannot set the locale after you have retrieved the bundle. Every ResourceBundle has one defined locale.
Naming stuff
Some thoughts about naming. Name the bundle properties after their contents. You can go a more general way by simply naming them "Messages" and "Errors" etc. but it also is possible to have a bundle per subsystem or component. Whatever fit's your needs. Maintaining the contents isn't easy with lots of entries. So any kind of contextual split makes developers happy. The bundle properties files are equivalent to classes; Name them accordingly. And further on you should find a common system for naming your keys. Depending on the split you have chosen for the property files you might also introduce some kind of subsystem or component namespace with your keys. Page prefixes are also possible. Think about this wisely and play around with it. You are aiming to have least possible dublicates in your keys.
Encapsulating
As you have seen, you use the string representation of the bundles a lot. The fact that those are actually file-names (or better class-names) you would be better of with a simple enum which encapsulates everything for you:
public enum ResourceBundles { MESSAGES("Messages"), ERRORS("Errors"); private String bundleName; ResourceBundles(String bundleName) { this.bundleName = bundleName; } public String getBundleName() { return bundleName; } @Override public String toString() { return bundleName; } }
Having this you simply can write
ResourceBundle bundle = ResourceBundle.getBundle(MESSAGES.getBundleName());
Java Server Faces and ResourceBundles
public void countryLocaleCodeChanged(ValueChangeEvent e) { String newLocaleValue = e.getNewValue().toString(); //loop country map to compare the locale code for (Map.Entry<String, Object> entry : countries.entrySet()) { if (entry.getValue().toString().equals(newLocaleValue)) { FacesContext.getCurrentInstance() .getViewRoot().setLocale((Locale) entry.getValue()); } } }
To use resource bundles in your jsf based application you simple have to define them in your faces-config.xml and use the shortcuts in your xhtml files.
<resource-bundle> <base-name>Messages</base-name> <var>msgs</var>
<h:outputLabel value="#{msgs['welcome.general']}" />
JSF takes care of the rest. What about parameter substitution? Think about a key-value pair like the following:
welcome.name=Hi {0}! How are you?
You can pass the parameter via the f:param tag:
<h:outputFormat value="#{msgs['welcome.name']}"> <f:param value="Markus" /> </h:outputFormat>
To change the language you have to set a specific locale for your current FacesContext instance. It's best to do this via a value change listener:
public void countryLocaleCodeChanged(ValueChangeEvent e) { String newLocaleValue = e.getNewValue().toString(); //loop country map to compare the locale code for (Map.Entry<String, Object> entry : countries.entrySet()) { if (entry.getValue().toString().equals(newLocaleValue)) { FacesContext.getCurrentInstance() .getViewRoot().setLocale((Locale) entry.getValue()); } } }
Resource Bundles in EJBs
JSF obviously is very easily integrated. What about using those bundles in EJBs? It is basically the same. You have the same mechanisms in place to get hand on the bundle and use it. There is one thing that you should keep in mind. You probably don't want to always use the default locale. So you have to find a way to pass the locale down from the UI. If you are thinking about @Injecting the MessageBundle via a @Produces annotation you have to think more than one time. Especially if you are working with @Stateless EJBs. Those instances get pooled and you have to pass the Locale to any business method that needs to know about the current Locale. You typically would do this with a parameter object or some kind of user session profile. Don't add the Locale as method signature all over.
Resource Bundles from the DB
In most of the cases I see you need to pull the keys from a DB. Given the inner workings of the ResourceBundle (one "class" per locale) you end up having to implement the logic in your own ResourceBundle implementation. Most of the examples you find on the web do this by overriding the handleGetObject(String key) method. I don't like this approach, especially since we have a far better way using the ResourceBundle.Control mechanism. Now you can override the newBundle() method and return your own ResourceBundle implementation. All you have to do is to set your own Control as a parent with your DatabaseResourceBundle:
public DatabaseResourceBundle() { setParent(ResourceBundle.getBundle(BUNDLE_NAME, FacesContext.getCurrentInstance().getViewRoot().getLocale(), new DBControl())); }
The DBControl returns MyResourceBundle which is a ListResourceBundle:
protected class DBControl extends Control { @Override public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { return new MyResources(locale); } /** * A simple ListResourceBundle */ protected class MyResources extends ListResourceBundle { private Locale locale; /** * ResourceBundle constructor with locale * * @param locale */ public MyResources(Locale locale) { this.locale = locale; } @Override protected Object[][] getContents() { TypedQuery<ResourceEntity> query = _entityManager.createNamedQuery("ResourceEntity.findForLocale", ResourceEntity.class); query.setParameter("locale", locale); List<ResourceEntity> resources = query.getResultList(); Object[][] all = new Object[resources.size()][2]; int i = 0; for (Iterator<ResourceEntity> it = resources.iterator(); it.hasNext();) { ResourceEntity resource = it.next(); all[i] = new Object[]{resource.getKey(), resource.getValue()}; values.put(resource.getKey(), resource.getValue()); i++; } return all; } } }
As you can see, this is backed by an entitymanager and a simple ResourceEntity which has all the fields and NamedQueries necessary for building up the different bundles.
@Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(name = "i18n_key") private String key; @Column(name = "i18n_value") private String value; @Column(name = "i18n_locale") private Locale locale;
By putting the bundles in a private Map<String, String> values = new HashMap<String, String>(); you also have a good way of caching the results after the bundles have been build up for the first time.
This still isn't the best solution as ResourceBundles have a way of caching. But I might dig into this in more detail later. Until now, this bundle is cached forever (or at least until the next redeployment).
Rewrite as Language Switch
On last thing to mention is that you also could have some fancy add-ons here. If you already have the JSF language switch magic in place it is simple to add ocpsoft's rewrite to your application. This is a simple way to encode the language in the URLs like this http://yourhost.com/Bundle-Provider-Tricks/en/index.html
All you have to do is to add rewrite to the game by adding two simple dependencies:
<dependency> <groupId>org.ocpsoft.rewrite</groupId> <artifactId>rewrite-servlet</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.ocpsoft.rewrite</groupId> <artifactId>rewrite-integration-faces</artifactId> <version>1.1.0.Final</version> </dependency>
Rewrite needs you to add your own ConfigurationProvider which is the central place to hold your rewriting rules. Implement the following:
public class BundleTricksProvider extends HttpConfigurationProvider { @Override public Configuration getConfiguration(ServletContext context) { return ConfigurationBuilder.begin() // Locale Switch .addRule(Join.path("/{locale}/{page}.html").to("/{page}.xhtml") .where("page").matches(".*") .where("locale").bindsTo(PhaseBinding.to(El.property("#{languageSwitch.localeCode}")).after(PhaseId.RESTORE_VIEW))); } @Override public int priority() { return 10; } }
Next is to add a file named "org.ocpsoft.rewrite.config.ConfigurationProvider" to your META-INF/services folder and put the fully qualified name of your ConfigurationProvider implementation there. One last thing to tweak is the logic in the LanguageSwitch bean. Rewrite isn't able to trigger a ValueChangeEvent (as far as I know :)) so you have to add some magic to change the Locale while the setter is called. That's it .. very easy!
This is all for today. Find the code on github.com. Happy to read about your thoughts.
Published at DZone with permission of Markus Eisele, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments