Java makes the string-replacement part of this pretty easy to do with its ResourceBundle class. If you use ResourceBundle consistently, you can easily ship localizations for multiple languages bundled with your application, and other people can take your program and resource files and localize them for other languages without having to recompile your program.
If you have thousands of strings being translated into dozens of languages, you might want to get yourself a sophisticated translation management system or ship around XLIFF files (or perhaps you would like to help write an open source localization repository). But for smaller projects such as I have worked on, I have found that some relatively simple support methods and a bit of convention have served me pretty well.
Contents
- Building The Resources
- The Basic Interface
- The Basic Implementation
- Field Names In Format Strings
- Runtime Overriding Of Resources
- Recursive Resources
Building The Resources
To simplify loading resources, I find it easiest to put them all into one properties file per locale. But to improve maintainability, it is best to keep the resources in multiple files close to the source files in which they are used. I bridge these two conflicting goals by keeping my resources in multiple properties files, then concatenating them all together into a single properties file when I build my application.To avoid confusion and emphasize the fact that my properties source files are not used in their source file form, I use the extension
.props
for those files.
My concatenation program collects all of the .props
files
for one locale into one .properties
file.
I run it once for each locale for which I have files.
The concatenation program
also adds separators and source filenames in comments between
the contents of each source file,
and converts non-ascii characters to backslash-u notation.
While I am at it, I also stuff some other information into the generated
properties files, such as version and date information, so that I can
easily access those details at runtime.
In conformance with the standard per-locale naming convention for resource bundle properties files, for each
.props
base file there can also be locale-specific translations.
For example, for the base file Foo.props
there could be
Foo_hu.props
with Hungarian translations,
Foo_fr.props
with French translations, and
Foo_fr_CA.props
with French-Canadian translations.
The properties file with the collected resources goes into a file in the same directory as the class files and gets packaged up in the jar file along with the class files. For example, I might name the collected resources file
net/jimmc/app/Resources.properties
,
so it goes in the directory with all of the files in the
net.jimmc.app
package.
When I load it, I specify its location as
net.jimmc.app.Resources
and the
ResourceBundle
code automatically finds it from my jar file,
as shown in the initResources
method below.
The Basic Interface
In order to be able to use the simplest possible code when calling to retrieve resources in my Java apps, I use support code, shown below, to do the following:- Encapsulate my ResourceBundle in the support code so that the caller does not need to worry about where the resources come from.
- Make my resource retrieval functions so that, when a key is not found in the resources, it returns the key as the value of the method rather than throwing an exception. The key then appears in my GUI, where I can see it and correct it later when I want, rather than causing an exception that stops the program and forces me to correct the string immediately before I proceed.
- Retrieve a resource string and format it with one or more arguments in one call.
ResourceSource
interface
with definitions for methods to implement the above functionality:
public interface ResourceSource { /** Get a string from resources. * @param name The resource name. * @return The value of the resource, * or the name if no value is found. */ public String getResourceString(String name); /** Get a string from resources, or null if not found. * @param name The resource name. * @return The value of the resource, * or null if no value is found. */ public String getResourceStringOrNull(String name); /** Get a string from a resource and pass it to MessageFormat.format * with the specified arguments, returning the result. * @param name The resource name. * @param args The args to MessageFormat.format. * @return The formatted resource string. */ public String getResourceFormatted(String name, Object[] args); /** Get a string from a resource and pass it to MessageFormat.format * with the specified one argument put into a new Object[1], * returning the result. * @param name The resource name. * @param arg The argument to MessageFormat.format. * @return The formatted resource string. */ public String getResourceFormatted(String name, Object arg); }
The Basic Implementation
My Application object, a singleton, implements myResourceSource
interface
and is the source for resource values for the application.
(This is conceptually the same as the IResources
interface I mentioned in a
previous post.)
It contains an internal reference to my resources, so that the
caller does not need to worry about that:
During initialization of myimport java.text.MessageFormat; import java.util.ResourceBundle; private ResourceBundle resources;
Application
object, I call a
simple initResources
method to load my resources
from that one file containing the concatenated contents of all
of the props
files:
Myprivate void initResources() { resources = ResourceBundle.getBundle("net.jimmc.app.Resources"); }
Application
object has a private method that accesses my
resources, which all of my other Application
resource methods call (to simplify adding
other functionality later):
The methods in theprivate String getResourceValue(String name) throws MissingResourceException { return resources.getString(name); }
Application
object
that implement the ResourceSource
interface
all call getResourceValue
to get the resource value:
public String getResourceString(String name) { try { return getResourceValue(name); } catch (MissingResourceException ex) { return name; //use the resource name } } public String getResourceStringOrNull(String name) { try { return getResourceValue(name); } catch (MissingResourceException ex) { return null; } } public String getResourceFormatted(String name, Object[] args){ String fmt = getResourceString(name); return MessageFormat.format(fmt,args); } public String getResourceFormatted(String name, Object arg) { String fmt = getResourceString(name); Object[] args = { arg }; return MessageFormat.format(fmt,args); }
Field Names In Format Strings
When using the formatting methods injava.text.MessageFormat
you specify the locations of replacement values using numbers
in braces, so your formatting string might contain phrases such as
{1}
or {2,date,medium}
.
You have to ensure that these index numbers correspond properly to
the arguments in the source code that uses that format string.
Since these format strings are in a different file
(the resource properties file) than the code
that uses them,
extra care is required to avoid mixing up the
numbers, such as when adding or modifying the argument list.
To minimize this problem, I add a version of getResourceFormatted
that allows using field names rather than numbers in the
format strings, so they contain phrases such as
{count}
and {modtime,date,medium}
.
When calling these methods, the fieldNames
array contains
names that correspond to the args
of the same index.
/** Get a string from the resource file, map fields names to numbers, * and format it with the given arguments. */ public String getResourceFormatted(String name, Object[] args, String[] fieldNames){ String fmt = getResourceFormat(name,fieldNames); return MessageFormat.format(fmt,args); } /** Get a resource string and map field names to numbers. */ public String getResourceFormat(String name, String[] fieldNames) { String fmt = getResourceString(name); return mapFieldNamesToNumbers(fmt, fieldNames); } /** Replace named MessageFormat field references by field numbers. * The named field reference looks just like a regular format * segment, but with a string in place of the number. */ public String mapFieldNamesToNumbers(String s, String[] fieldNames) { for (int i=0; i<fieldNames.length; i++) { String fieldName = fieldNames[i]; if (fieldName==null || fieldName.equals("")) continue; //skip blanks in the array String p = "\\{"+fieldName+"\\}"; String q = "{"+i+"}"; s = s.replaceAll(p,q); p = "\\{"+fieldName+"\\,"; q = "{"+i+","; s = s.replaceAll(p,q); } return s; }
Runtime Overriding Of Resources
In order to allow loading additional resources at run time, for debugging, I add an additional variable to store my override resources:I then add a method, which gets called from a debugging command to which I have specified a resources properties file name, to load values from the specified properties file into my override resources:private Properties resourceProperties;
I modify my privatepublic void loadResourceProperties(String filename) { if (resourceProperties==null) resourceProperties = new Properties(); try { FileInputStream ifile = new FileInputStream(filename); resourceProperties.load(ifile); String msg = getResourceFormatted("info.LoadedResourceProperties", filename); startupMessage(msg); } catch (Exception ex) { String msg = getResourceFormatted( "error.LoadingResourceProperties",filename); throw new RuntimeException(msg,ex); } }
getResourceValue
method
to look in my override properties before using the regular properties:
private String getResourceValue(String name) throws MissingResourceException { if (resourceProperties!=null) { String v = resourceProperties.getProperty(name); if (v!=null) return v; //found an override property value } return resources.getString(name); }
Recursive Resources
For some applications I want to be able to define resource strings that reference other resource strings or context values provided by the application. The standard replacement text in a message format starts with an open brace followed by a number. In my resource files, after implementing my Field Name extension above, I can also follow an open brace by a field name. With the extension in this section, I can follow an open brace by the "@" character and a context name or the name of another resource. The values for the context names are passed to the formatting methods in a Map.For example, if I have these resources:
and I create a context Map withSample.abc.foo=A formatted string with {@Sample.abc.bar} for {@User} Sample.abc.bar=a nested property
User=Jim
,
then using the methods in this section I can call
getResourceFormattedRecurse
passing name as "Sample.abc.foo"
along with my context and get back
"A formatted string with a nested property for Jim".
To implement this capability, I add the following to my
ResourceSource
interface:
I add the corresponding code to my/** Get a string from resources, recursing to do replacement of * special patterns with other resources. */ public String getResourceStringRecurse(String name); /** Get a formatted string from the resource file, including * recursive replacement of special strings. */ public String getResourceFormattedRecurse(String name, Object[] args, String[] fieldNames, Map ctx, int maxRecurse);
Application
object:
public final static int DEFAULT_MAX_RESOURCE_RECURSE = 20; /** Get the value of a resource, replacing the special tags. * @see #replaceResourceRecurse */ public String getResourceStringRecurse(String name) { String value = getResourceStringOrNull(name); if (value==null) return name; return replaceResourceRecurse(value); } /** Get a formatted string from the resource file, including * recursive replacement of special strings. */ public String getResourceFormattedRecurse(String name, Object[] args, String[] fieldNames, Map ctx, int maxRecurse) { String fmt = getResourceString(name); if (maxRecurse>0) fmt = replaceResourceRecurse(fmt, ctx, maxRecurse); if (fieldNames!=null) fmt = mapFieldNamesToNumbers(fmt, fieldNames); return MessageFormat.format(fmt,args); } /** Given a string, look for occurrences of our special replacement * syntax and replace with the referenced value, with a * default recursion limit. * @param s The string through which we look for our special * replacement tags. The replacement tags are of the * form {@XXX} where XXX is the name of the field with * which to replace that tag. * @throws RecursionLimitException if the maximum recursion depth * is exceeded. * @see #replaceResourceRecurse(String,Items,int) */ public String replaceResourceRecurse(String s) { return replaceResourceRecurse(s, null, DEFAULT_MAX_RESOURCE_RECURSE); } /** Given a string, look for occurrences of our special replacement * syntax and replace with the referenced value. * @param s The string through which we look for our special * replacement tags. The replacement tags are of the * form {@XXX} where XXX is the name of the field with * which to replace that tag. * @param ctx The context dictionary for replacement. We first look * for XXX in this dictionary; if not found, we look for * a resource named XXX. May be null. * @param maxRecurse The maximum recursion depth. * @throws RecursionLimitException if the maximum recursion depth * is exceeded. */ public String replaceResourceRecurse(String s, Map ctx, int maxRecurse){ if (s==null) return s; int x = s.indexOf("{@"); //look for our replacements if (x<0) return s; //no replacements StringBuffer sb = new StringBuffer(); int a = 0; while (x>=0 && x<s.length()) { sb.append(s.substring(a,x)); int y = s.indexOf("}",x); if (y<0) { String eMsg = "Missing close brace "+ "in resource after "+ s.substring(x,s.length()); //TBD i18n throw new RuntimeException(eMsg); } String name = s.substring(x+2,y); String v = null; if (ctx!=null) //try the context if we have one v = (String)ctx.get(name); if (v==null) { //If no context property, try resource v = getResourceStringOrNull(name); } if (v!=null) { if (maxRecurse<=0) { throw new RecursionLimitException(name); } //Found the specified resource, recurse on it and //replace specials in it before we plug it into our string. try { v = replaceResourceRecurse(v,ctx,maxRecurse-1); } catch (RecursionLimitException ex) { //If we exceeded the recursion limit, we will be popping //back through here many times; we add on the name of //each item in the recursion chain to add in debugging. ex.setMessage(name+"->"+ex.getMessage()); throw ex; } sb.append(v); } a = y+1; x = s.indexOf("{@",a); } if (a<s.length()) sb.append(s.substring(a)); return sb.toString(); }
RecursionLimitException
is a simple exception class that
also allows setting the error message:
public class RecursionLimitException extends RuntimeException { private String ourMessage; /** Create a RecursionLimitException. */ public RecursionLimitException(String message) { super(message); } public void setMessage(String msg) { this.ourMessage = msg; } public String getMessage() { if (ourMessage!=null) return ourMessage; else return super.getMessage(); } }
No comments:
Post a Comment