Search code examples
javainternationalizationjakarta-eeresourcebundle

Architecture setup for constantly changing text, property files? In a Java EE environment


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?


Solution

  • 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:

    1. ResourceBundle requires Fully Qualified Class Name (thus the package name).
    2. If you omit Locale parameter, default (which means Server) Locale would be used. Be sure to always pass Locale while instantiating ResourceBundle.
    3. 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.
    4. You should always attempt to create Locale objects passing both language and country code. That is just because, languages like Chinese Simplified (zh_CN) and Chinese Traditional (zh_TW) for example, are totally different languages (at least in terms of writing) and you would need to support two flavors of them. For other countries, ResourceBundle will actually load correct language resource automatically (note that I have created 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 :)