We have implemented an internationalisation interface to Log4J. We are using a special class, called MessageCode to handle that. We use the category name to store the message's identifier and the text to store the parameters ('|' seperated).
The localisation can then be done directly in an LocalisingAppender (using a ResourceBundle) or at read time using a special reader. The main problem is that it creates one Logger per type of message => two class creation (the MessageCode helper object and the Logger) per message type. For our project, this extra memory usage was not a big issue, and the use of one Logger per MessageCode eases support and implementation. We declare all the possible MessageCodes in interfaces: public interface CatICParser { public static final MessageCode INVALID_XML = MessageCode.getMessageCode("2.1.0.1"); public static final MessageCode INVALID_HML = MessageCode.getMessageCode("2.1.0.2"); } Then, usage in the code is then very easy: CatICParser.INVALID_XML.generate(fileName); In case two parameters are required: CatICParser.INVALID_HML.generate(new String[]{fileName,tagName}); or CatICParser.INVALID_HML.generate(fileName,tagName); There is currently no check for the number of parameters for each message type. But this wont be a major issue, as the 'localizer' supports missing/extra parameters. I am planning to have a small program looking in the code and verifying the number of parameters used for each generate message, but nothing is done yet. We declare all the messages (code (identifier), list of required parameters, and text in different languages) in a XML file. Some XSLT, then creates the ResourceBundles and the Java interfaces declaring the MessageCode instance (static final, named CatXXX). I am working on a good documentation on this class, and if I get the authorisation from management, I will provide full code for it, and if Ceki is interested, it could be included as a Log4J Add-on. For the moment all I can offer is this short introduction and an uggly copy paste from our internal doc (see below). If you are interested for more information, just contact me. Benoit 1. Logging Implementation usage 1.1 Introduction v All messages that can be send must be of a predeclared Message Code. (There is one message code per message that can be sent). There is an exception for pure tracing messages that are used for the developer at development time.<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> v Message codes are defined in a centralised XML file and are identified by a unique id v Message codes are grouped by categories which can inturn be grouped in super-categories v A message code include different texts (title, description, trouble shooting, ...), which can all be caracterised. v Messages are localised at read time (not at generation time): only the message code's id, parameters and context info are stored in file or db. There is a small sample project using this library and you should use it as a base. It is situated in : W:\work\prototype\src\common\logging 1.2 Definition of messages Messages are defined in a messageList.xml file, which contains all the information about the messages. This XML file is then passed through various XSLT to generate: v The Java interfaces defining the messages v The message_XX.properties, containing the localisation strings for each message. v SQL statements inserting the message localisation strings in the database (to be implemented) In a previous version, the Log4j configuration file was generated from this file. As it increases loading time and does not give much extra flexibility, this was forgotten. 1.2.1 The Message definition file: messageList.xml This is the file that defines the list of messages. There should be one messageList.xml file per module or even by sub module in certain cases. The only limitation is that the messagecategory hierarchy (concatanation of category codes) must not clash. You will find the commented XML Schema in W:\work\epostmessenger\src\common\logging\xslt and the generated documentation in W:\work\epostmessenger\src\common\logging\doc (both in PVCS). Here is a sample file. <?xml version="1.0" encoding="UTF-8"?> <!-- edited with XML Spy v4.3 U (http://www.xmlspy.com) by Benoit Voisin (ErgoIDP) --> <!-- * XML file containing the initial Log4J configuration and the list of messages * It is used by: * - genJavaClasses.xsl to generate the CatXXX interface defining the legal messages * - genResourceBundle.xsl to generate the Message_en.properties containing the localised text of the messages in a ResourceBundle */ --> <messagelist javaSrcDir="." javaPackageName="logging.logcat" categoryPrefix="Id" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../epostmessenger/src/common/logg ing/xslt/messageList.xsd"> <!--javaSrcDir: root src where the java interfaces should be stored(usually "." as this file is stored in the src directory) javaPackageName: package to which all java interface should be placed in categoryPrefix: Prefix to add to the Category code (Usually "Id")--> <messagecategory code="1"> <!-- code: identifier of this category within its mother category javaname: the name of the Java interface that will represent this category threshold: Log4J Level below which messages will be forgotten--> <javaname>CatIC</javaname> <threshold>INFO</threshold> <text lang="en"> <!--Localised text, in the language in lang--> <displayname>General IC</displayname> </text> <messagecategory code="1"> <javaname>CatICParser</javaname> <threshold>INFO</threshold> <text lang="en"> <displayname>IC Parser</displayname> </text> <message code="1"> <javaname>INVALID_XML</javaname> <messagelevel>WARN</messagelevel> <nbparams>1</nbparams> <text lang="en"> <!--Localised text, in the language in lang--> <displayname>Invalid XML</displayname> <displaymessage>The file provided is not valid XML :{0}</displaymessage> </text> <!--code: identifier of this message within its mother category javaname: the name of the constant in the Java interface that will represent this message(in capital letters) messagelevel: the Log4J Level of this message--> </message> <message code="2"> <javaname>INVALID_HML</javaname> <messagelevel>INFO</messagelevel> <nbparams>2</nbparams> <text lang="en"> <displayname>Invalid HML</displayname> <displaymessage>The file provided is not valid HML. There is an error on line {1} of file {0}</displaymessage> </text> </message> </messagecategory> </messagecategory> </messagelist> 1.2.2 The Java interfaces CatXXX.java A series of Java interfaces acting as enumeration of MessageCodes will be created from this XML file: /** * file generated by genJavaClasses.xsl from a message.xml file */ package dk.ergoidp.epostmessenger.prototype.util.logging.category; import dk.ergoidp.epostmessenger.prototype.util.logging.MessageCode; public interface CatICParser { MessageCode INVALID_XML = MessageCode.getMessageCode("2.1.0.1"); MessageCode INVALID_HML = MessageCode.getMessageCode("2.1.0.2"); } Then, when a developer needs to generate a message stating the XML is invalid, he just has to write this line: CatICParser.INVALID_XML.generate(fileName); In case two parameters are required: CatICParser.INVALID_HML.generate(new String[]{fileName,tagName}); or CatICParser.INVALID_HML.generate(fileName,tagName); Generate methods exists with 1,2, or 3 parameters. 1.2.3 Generating the Java interfaces and the ResourceBundle They are generated using XSLTs over the messageList.xml file: v genJavaClasses.xsl à Cat*.java v genResourceBundle.xsl à Messages_en.properties (for development time translation of messages) Soon, other will generate SQL statements to insert the localized texts into the database The easiest way is to launch theses transformations is to use Ant. Below is a sample build.xml file: <?xml version="1.0" encoding="windows-1252"?> <project name="LoggingImplementation" default="genAllLogging" basedir="."> <description> Ant file used to launch several actions. Compiling and running is still done within JDevelopper, and not here. For the moment: - Doxygen : generate Doxygen documentation on ./src - JavaDoc : generate JavaDoc documentation on ./src - Jalopy : format the code according to certain standards - genAllLogging : Generates : - genJavaClasses : the CatXXX interfaces file (enumeration of the different messages) - genLog4J : classes/Log4J.xml - genResourceBundle : Resource_en.properties (the list of localized messages) </description> <!-- set global properties for this build --> <taskdef name="jalopy" classname="de.hunsicker.jalopy.plugin.ant.AntPlugin"/> <property name="messageListFile" location="src/prototypeMessageList.xml"/> <property name="srcDir" location="src"/> <property name="ePostMessengerRoot" location="../../../../epostmessenger"/> <property name="libDir" location="${ePostMessengerRoot}/src/lib"/> <property name="tempDir" location="temp"/> <property name="buildDir" location="classes"/> <property name="messageXsltDir" location="${ePostMessengerRoot}/src/common/logging/xslt"/> <property name="dist" location="dist"/> <target name="init"> <!-- Create the time stamp --> <tstamp/> <!-- Create the build directory structure used by compile --> <mkdir dir="${buildDir}"/> <mkdir dir="${tempDir}"/> </target> <!-- ==================================================================== --> <!-- Logging code + resource generation --> <!-- ==================================================================== --> <target name="genAllLogging" description="generates the Java interfaces enumarating the MessageCode and the ResourceBundle" depends="init,genJavaClasses,genResourceBundle"/> <target name="genJavaClasses" description="run genJavaClasses.xsl on log4j_pre.xml to generate the Java classes. A temporary file is created and cut into the .java files using bvo.utils.FileCutter" depends="init"> <xslt in="${messageListFile}" out="${tempDir}/JavaFiles.txt" style="${messageXsltDir}/genJavaClasses.xsl"/> <echo message="XSLT done"/> <java fork="true" dir="${srcDir}" classname="dk.ergoidp.util.FileCutter" classpath="${libDir}/ErgoIdpUtils.jar"> <arg value="${tempDir}/JavaFiles.txt"/> <arg value="///Start of:"/> <arg value="///EOF"/> </java> </target> <target name="genResourceBundle" description="run genResourceBundle.xsl on log4j_pre.xml to generate the Messages_en.properties resource file" depends="init"> <xslt in="${messageListFile}" out="${buildDir}/Messages_en.properties" style="${messageXsltDir}/genResourceBundle.xsl"/> </target> </project> Note that genJavaClasses.xsl is not able to cut in different files. Instead it generates one single JavaFiles.txt, that is cut by a home made FileCutter class. Also, Ant 1.5 or later is required for the xslt task used in this build.xml file. 1.2.4 The Message Lookup Addin To ease developers knowing the parameters to include in each message (this cannot be checked at compile time), an JDeveloper addin can help them find the related display text: When the cursor is on a line containing the start of a generate call, this Addin finds the closest messageList.xml file and shows the display text (for english) of the message code. On the line: CatICParser.INVALID_HML ... a call to the Addin (Default Ctrl-L) displays in the status bar: CatICParser.INVALID_HML=>The file provided is not valid HML. There is an error on line {1} of file {0} [logged at:INFO] 1.2.5 The Localising appender - Messages_en.properties To ease development and understanding of the messages generated by the code, Log4J can be configured to copy localised versions of the messages in the standard out. Look at the Log4J configuration file below for the LOCALISED_STDOUT appender or at the the dk.ergoidp.epostmessenger.prototype.util.logging.LocalisingPatternLayout API documentation for more information. Note that you need to have generated the Messages_en.properties (see above) and that it should be accessible as a resource by the JVM (in the class path). 1.2.6 Context information Context information, such as, the main request, batch Id, ... are to be set in the code using Log4J's MDC. This context information will be stored along with the message code and parameters. The keys are to be defined, depending on the needs. 1.2.7 Pure tracing (debugging) For pure tracing statements, it is not required to declare MessageCodes. It will be too heavy to use and not very usefull, as pure tracing statements never have to be translated. In this case, pure Log4J calls can be used : In each class where tracing has to be used, insert the line: static final org.apache.log4j.Logger debugLogger = org.apache.log4j.Logger.getLogger(CLASS_NAME.class); And trace using for example: debugLogger.error("Should never happen",exception); debugLogger.debug("Entered myProcedure(" + param1 + ")"); In a development configuration, these messages will be logged in the Standard Output. It should still be much prefered over System.out.println, since at config or run time, the tracing level for each class can be adjusted. System.out.println should not be used in the code. 1.3 The Log4J configuration file Log4j.xml It defines the different Log4J Loggers corresponding to the message codes and message categories, respecting the category hierarchy. There is one Log4J logger per message code and per message category, but they do not have to be defined in this file. Only the root category and the one with specific configuration have to. Here is a sample file: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "H:\Tech\Documentation\JavaLibs\jakarta-log4j-1.2rc1\src\java\org\apache\log 4j\xml\log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <!-- appender writing to the STD OUT (System.out)--> <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d %-5p [%t] %c '%m'\n"/> </layout> </appender> <!-- appender writing to the STD OUT but translating the message codes into english text --> <appender name="LOCALISED_STDOUT" class="org.apache.log4j.ConsoleAppender"> <layout class="dk.ergoidp.epostmessenger.common.logging.LocalisingPatternLayout"> <param name="ConversionPattern" value="%d %-5p [%t] %c: %z\n"/> <!-- the %z represents the localized text --> <param name="LocaleLanguage" value="en"/> <!-- language --> <param name="ResourceName" value="Messages"/> <!-- ResourceBundle of messages --> </layout> </appender> <!-- ================================================================= Root Logger for all MessageCodes. Additivity =false so that it does not take into account the appender of the rot category(below) ================================================================= --> <logger name="Id" additivity="false"> <!-- all message codes starting with 'Id.' they will be taken into account here --> <appender-ref ref="LOCALISED_STDOUT"/> </logger> <!-- if uncommented, will specialize tracing for the package logging <logger name="logging" > <level value="DEBUG"/> </logger> --> <!-- if uncommented, will specialize tracing for the class logging.LogTester <logger name="logging.LogTester" > <level value="DEBUG"/> </logger> --> <!-- ================================================================= Global Root category We ask it to be forget any message below INFO level. ================================================================= --> <root> <level value="INFO"/> <appender-ref ref="STDOUT"/> </root> </log4j:configuration>