8.6 Formatted Messages
We've seen that in
order to internationalize programs, you must place all user-visible
messages into resource bundles. This is straightforward when the text
to be localized consists of simple labels such as those on buttons
and menu items. It is trickier, however, with messages that are
composed partially of static text and partially of dynamic values.
For example, a compiler might have to display a message such as
"Error at line 5 of file
"hello.java", in which the line number and filename
are dynamic and locale-independent, while the rest of the message is
static and needs to be localized.The MessageFormat
class of the java.text package helps tremendously
with these types of messages. To use it, you store only the static
parts of a message in the ResourceBundle and
include special characters that indicate where the dynamic parts of
the message are to be placed. For example, one resource bundle might
contain the message: "Error at line {0} of file
{1}". And another resource bundle might contain a
"translation" that looks like this:
"Erreur: {1}: {0}".To use such a localized message,
you create a MessageFormat object from the static
part of the message and then call its format( )
method, passing in an array of the values to be substituted. In this
case, the array contains an Integer object that
specifies the line number and a String object that
specifies the filename. The MessageFormat class
knows about other Format classes defined in
java.text. It creates and uses
NumberFormat objects to format numbers and
DateFormat objects to format dates and times. In
addition, you can design messages that create
ChoiceFormat objects to convert from numbers to
strings. This is useful when working with enumerated types, such as
numbers that correspond to month names, or when you need to use the
singular or plural form of a word based on the value of some number.Example 8-5 demonstrates this kind of
MessageFormat usage. It is a convenience class
with a single static method for the localized display of exception
and error messages. When invoked, the code attempts to load a
ResourceBundle with the basename
"Errors". If found, it looks up a
message resource using the class name of the exception object that
was passed. If such a resource is found, it displays the error
message. An array of five values is passed to the format(
) method. The localized error message can include any or
all of these arguments.The
LocalizedError.display( ) method defined in this
example was used in Example 8-2 at the beginning of
this chapter. The default Errors.properties
resource bundle used in conjunction with this example is shown
following the code listing. Error message display for the program is
nicely internationalized. Porting the program's
error message to a new locale is simply a matter of translating
(localizing) the Errors.properties file.
Example 8-5. LocalizedError.java
package je3.i18n;
import java.text.*;
import java.io.*;
import java.util.*;
/**
* A convenience class that can display a localized exception message
* depending on the class of the exception. It uses a MessageFormat,
* and passes five arguments that the localized message may include:
* {0}: the message included in the exception or error.
* {1}: the full class name of the exception or error.
* {2}: the file the exception occurred in
* {3}: a line number in that file.
* {4}: the current date and time.
* Messages are looked up in a ResourceBundle with the basename
* "Errors", using a the full class name of the exception object as
* the resource name. If no resource is found for a given exception
* class, the superclasses are checked.
**/
public class LocalizedError {
public static void display(Throwable error) {
ResourceBundle bundle;
// Try to get the resource bundle.
// If none, print the error in a nonlocalized way.
try {
String bundleName = "com.davidflanagan.examples.i18n.Errors";
bundle = ResourceBundle.getBundle(bundleName);
}
catch (MissingResourceException e) {
error.printStackTrace(System.err);
return;
}
// Look up a localized message resource in that bundle, using the
// classname of the error (or its superclasses) as the resource name.
// If no resource was found, display the error without localization.
String message = null;
Class c = error.getClass( );
while((message == null) && (c != Object.class)) {
try { message = bundle.getString(c.getName( )); }
catch (MissingResourceException e) { c = c.getSuperclass( ); }
}
if (message == null) { error.printStackTrace(System.err); return; }
// Get the filename and linenumber for the exception
// In Java 1.4, this is easy, but in prior releases, we had to try
// parsing the output Throwable.printStackTrace( );
StackTraceElement frame = error.getStackTrace( )[0]; // Java 1.4
String filename = frame.getFileName( );
int linenum = frame.getLineNumber( );
// Set up an array of arguments to use with the message
String errmsg = error.getMessage( );
Object[ ] args = {
((errmsg!= null)?errmsg:"), error.getClass( ).getName( ),
filename, new Integer(linenum), new Date( )
};
// Finally, display the localized error message, using
// MessageFormat.format( ) to substitute the arguments into the message.
System.err.println(MessageFormat.format(message, args));
}
/**
* This is a simple test program that demonstrates the display( ) method.
* You can use it to generate and display a FileNotFoundException or an
* ArrayIndexOutOfBoundsException
**/
public static void main(String[ ] args) {
try { FileReader in = new FileReader(args[0]); }
catch(Exception e) { LocalizedError.display(e); }
}
}
The
following listing shows the resource bundle properties file used to
localize the set of possible error messages that can be thrown by the
ConvertEncoding class of Example 8-2:
#
# This is the file Errors.properties
# One property for each class of exceptions that our program might
# report. Note the use of backslashes to continue long lines onto the
# next. Also note the use of \n and \t for newlines and tabs
#
java.io.FileNotFoundException: Error:
File "{0}" not found\n\tError occurred
at line {3} of file "{2}"\n\tat {4}
java.io.UnsupportedEncodingException: Error:
Specified encoding not supported\n
tError occurred at line {3} of file "{2}"\n\tat {4,time} on {4,date}
java.io.CharConversionException:Error:
Character conversion failure.
Input data is not in specified format.
# A generic resource.
Display a message for any error or exception that
# is not handled by a more specific resource.
java.lang.Throwable:Error: {1}:
{0}\n\tError occurred at line {3} of file "{2}"
n\t{4,time,long} {4,date,long}
With a resource bundle like this, ConvertEncoding
produces error messages like the following:
Error: File "myfile (No such file or directory)" not found
Error occurred at line 64 of file "FileInputStream.java"
at 7/9/00 9:28 PM
Or, if the current locale is fr_FR:
Error: File "myfile (Aucun fichier ou repertoire de ce type)" not found
Error occurred at line 64 of file "FileInputStream.java"
at 09/07/00 21:28