views:

1180

answers:

4

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.

+2  A: 
matt b
So they properties file is actually mixed in among the sources, and therefore among the classes, and looked up by fully-qualified path name? I just tried that and it worked. Thanks, that makes it clearer and may be the beginnings of a solution...
Marcus Downing
Yep, that's one way you could do it - and probably the simplest.
matt b
For internationalization, we have found it easier to use external properties files so we don't have to send out sources to the translators.
Chris Morley
+1  A: 

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.

Julien Grenier
I'm going to need some clarification. When do I instanciate the subclass? How does ResourceBundle.getBundle know of its existence?
Marcus Downing
ResourceBundle.getBundle() searches the classpath for a class whose name matches "the candidate bundle name" ... take a look at the ResourceBundle javadoc link I posted above for a full and more complete explanation
matt b
+1  A: 

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.

Chris Morley
+1  A: 

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;
    }    

}
David Waters