views:

261

answers:

2

We had a lot of strings which contained the same sub-string, from sentences about checking the log or how to contact support, to branding-like strings containing the company or product name. The repetition was causing a few issues for ourselves (primarily typos or copy/paste errors) but it also causes issues in that it increases the amount of text our translator has to translate.

The solution I came up with went something like this:

public class ExpandingResourceBundleControl extends ResourceBundle.Control {
  public static final ResourceBundle.Control EXPANDING =
    new ExpandingResourceBundleControl();

  private ExpandingResourceBundleControl() { }

  @Override
  public ResourceBundle newBundle(String baseName, Locale locale, String format,
                                  ClassLoader loader, boolean reload)
    throws IllegalAccessException, InstantiationException, IOException {

      ResourceBundle inner = super.newBundle(baseName, locale, format, loader, reload);
      return inner == null ? null : new ExpandingResourceBundle(inner, loader);
  }
}

ExpandingResourceBundle delegates to the real resource bundle but performs conversion of {{this.kind.of.thing}} to look up the key in the resources.

Every time you want to get one of these, you have to go:

ResourceBundle.getBundle("com/acme/app/Bundle", EXPANDING);

And this works fine -- for a while.

What eventually happens is that some new code (in our case autogenerated code which was spat out of Matisse) looks up the same resource bundle without specifying the custom control. This appears to be non-reproducible if you write a simple unit test which calls it with and then without, but it occurs when the application is run for real. Somehow the cache inside ResourceBundle ejects the good value and replaces it with the broken one. I am yet to figure out why and Sun's jar files were compiled without debug info so debugging it is a chore.

My questions:

  1. Is there some way of globally setting the default ResourceBundle.Control that I might not be aware of? That would solve everything rather elegantly.

  2. Is there some other way of handling this kind of thing elegantly, perhaps without tampering with the ResourceBundle classes at all?

A: 

If the string repetitions are localized in the sense that you know a certain string will be repeated but only within the same project such that sharing resource bundles is not a design nightmare, then you might consider breaking the strings down into multiple key-value parts. Separate the parts that repeat from those that do not and reuse the repeated parts. For example, lets say you have the following two strings you need to display:

  1. "The Red-capped Robin is a small passerine bird native to Australia. "
  2. "The Red-capped Robin is found in dryer regions across much of the continent."

The resource bundle could be as follows:

robin.name=The Red-capped Robin
robin.native=is a small passerine bird native to Australia.
robin.region=is found in dryer regions across much of the continent.

and then combine the required parts where needed bundle.getString("robin.name")+bundle.getString(robin.native).

One thing you need to be careful about though is that the grammar rules like subject predicate order etc. might not be the same in all languages. So you would need to be a little careful when splitting sentences.

Prachi
Actually that's also a bad idea for different contexts (e.g. on a menu or in a status bar), the gender of the subject, and plurality (1, 2, 27, etc), since these also change depending on the culture.
devstuff
This is actually the precise reason we went with the approach of doing all the insertions from the ResourceBundle side. With our current approach that example would be done by putting {{robin.name}} into the appropriate place in the values below it. If someone encountered a language where it wouldn't work, they have the power to remove the variable entirely. But... our solution is of course tricky to make work correctly.
Trejkaz
+1  A: 

I think this is a fundamental flaw in the way ResourceBundles are designed to function: keys that reference other keys automatically violate the DRY (don't repeat yourself) principle. The way I got around this was similar to your method: create a ReflectiveResourceBundle class that allows you to specify Resource keys in the messages using EL notation.

THE WRONG WAY:

my.name.first=Bob
my.name.last=Smith
my.name.full=Bob Smith

THE RIGHT WAY:

my.name.first=Bob
my.name.last=Smith
my.name.full=${my.name.first} ${my.name.last}

I've uploaded the code to Google Code so you or anyone else can download it. Additionally, I've added some sample code for anyone using the Stripes Framework (http://www.stripesframework.org/) to get you quickly up-and-running.

The trick to getting this to work with standard JSTL fmt taglibs was to set up an interceptor that replaced the HttpServletRequest's resource with our own. The code looks something like this:

ResourceBundle bundle = MyStaticResourceHoldingTheBundle.getBundle();
Config.set(request, Config.FMT_LOCALIZATION_CONTEXT, new LocalizationContext(bundle, locale));

Take a look at the stripes.interceptor package in the link above for more details.

Matt Brock
You're right, this is very similar to our solution, and it has the same drawback which is that generated code using ResourceBundle.getBundle() will not receive the workaround. The primary difference is that in our case we use the existing ResourceBundle cache whereas in your case you keep the cache separately, presumably to work around Sun's cache occasionally clobbering the cached bundles.
Trejkaz