In a Java EE environment, we are normally used to storing text in a property/resource file. And that property file is associated with some view HTML markup file. E.g. if your label 'First Name' changes to 'Full Name' on a HTML page, you could use the property to make that update.
firstName=First Name
someOtherData=This is the data to display on screen, from property file
If you are in an environment, where it is difficult to update those property files on a regular basis, what architecture are developers using to change text/label content that would normally reside in a property file? Or let's say you need to change that content before redeploying a property file change. A bad solution is to store that in a database? Are developers using memcache? Is that usually used for caching solutions?
Edit-1 A database is really not designed for this type of task (pulling text to display on the screen), but there are use-cases for a database. I can add a locale column or state field, also add a column filter by group. If I don't use a database or property file, what distributed key/value solution would allow me to add custom filters?
Edit-2 Would you use a solution outside of the java framework? Like a key/value datastore? memcachedb?
I want to assure you that if you need constant changes on localized texts, for example they tend to differ from deployment to deployment, database is the way to go. Well, not just the database, you need to cache your strings somehow. And of course you wouldn't want to totally re-build your resource access layer, I suppose.
For that I can suggest extending ResourceBundle
class to automatically load strings from database and store it in WeakHashMap
. I choose WeakHashMap
because of its features - it removes a key from the map when it is no longer needed reducing memory footprint. Anyway, you need to create an accessor class. Since you mentioned J2EE, which is pretty ancient technology, I will give you Java SE 1.4 compatible example (it could be easily re-worked for newer Java, just put @Override when needed and add some String generalization to Enumeration):
public class WeakResourceBundle extends ResourceBundle {
private Map cache = new WeakHashMap();
protected Locale locale = Locale.US; // default fall-back locale
// required - Base is abstract
// @Override
protected Object handleGetObject(String key) {
if (cache.containsKey(key))
return cache.get(key);
String value = loadFromDatabase(key, locale);
cache.put(key, value);
return value;
}
// required - Base is abstract
// @Override
public Enumeration getKeys() {
return loadKeysFromDatabase();
}
// optional but I believe needed
// @Override
public Locale getLocale() {
return locale;
}
// dummy testing method, you need to provide your own
// should throw MissingResourceException if key does not exist
private String loadFromDatabase(String key, Locale aLocale) {
System.out.println("Loading key: " + key
+ " from database for locale:"
+ aLocale );
return "dummy_" + aLocale.getDisplayLanguage(aLocale);
}
// dummy testing method, you need to provide your own
private Enumeration loadKeysFromDatabase() {
return Collections.enumeration(new ArrayList());
}
}
Because of some strange ResourceBundle's loading rules, you would actually need to extend WeakResourceBundle
class to create one class each for supported languages:
// Empty Base class for Invariant Language (usually English-US) resources
// Do not need to modify anything here since I already set fall-back language
package com.example.i18n;
public class MyBundle extends WeakResourceBundle {
}
One supported language each (I know it sucks):
// Example class for Polish ResourceBundles
package com.example.i18n;
import java.util.Locale;
public class MyBundle_pl extends WeakResourceBundle {
public MyBundle_pl() {
super();
locale = new Locale("pl");
}
}
Now, if you need to instantiate your ResourceBundle
, you would only call:
// You probably need to get Locale from web browser
Locale polishLocale = new Locale("pl", "PL");
ResourceBundle myBundle = ResourceBundle.getBundle(
"com.example.i18n.MyBundle", polishLocale);
And to access the key:
String someValue = myBundle.getString("some.key");
Possible gotchas:
ResourceBundle
requires Fully Qualified Class Name (thus the package name).Locale
parameter, default (which means Server) Locale
would be used. Be sure to always pass Locale
while instantiating ResourceBundle
.myBundle.getString()
could throw MissingResourceException
if you follow my suggestion. You would need to use try-catch block to avoid problems. Instead you may decide on returning some dummy string from database access layer in the event of missing key (like return "!" + key + "!"
) but either way it should probably be logged as an error.MyBundle_pl.java
, not MyBundle_pl_PL.java
and it still works. Also, ResourceBundle would automatically fall-back to Enlish-US (MyBundle.java
) if there is no resource class for given language (that is why I used such a strange class hierarchy).EDIT
Some random thoughts about how to make it more awsome.
Static factories (avoid using ResourceBundle directly)
Instead of directly instantiating the bundles with ResourceBundle, you could add static factory method(s):
public static ResourceBundle getInstance(Locale aLocale) {
return ResourceBundle.getBundle("com.example.i18n.MyBundle", aLocale);
}
If you decide to change the name of WeakResourceBundle
class to something more appropriate (I decided to use LocalizationProvider
), you could now easily instantiate your bundles from consuming code:
ResourceBundle myBundle = LocalizationProvider.getInstance(polishLocale);
Auto-generated resource classes
Localized MyBundle
classes could be easily generated via building script. The script could be either configuration file or database driven - it somehow needs to know which Locale are in use within the system. Either way, the classes share very similar code, so generating them automatically really makes sense.
Auto-detecting Locale
Since you are the one that implement the class, you have full control of its behavior. Therefore (knowing your application architecture) you can include Locale detection here and modify getInstance()
to actually load appropriate language resources automatically.
Implement additional Localization-related methods
There are common tasks that needs to be done in Localized application - formatting and parsing dates, numbers, currencies, etc. are usual examples. Having end user's Locale in place, you can simply wrap such methods in LocalizationProvider.
Gee, I really love my job :)