LaVOZs

The World’s Largest Online Community for Developers

'; java - Locales and ResourceBundles in a plugin-based program - LavOzs.Com

We need to start adding internationalisation to our program. Thankfully not the whole thing yet, just a few bits, but I want the way we do it to scale up to potentially cover the whole program. The thing is, our program is based on plugins, so not all strings belong in the same place.

As far as I understand it, Java's ResourceBundle work like this. You create a class that extends ResourceBundle, called something like MyProgramStrings, and also language-specific classes called MyProgramStrings_fr, MyProgramStrings_es etc. Each of these classes maps keys (strings) to values (any object). It's up to each of these classes where to get its data from, but a common place for them is a properties file.

You look up values in two stages: first you get the correct bundle, then you query it for the string you want.

Locale locale = Locale.getDefault(); // or = new Locale("en", "GB");
ResourceBundle rb = ResourceBundle.getBundle("MyProgramStrings", locale);
String wotsitName = rb.getString("wotsit.name");

However, what we need is to combine the results of several locales into a single resource space. For example, a plugin needs to be able to override a string that's already defined, and have that new value returned whenever code looks up the string.

I'm a little lost in all this. Can anybody help?


Update: David Waters asked:

I have put my answer at the bottom but I would be interested in hearing how you solved this problem.

Well, we haven't got very far yet - long term WIBNIs always fall victim to the latest crisis - but we're basing it on the interface that a plugin implements, with the convention that resources have the same fully qualified name as the interface.

So an interface UsersAPI may have various different implementations. A method getBundle() on that interface by default returns the equivalent of ResourceBundle.get("...UsersAPI", locale). That file can be replaced, or implementations of UsersAPI can override the method if they need something more complicated.

So far that does what we need, but we're still looking at more flexible solutions based on the plugins.

You don't have to implement ResourceBundles as a series of classes, with one class per locale (i.e. a class named MyProgramStrings, MyProgramStrings_fr, MyProgramStrings_de). The ResourceBundle class will fall back to using properties files if need be:

public static void main(String[] args) {

    ResourceBundle bundle = ResourceBundle.getBundle("MyResources");
    System.out.println("got bundle: " + bundle);

    String valueInBundle = bundle.getString("someKey");
    System.out.println("Value in bundle is: " + valueInBundle);
}

If I have a file named MyResources.properties on the classpath, then this method will result in:

got bundle: java.util.PropertyResourceBundle@42e816  
Value in bundle is: someValue

As for setting up a hierarchy of bundles, or "merging" them together, I'm afraid I can't help much there, except that I know that Spring does have a concept of hierchical MessageSources (link to API) which are implemented on top of java.util.ResourceBundle, so perhaps you can use Spring's functionality to achieve what you want?

BTW, here is the relevant part of the ResourceBundle.getBundle() javadoc that explains it's "search and instantiation strategy":

http://java.sun.com/j2se/1.5.0/docs/api/java/util/ResourceBundle.html#getBundle(java.lang.String, java.util.Locale, java.lang.ClassLoader)

  • First, it attempts to load a class using the candidate bundle name. If such a class can be found and loaded using the specified class loader, is assignment compatible with ResourceBundle, is accessible from ResourceBundle, and can be instantiated, getBundle creates a new instance of this class and uses it as the result resource bundle.
  • Otherwise, getBundle attempts to locate a property resource file. It generates a path name from the candidate bundle name by replacing all "." characters with "/" and appending the string ".properties". It attempts to find a "resource" with this name using ClassLoader.getResource. (Note that a "resource" in the sense of getResource has nothing to do with the contents of a resource bundle, it is just a container of data, such as a file.) If it finds a "resource", it attempts to create a new PropertyResourceBundle instance from its contents. If successful, this instance becomes the result resource bundle.

I know Struts and Spring have something for that. But let's say you can't use Struts or Spring then what I would do is to create a subclass of ResourceBundle and load the *.properties (one per plugin) in this ResourceBundle. Then you can use

ResourceBundle bundle = ResourceBundle.getBundle("MyResources");

Like you would normally do with a property file.

Of course, you should cached the result so you don't have to search each Properties every time.

getBundle loads a set of candidate bundle files generated using the specified locale (if any) and the default locale.

Typically, a resource file with no locale info has the "default" values (eg, perhaps en_US values are in MyResources.properties), then over riding resource values are created for different locales (eg ja strings are in MyResources_ja.properties) Any resource in the more specific file overrides the less specific file properties.

Now you want to add in the ability for each plug-in to provide it's own set of properties files. It isn't clear whether the plug-in would be able to modify the resources of the main app or other plug-ins, but it sounds like you want to have a singleton class where each plug-in can register its base resource file name. All requests for property values go through that singleton, which would then look through the resource bundles of each plug in (in some order ...) and finally the app itself for the property value.

The Netbeans NbBundle class does similar stuff.

I have had a slightly similar problem see question 653682 . The solution I found was to have a class that extends ResourceBundle and, checks if we are overriding the value if not delegate to the PropertyResourceBundle generated from files.

So if you wanted to store Labels for the UI you would have a class com.example.UILabels and a properties file com.example.UILabelsFiles.

package com.example;

public class UILabels extends ResourceBundle{
    // plugins call this method to register there own resource bundles to override
    public static void addPluginResourceBundle(String bundleName){
        extensionBundles.add(bundleName);
    }

    // Find the base Resources via standard Resource loading
    private ResourceBundle getFileResources(){
        return ResourceBundle.getBundle("com.example.UILabelsFile", this.getLocale());
    }
    private ResourceBundle getExtensionResources(String bundleName){
        return ResourceBundle.getBundle(bundleName, this.getLocale());
    }

    ...
    protected Object handleGetObject(String key){
        // If there is an extension value use that
        Object extensionValue = getValueFromExtensionBundles(key);
        if(extensionValues != null)
            return extensionValues;
        // otherwise use the one defined in the property files
        return getFileResources().getObject(key);
    }

    //Returns the first extension value found for this key, 
    //will return null if not found    
    //will return the first added if there are multiple.
    private Object getValueFromExtensionBundles(String key){
        for(String bundleName : extensionBundles){
            ResourceBundle rb = getExtensionResources(bundleName);
            Object o = rb.getObject(key);
            if(o != null) return o;
        }
        return null;
    }    

}
Related
How to fix a locale setting warning from Perl?
How to use UTF-8 in resource properties with ResourceBundle
How to add local jar files to a Maven project?
Generate a Properties object from a ResourceBundle?
Loading ResourceBundle object in Spring
Loading a ResourceBundle within an OSGi bundle
Static ResourceBundle
Why is “final” not allowed in Java 8 interface methods?
Trouble getting ResourceBundle to find bundle. How can I debug this?
ResourceBundle does not correspond to the requested locale