cocoon-cvs mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ilgro...@apache.org
Subject svn commit: r1156644 [1/2] - in /cocoon/cocoon3/trunk: cocoon-sample/rcl-config/WEB-INF/classes/ cocoon-sample/src/main/resources/COB-INF/ cocoon-sample/src/main/resources/COB-INF/i18n/ cocoon-sax/src/main/java/org/apache/cocoon/sax/component/ cocoon-s...
Date Thu, 11 Aug 2011 15:11:05 GMT
Author: ilgrosso
Date: Thu Aug 11 15:11:04 2011
New Revision: 1156644

URL: http://svn.apache.org/viewvc?rev=1156644&view=rev
Log:
COCOON3-64 #resolve

Added:
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base.xml
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_en.properties
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_it.properties
    cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/component/I18nTransformer.java
    cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/util/VariableExpressionTokenizer.java
    cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/xml/
    cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/xml/sax/
    cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/xml/sax/ParamSAXBuffer.java
    cocoon/cocoon3/trunk/cocoon-sax/src/test/java/org/apache/cocoon/sax/component/I18NTransformerTest.java
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/base.xml
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/base_en.properties
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/base_it.properties
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/formatting.xml
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/formatting_en.properties
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/translate.xml
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/translate_de.properties
    cocoon/cocoon3/trunk/cocoon-sax/src/test/resources/i18n/translate_en.properties
Modified:
    cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/classes/logback.xml
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/overview.html
    cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/sitemap.xmap
    cocoon/cocoon3/trunk/cocoon-sax/src/test/java/org/apache/cocoon/sax/component/LinkRewriterTransformerTest.java
    cocoon/cocoon3/trunk/cocoon-sax/src/test/java/org/apache/cocoon/sax/component/LogAsXMLTransformerTestCase.java
    cocoon/cocoon3/trunk/cocoon-sitemap/src/main/resources/META-INF/cocoon/spring/cocoon-pipeline-component.xml

Modified: cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/classes/logback.xml
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/classes/logback.xml?rev=1156644&r1=1156643&r2=1156644&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/classes/logback.xml (original)
+++ cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/classes/logback.xml Thu Aug 11 15:11:04 2011
@@ -34,9 +34,21 @@
         <level value="DEBUG"/>
         <appender-ref ref="CORE"/>
     </logger>
+    <logger name="org.apache.cocoon.sitemap.node" additivity="false">
+        <level value="ERROR"/>
+        <appender-ref ref="CORE"/>
+    </logger>
+    <logger name="org.apache.cocoon.profiling" additivity="false">
+        <level value="ERROR"/>
+        <appender-ref ref="CORE"/>
+    </logger>
+    <logger name="org.apache.cocoon.spring.configurator" additivity="false">
+        <level value="ERROR"/>
+        <appender-ref ref="CORE"/>
+    </logger>
     
     <root>
         <level value="WARN"/>
         <appender-ref ref="CORE"/>
     </root>
-</configuration>
\ No newline at end of file
+</configuration>

Added: cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base.xml
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base.xml?rev=1156644&view=auto
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base.xml (added)
+++ cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base.xml Thu Aug 11 15:11:04 2011
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+  
+  http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<test xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xmlns:i18n="http://apache.org/cocoon/i18n/3.0">
+
+    <para title="first" name="article" i18n:attr="title name">
+        <i18n:text>This text will be translated.</i18n:text>
+        <br/>
+        <i18n:text i18n:key="key_text">Default value</i18n:text>
+    </para>
+</test>

Added: cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_en.properties
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_en.properties?rev=1156644&view=auto
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_en.properties (added)
+++ cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_en.properties Thu Aug 11 15:11:04 2011
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+first=First
+This\ text\ will\ be\ translated.=Sample text
+article=Article
+key_text=A value

Added: cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_it.properties
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_it.properties?rev=1156644&view=auto
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_it.properties (added)
+++ cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/i18n/base_it.properties Thu Aug 11 15:11:04 2011
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+first=Primo
+This\ text\ will\ be\ translated.=Testo di esempio
+article=Articolo
+key_text=

Modified: cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/overview.html
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/overview.html?rev=1156644&r1=1156643&r2=1156644&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/overview.html (original)
+++ cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/overview.html Thu Aug 11 15:11:04 2011
@@ -79,6 +79,11 @@
   <ul>
     <li><a href="object-model/request-parameters?a=1&b=2&c=3">All request parameters</a>: Print all request parameters.</li>
   </ul>
+  <h2>i18N</h2>
+  <ul>
+    <li><a href="i18n/localefrombrowser">Get locale from browser's preferred</a>: Translate with locale from browser's preferred.</li>
+    <li><a href="i18n/localefromparameter?lang=it">Get locale from request parameter</a>: Translate with locale from request parameter.</li>
+  </ul>
   <h2>Advanced Matching</h2>
   <p>(using a JEXL expression to set a test value)</p>
   <ul>

Modified: cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/sitemap.xmap
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/sitemap.xmap?rev=1156644&r1=1156643&r2=1156644&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/sitemap.xmap (original)
+++ cocoon/cocoon3/trunk/cocoon-sample/src/main/resources/COB-INF/sitemap.xmap Thu Aug 11 15:11:04 2011
@@ -424,6 +424,26 @@
       </map:match>
     </map:pipeline>
 
+    <!-- ~~~~~~~~~~~~~~~~ i18N ~~~~~~~~~~~~~~~ -->
+    <map:pipeline>
+      <map:match equals="i18n/localefrombrowser">
+        <map:generate src="i18n/base.xml" />
+        <map:transform type="i18n">
+          <map:parameter name="locale" value="{jexl:cocoon.request.locale}" />
+          <map:parameter name="bundle" value="COB-INF/i18n/base" />
+        </map:transform>
+        <map:serialize type="xml" />
+      </map:match>
+      <map:match equals="i18n/localefromparameter">
+        <map:generate src="i18n/base.xml" />
+        <map:transform type="i18n">
+          <map:parameter name="locale" value="{jexl:cocoon.request.lang}" />
+          <map:parameter name="bundle" value="COB-INF/i18n/base" />
+        </map:transform>
+        <map:serialize type="xml" />
+      </map:match>
+    </map:pipeline>
+    
     <!-- ~~~~~~~~~~~~~~~~ controller ~~~~~~~~~~~~~~~ -->
     <map:pipeline>
       <map:match pattern="controller/conditional-get/{id}/{name}">

Added: cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/component/I18nTransformer.java
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/component/I18nTransformer.java?rev=1156644&view=auto
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/component/I18nTransformer.java (added)
+++ cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/component/I18nTransformer.java Thu Aug 11 15:11:04 2011
@@ -0,0 +1,1905 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cocoon.sax.component;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.StringTokenizer;
+import org.apache.cocoon.pipeline.SetupException;
+import org.apache.cocoon.pipeline.caching.CacheKey;
+import org.apache.cocoon.pipeline.caching.ParameterCacheKey;
+import org.apache.cocoon.pipeline.component.CachingPipelineComponent;
+import org.apache.cocoon.sax.AbstractSAXTransformer;
+import org.apache.cocoon.sax.util.VariableExpressionTokenizer;
+import org.apache.cocoon.sax.util.VariableExpressionTokenizer.TokenReceiver;
+import org.apache.cocoon.xml.sax.ParamSAXBuffer;
+import org.apache.cocoon.xml.sax.SAXBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+/**
+ * Internationalization transformer is used to transform i18n markup into text
+ * based on a particular locale.
+ *
+ * <h3>Overview</h3>
+ * <p>The i18n transformer works by finding a translation for the user's locale
+ * in the passed resource bundles {@link java.util.ResourceBundle}.
+ * Locale is passed as parameter to the transformer.</p>
+ *
+ * <p>For the passed local it then attempts to find a resource bundle that
+ * satisifies the locale, and uses it for for processing text replacement
+ * directed by i18n markup.</p>
+ *
+ * <h3>Usage</h3>
+ * <p>Files to be translated contain the following markup:
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * ... some text, translate &lt;i18n:text&gt;key&lt;/i18n:text&gt;
+ * </pre>
+ * At runtime, the i18n transformer will find a resource bundle for the
+ * user's locale, and will appropriately replace the text between the
+ * <code>&lt;i18n:text&gt;</code> markup, using the value between the tags as
+ * the lookup key.</p>
+ *
+ * <h3>Pipeline Usage</h3>
+ * <p>To use the transformer in a pipeline, simply specify it in a particular
+ * transform, and pass locale, budle and (optional) untraslated parameters:
+ * <pre>
+ * &lt;map:match pattern="file"&gt;
+ *   &lt;map:generate src="file.xml"/&gt;
+ *   &lt;map:transform type="i18n"&gt;
+ *     &lt;map:parameter name="locale" value="..."/&gt;
+ *     &lt;map:parameter name="bundle" value="..."/&gt;
+ *     &lt;map:parameter name="untraslated" value="..."/&gt;
+ *   &lt;/map:transform&gt;
+ *   &lt;map:serialize/&gt;
+ * &lt;/map:match&gt;
+ * </pre>
+ *
+ * <h3>i18n markup</h3>
+ *
+ * <p>For date, time and number formatting use the following tags:
+ * <ul>
+ *  <li><strong>&lt;i18n:date/&gt;</strong> gives localized date.</li>
+ *  <li><strong>&lt;i18n:date-time/&gt;</strong> gives localized date and time.</li>
+ *  <li><strong>&lt;i18n:time/&gt;</strong> gives localized time.</li>
+ *  <li><strong>&lt;i18n:number/&gt;</strong> gives localized number.</li>
+ *  <li><strong>&lt;i18n:currency/&gt;</strong> gives localized currency.</li>
+ *  <li><strong>&lt;i18n:percent/&gt;</strong> gives localized percent.</li>
+ * </ul>
+ * Elements <code>date</code>, <code>date-time</code> and <code>time</code>
+ * accept <code>pattern</code> and <code>src-pattern</code> attribute, with
+ * values of:
+ * <ul>
+ *  <li><code>short</code>
+ *  <li><code>medium</code>
+ *  <li><code>long</code>
+ *  <li><code>full</code>
+ * </ul>
+ * See {@link java.text.DateFormat} for more info on these values.</p>
+ *
+ * <p>Elements <code>date</code>, <code>date-time</code>, <code>time</code> and
+ * <code>number</code>, a different <code>locale</code> and
+ * <code>source-locale</code> can be specified:
+ * <pre>
+ * &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
+ *   12/24/11
+ * &lt;/i18n:date&gt;
+ * </pre>
+ * Will result in 24.12.2011.</p>
+ *
+ * <p>A given real <code>pattern</code> and <code>src-pattern</code> (not
+ * keywords <code>short, medium, long, full</code>) overrides any value
+ * specified by <code>locale</code> and <code>src-locale</code> attributes.</p>
+ */
+public class I18nTransformer extends AbstractSAXTransformer
+        implements CachingPipelineComponent {
+
+    /**
+     * This class log.
+     */
+    private static final Logger LOG =
+            LoggerFactory.getLogger(I18nTransformer.class);
+
+    public static final String NS_I18N =
+            "http://apache.org/cocoon/i18n/3.0";
+
+    /**
+     * Locale string delimiter
+     */
+    private static final String LOCALE_DELIMITER = "_-@.";
+
+    //
+    // i18n elements
+    //
+    /**
+     * <code>i18n:text</code> element is used to translate any text, with
+     * or without markup. Example:
+     * <pre>
+     *   &lt;i18n:text&gt;
+     *     This is &lt;strong&gt;translated&lt;/strong&gt; string.
+     *   &lt;/i18n:text&gt;
+     * </pre>
+     */
+    public static final String ELEM_TEXT = "text";
+
+    /**
+     * <code>i18n:translate</code> element is used to translate text with
+     * parameter substitution. Example:
+     * <pre>
+     * &lt;i18n:translate&gt;
+     *   &lt;i18n:text&gt;This is translated string with {0} param&lt;/i18n:text&gt;
+     *   &lt;i18n:param&gt;1&lt;/i18n:param&gt;
+     * &lt;/i18n:translate&gt;
+     * </pre>
+     * The <code>i18n:text</code> fragment can include markup and parameters
+     * at any place. Also do parameters, which can include <code>i18n:text</code>,
+     * <code>i18n:date</code>, etc. elements (without keys only).
+     *
+     * @see #ELEM_TEXT
+     * @see #ELEM_PARAM
+     */
+    public static final String ELEM_TRANSLATE = "translate";
+
+    /**
+     * <code>i18n:choose</code> element is used to translate elements in-place.
+     * The first <code>i18n:when</code> element matching the current locale
+     * is selected and the others are discarded.
+     *
+     * <p>To specify what to do if no locale matched, simply add a node with
+     * <code>locale="*"</code>. <em>Note that this element must be the last
+     * child of &lt;i18n:choose&gt;.</em></p>
+     * <pre>
+     * &lt;i18n:choose&gt;
+     *   &lt;i18n:when locale="en"&gt;
+     *     Good Morning
+     *   &lt;/en&gt;
+     *   &lt;i18n:when locale="fr"&gt;
+     *     Bonjour
+     *   &lt;/jp&gt;
+     *   &lt;i18n:when locale="jp"&gt;
+     *     Aligato?
+     *   &lt;/jp&gt;
+     *   &lt;i18n:otherwise&gt;
+     *     Sorry, i don't know how to say hello in your language
+     *   &lt;/jp&gt;
+     * &lt;i18n:translate&gt;
+     * </pre>
+     * <p>You can include any markup within <code>i18n:when</code> elements,
+     * with the exception of other <code>i18n:*</code> elements.</p>
+     *
+     * @see #ELEM_IF
+     * @see #ATTR_LOCALE
+     * @since 2.1
+     */
+    public static final String ELEM_CHOOSE = "choose";
+
+    /**
+     * <code>i18n:when</code> is used to test a locale.
+     * It can be used within <code>i18:choose</code> elements or alone.
+     * <em>Note: Using <code>locale="*"</code> here has no sense.</em>
+     * Example:
+     * <pre>
+     * &lt;greeting&gt;
+     *   &lt;i18n:when locale="en"&gt;Hello&lt;/i18n:when&gt;
+     *   &lt;i18n:when locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
+     * &lt;/greeting&gt;
+     * </pre>
+     *
+     * @see #ATTR_LOCALE
+     * @see #ELEM_CHOOSE
+     * @since 2.1
+     */
+    public static final String ELEM_WHEN = "when";
+
+    /**
+     * <code>i18n:if</code> is used to test a locale. Example:
+     * <pre>
+     * &lt;greeting&gt;
+     *   &lt;i18n:if locale="en"&gt;Hello&lt;/i18n:when&gt;
+     *   &lt;i18n:if locale="fr"&gt;Bonjour&lt;/i18n:when&gt;
+     * &lt;/greeting&gt;
+     * </pre>
+     *
+     * @see #ATTR_LOCALE
+     * @see #ELEM_CHOOSE
+     * @see #ELEM_WHEN
+     * @since 2.1
+     */
+    public static final String ELEM_IF = "if";
+
+    /**
+     * <code>i18n:otherwise</code> is used to match any locale when
+     * no matching locale has been found inside an <code>i18n:choose</code>
+     * block.
+     *
+     * @see #ELEM_CHOOSE
+     * @see #ELEM_WHEN
+     * @since 2.1
+     */
+    public static final String ELEM_OTHERWISE = "otherwise";
+
+    /**
+     * <code>i18n:param</code> is used with i18n:translate to provide
+     * substitution params. The param can have <code>i18n:text</code> as
+     * its value to provide multilungual value. Parameters can have
+     * additional attributes to be used for formatting:
+     * <ul>
+     *   <li><code>type</code>: can be <code>date, date-time, time,
+     *   number, currency, currency-no-unit or percent</code>.
+     *   Used to format params before substitution.</li>
+     *   <li><code>value</code>: the value of the param. If no value is
+     *   specified then the text inside of the param element will be used.</li>
+     *   <li><code>locale</code>: used only with <code>number, date, time,
+     *   date-time</code> types and used to override the current locale to
+     *   format the given value.</li>
+     *   <li><code>src-locale</code>: used with <code>number, date, time,
+     *   date-time</code> types and specify the locale that should be used to
+     *   parse the given value.</li>
+     *   <li><code>pattern</code>: used with <code>number, date, time,
+     *   date-time</code> types and specify the pattern that should be used
+     *   to format the given value.</li>
+     *   <li><code>src-pattern</code>: used with <code>number, date, time,
+     *   date-time</code> types and specify the pattern that should be used
+     *   to parse the given value.</li>
+     * </ul>
+     *
+     * @see #ELEM_TRANSLATE
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ELEM_PARAM = "param";
+
+    /**
+     * This attribute affects a name to the param that could be used
+     * for substitution.
+     *
+     * @since 2.1
+     */
+    public static final String ATTR_PARAM_NAME = "name";
+
+    /**
+     * <code>i18n:date</code> is used to provide a localized date string.
+     * Allowed attributes are: <code>pattern, src-pattern, locale,
+     * src-locale</code>. Usage examples:
+     * <pre>
+     *  &lt;i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
+     *    12/24/01
+     *  &lt;/i18n:date&gt;
+     *
+     *  &lt;i18n:date pattern="dd/MM/yyyy" /&gt;
+     * </pre>
+     *
+     * If no value is specified then the current date will be used. E.g.:
+     * <pre>
+     *   &lt;i18n:date /&gt;
+     * </pre>
+     * Displays the current date formatted with default pattern for
+     * the current locale.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ELEM_DATE = "date";
+
+    /**
+     * <code>i18n:date-time</code> is used to provide a localized date and
+     * time string. Allowed attributes are: <code>pattern, src-pattern,
+     * locale, src-locale</code>. Usage examples:
+     * <pre>
+     *  &lt;i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
+     *    12/24/01 1:00 AM
+     *  &lt;/i18n:date&gt;
+     *
+     *  &lt;i18n:date-time pattern="dd/MM/yyyy hh:mm" /&gt;
+     * </pre>
+     *
+     * If no value is specified then the current date and time will be used.
+     * E.g.:
+     * <pre>
+     *  &lt;i18n:date-time /&gt;
+     * </pre>
+     * Displays the current date formatted with default pattern for
+     * the current locale.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ELEM_DATE_TIME = "date-time";
+
+    /**
+     * <code>i18n:time</code> is used to provide a localized time string.
+     * Allowed attributes are: <code>pattern, src-pattern, locale,
+     * src-locale</code>. Usage examples:
+     * <pre>
+     *  &lt;i18n:time src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
+     *    1:00 AM
+     *  &lt;/i18n:time&gt;
+     *
+     * &lt;i18n:time pattern="hh:mm:ss" /&gt;
+     * </pre>
+     *
+     * If no value is specified then the current time will be used. E.g.:
+     * <pre>
+     *  &lt;i18n:time /&gt;
+     * </pre>
+     * Displays the current time formatted with default pattern for
+     * the current locale.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_NUMBER
+     */
+    public static final String ELEM_TIME = "time";
+
+    /**
+     * <code>i18n:number</code> is used to provide a localized number string.
+     * Allowed attributes are: <code>pattern, src-pattern, locale, src-locale,
+     * type</code>. Usage examples:
+     * <pre>
+     *  &lt;i18n:number src-pattern="short" src-locale="en_US" locale="de_DE"&gt;
+     *    1000.0
+     *  &lt;/i18n:number&gt;
+     *
+     * &lt;i18n:number type="currency" /&gt;
+     * </pre>
+     *
+     * If no value is specifies then 0 will be used.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_TIME
+     * @see #ELEM_DATE
+     */
+    public static final String ELEM_NUMBER = "number";
+
+    /**
+     * Currency element name
+     */
+    public static final String ELEM_CURRENCY = "currency";
+
+    /**
+     * Percent element name
+     */
+    public static final String ELEM_PERCENT = "percent";
+
+    /**
+     * Integer currency element name
+     */
+    public static final String ELEM_INT_CURRENCY = "int-currency";
+
+    /**
+     * Currency without unit element name
+     */
+    public static final String ELEM_CURRENCY_NO_UNIT = "currency-no-unit";
+
+    /**
+     * Integer currency without unit element name
+     */
+    public static final String ELEM_INT_CURRENCY_NO_UNIT =
+            "int-currency-no-unit";
+
+    //
+    // i18n general attributes
+    //
+    /**
+     * This attribute is used with i18n:text element to indicate the key of
+     * the according message. The character data of the element will be used
+     * if no message is found by this key. E.g.:
+     * <pre>
+     * &lt;i18n:text i18n:key="a_key"&gt;article_text1&lt;/i18n:text&gt;
+     * </pre>
+     */
+    public static final String KEY_ATTR = "key";
+
+    /**
+     * This attribute is used with <strong>any</strong> element (even not i18n)
+     * to translate attribute values. Should contain whitespace separated
+     * attribute names that should be translated:
+     * <pre>
+     * &lt;para title="first" name="article" i18n:attr="title name"/&gt;
+     * </pre>
+     */
+    public static final String ATTR_ATTR = "attr";
+
+    /**
+     * This attribute is used with <strong>any</strong> element (even not i18n)
+     * to evaluate attribute values. Should contain whitespace separated
+     * attribute names that should be evaluated:
+     * <pre>
+     * &lt;para title="first" name="{one} {two}" i18n:attr="name"/&gt;
+     * </pre>
+     */
+    public static final String ATTR_EXPR = "expr";
+
+    //
+    // i18n number and date formatting attributes
+    //
+    /**
+     * This attribute is used with date and number formatting elements to
+     * indicate the pattern that should be used to parse the element value.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ATTR_SRC_PATTERN = "src-pattern";
+
+    /**
+     * This attribute is used with date and number formatting elements to
+     * indicate the pattern that should be used to format the element value.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ATTR_PATTERN = "pattern";
+
+    /**
+     * This attribute is used with date and number formatting elements to
+     * indicate the locale that should be used to format the element value.
+     * Also used for in-place translations.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     * @see #ELEM_WHEN
+     */
+    public static final String ATTR_LOCALE = "locale";
+
+    /**
+     * This attribute is used with date and number formatting elements to
+     * indicate the locale that should be used to parse the element value.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ATTR_SRC_LOCALE = "src-locale";
+
+    /**
+     * This attribute is used with date and number formatting elements to
+     * indicate the value that should be parsed and formatted. If value
+     * attribute is not used then the character data of the element will be used.
+     *
+     * @see #ELEM_PARAM
+     * @see #ELEM_DATE_TIME
+     * @see #ELEM_DATE
+     * @see #ELEM_TIME
+     * @see #ELEM_NUMBER
+     */
+    public static final String ATTR_VALUE = "value";
+
+    /**
+     * This attribute is used with number formatting elements to
+     * indicate the type of formatting (currency | int-currency | percent | ...)
+     *
+     * @see #ELEM_NUMBER
+     */
+    public static final String ATTR_TYPE = "type";
+
+    /**
+     * This attribute is used to specify a different locale for the
+     * currency. When specified, this locale will be combined with
+     * the "normal" locale: e.g. the seperator symbols are taken from
+     * the normal locale but the currency symbol and possition will
+     * be taken from the currency locale.
+     * This enables to see a currency formatted for Euro but with US
+     * grouping and decimal char.
+     */
+    public static final String ATTR_CURRENCY = "currency";
+
+    /**
+     * <code>fraction-digits</code> attribute is used with
+     * <code>i18:number</code> to
+     * indicate the number of digits behind the fraction
+     */
+    public static final String ATTR_FRACTION_DIGITS = "fraction-digits";
+
+    //
+    // Configuration parameters
+    //
+    /**
+     * This configuration parameter specifies the default locale to be used.
+     */
+    public static final String PARAM_LOCALE = "locale";
+
+    /**
+     * This configuration parameter specifies the resource bundle's name to be
+     * used.
+     */
+    public static final String PARAM_BUNDLE = "bundle";
+
+    /**
+     * This configuration parameter specifies the message that should be
+     * displayed in case of a not translated text (message not found).
+     */
+    public static final String PARAM_UNTRANSLATED = "untranslated-text";
+
+    /**
+     * States of the transformer.
+     */
+    private enum TransformerState {
+
+        OUTSIDE,
+        INSIDE_TEXT,
+        INSIDE_PARAM,
+        INSIDE_TRANSLATE,
+        INSIDE_CHOOSE,
+        INSIDE_WHEN,
+        INSIDE_OTHERWISE,
+        INSIDE_DATE,
+        INSIDE_DATE_TIME,
+        INSIDE_TIME,
+        INSIDE_NUMBER
+
+    }
+
+    // All date-time related parameter types and element names
+    private static final Set<String> DATE_TYPES;
+
+    // All number related parameter types and element names
+    private static final Set<String> NUMBER_TYPES;
+
+    // Date pattern types map: short, medium, long, full
+    private static final Map<String, Integer> DATE_PATTERNS;
+
+    static {
+        // initialize date types set
+        Set<String> set = new HashSet<String>(3);
+        set.add(ELEM_DATE);
+        set.add(ELEM_TIME);
+        set.add(ELEM_DATE_TIME);
+        DATE_TYPES = Collections.unmodifiableSet(set);
+
+        // initialize number types set
+        set = new HashSet<String>(6);
+        set.add(ELEM_NUMBER);
+        set.add(ELEM_PERCENT);
+        set.add(ELEM_CURRENCY);
+        set.add(ELEM_INT_CURRENCY);
+        set.add(ELEM_CURRENCY_NO_UNIT);
+        set.add(ELEM_INT_CURRENCY_NO_UNIT);
+        NUMBER_TYPES = Collections.unmodifiableSet(set);
+
+        // initialize date patterns map
+        final Map<String, Integer> map = new HashMap(4);
+        map.put("SHORT", DateFormat.SHORT);
+        map.put("MEDIUM", DateFormat.MEDIUM);
+        map.put("LONG", DateFormat.LONG);
+        map.put("FULL", DateFormat.FULL);
+        DATE_PATTERNS = Collections.unmodifiableMap(map);
+    }
+
+    //
+    // Global configuration variables
+    //
+    /**
+     * Locale.
+     */
+    private Locale locale;
+
+    /**
+     * Bundle.
+     */
+    private ResourceBundle bundle;
+
+    /**
+     * Bundle name.
+     */
+    private String bundleName;
+
+    /**
+     * Current (local) untranslated message value
+     */
+    private String untranslated;
+
+    /**
+     * {@link SaxBuffer} containing the contents of {@link #untranslated}.
+     */
+    private ParamSAXBuffer untranslatedRecorder;
+
+    //
+    // Current state of the transformer
+    //
+    /**
+     * Current state of the transformer. Default value is TransformerState.OUTSIDE.
+     */
+    private TransformerState currentState;
+
+    /**
+     * Previous state of the transformer.
+     * Used in text translation inside params and translate elements.
+     */
+    private TransformerState prevState;
+
+    /**
+     * The i18n:key attribute is stored for the current element.
+     * If no translation found for the key then the character data of element is
+     * used as default value.
+     */
+    private String currentKey;
+
+    /**
+     * Character data buffer. used to concat chunked character data
+     */
+    private StringBuilder strBuffer;
+
+    /**
+     * A flag for copying the node when doing in-place translation
+     */
+    private boolean translateCopy;
+
+    // A flag for copying the _GOOD_ node and not others
+    // when doing in-place translation within i18n:choose
+    private boolean translateEnd;
+
+    // Translated text. Inside i18n:translate, collects character events.
+    private ParamSAXBuffer translatedTextRecorder;
+
+    // Current "i18n:text" events
+    private ParamSAXBuffer textRecorder;
+
+    // Current parameter events
+    private SAXBuffer paramRecorder;
+
+    // Param count when not using i18n:param name="..."
+    private int paramCount;
+
+    // Param name attribute for substitution.
+    private String paramName;
+
+    // i18n:param's hashmap for substitution
+    private Map<String, SAXBuffer> indexedParams;
+
+    // Current parameter value (translated or not)
+    private String paramValue;
+
+    // Date and number elements and params formatting attributes with values.
+    private Map<String, String> formattingParams;
+
+    /**
+     * Empty constructor, for usage with sitemap.
+     */
+    public I18nTransformer() {
+        super();
+    }
+
+    public I18nTransformer(final String bundle) {
+        this(null, bundle, null);
+    }
+
+    public I18nTransformer(final Locale locale, final String bundle) {
+        this(locale, bundle, null);
+    }
+
+    public I18nTransformer(final Locale locale, final String bundle,
+            final String untranslated) {
+
+        super();
+
+        this.init(locale, bundle, untranslated);
+    }
+
+    private void init(final Locale locale, final String bundle,
+            final String untranslated) {
+
+        LOG.debug("Initializing with locale={}, bundle={}, untraslated={}",
+                new Object[]{locale, bundle, untranslated});
+
+        if (bundle == null || bundle.isEmpty()) {
+            throw new SetupException("Empty or null resource bundle");
+        }
+
+        this.locale = locale == null ? Locale.getDefault() : locale;
+
+        try {
+            this.bundle = ResourceBundle.getBundle(bundle, this.locale);
+        } catch (MissingResourceException e) {
+            throw new SetupException(e);
+        }
+        this.bundleName = bundle;
+
+        this.untranslated = untranslated;
+        if (this.untranslated != null) {
+            untranslatedRecorder = new ParamSAXBuffer();
+            try {
+                untranslatedRecorder.characters(untranslated.toCharArray(), 0,
+                        untranslated.length());
+            } catch (SAXException e) {
+                throw new SetupException(
+                        "While reading " + PARAM_UNTRANSLATED, e);
+            }
+        }
+
+        // initialize instance state variables
+        this.locale = locale;
+        this.currentState = TransformerState.OUTSIDE;
+        this.prevState = TransformerState.OUTSIDE;
+        this.currentKey = null;
+        this.translateCopy = false;
+        this.translatedTextRecorder = null;
+        this.textRecorder = new ParamSAXBuffer();
+        this.paramCount = 0;
+        this.paramName = null;
+        this.paramValue = null;
+        this.paramRecorder = null;
+        this.indexedParams = new HashMap<String, SAXBuffer>(3);
+        this.formattingParams = null;
+        this.strBuffer = new StringBuilder();
+    }
+
+    @Override
+    public void setConfiguration(
+            final Map<String, ? extends Object> configuration) {
+
+        this.setup((Map<String, Object>) configuration);
+    }
+
+    @Override
+    public void setup(final Map<String, Object> parameters) {
+        if (parameters == null || !parameters.containsKey(PARAM_BUNDLE)) {
+            return;
+        }
+
+        this.init(parseLocale((String) parameters.get(PARAM_LOCALE)),
+                (String) parameters.get(PARAM_BUNDLE),
+                (String) parameters.get(PARAM_UNTRANSLATED));
+    }
+
+    public CacheKey constructCacheKey() {
+        final ParameterCacheKey result = new ParameterCacheKey();
+
+        // 1. locale
+        if (this.locale == null) {
+            throw new SetupException(
+                    "Parameter " + PARAM_LOCALE + " cannot be null");
+        }
+        result.addParameter("locale.language", this.locale.getLanguage());
+        result.addParameter("locale.country", this.locale.getCountry());
+        result.addParameter("locale.variant", this.locale.getVariant());
+
+        // 2. bundle
+        if (this.bundleName == null || this.bundleName.isEmpty()) {
+            throw new SetupException(
+                    "Parameter " + PARAM_BUNDLE + " cannot be null or empty");
+        }
+        result.addParameter("bundle", this.bundleName);
+
+        // 3. unstraslated
+        if (this.untranslated != null) {
+            result.addParameter("unstraslated", this.untranslated);
+        }
+
+        return result;
+    }
+
+    //
+    // Standard SAX event handlers
+    //
+    @Override
+    public void startElement(final String uri, final String name,
+            final String raw, final Attributes attr)
+            throws SAXException {
+
+        // Handle previously buffered characters
+        if (currentState != TransformerState.OUTSIDE
+                && strBuffer.toString().trim().length() > 0) {
+
+            i18nCharacters(strBuffer.toString().trim());
+            strBuffer.setLength(0);
+        }
+
+        // Process start element event
+        if (NS_I18N.equals(uri)) {
+            LOG.debug("Starting i18n element: {}", name);
+            startI18NElement(name, attr);
+        } else {
+            // We have a non i18n element event
+            switch (currentState) {
+                case OUTSIDE:
+                    super.startElement(uri, name, raw,
+                            translateAttributes(name, attr));
+                    break;
+
+                case INSIDE_PARAM:
+                    paramRecorder.startElement(uri, name, raw, attr);
+                    break;
+
+                case INSIDE_TEXT:
+                    textRecorder.startElement(uri, name, raw, attr);
+                    break;
+
+                case INSIDE_WHEN:
+                case INSIDE_OTHERWISE:
+                    super.startElement(uri, name, raw, attr);
+
+                default:
+            }
+        }
+    }
+
+    @Override
+    public void endElement(final String uri, final String name, final String raw)
+            throws SAXException {
+
+        // Handle previously buffered characters
+        if (currentState != TransformerState.OUTSIDE
+                && strBuffer.toString().trim().length() > 0) {
+
+            i18nCharacters(strBuffer.toString().trim());
+            strBuffer.setLength(0);
+        }
+
+        if (NS_I18N.equals(uri)) {
+            endI18NElement(name);
+        } else {
+            if (currentState == TransformerState.INSIDE_PARAM) {
+                paramRecorder.endElement(uri, name, raw);
+            } else if (currentState == TransformerState.INSIDE_TEXT) {
+                textRecorder.endElement(uri, name, raw);
+            } else if (currentState == TransformerState.INSIDE_CHOOSE
+                    || (currentState == TransformerState.INSIDE_WHEN
+                    || currentState == TransformerState.INSIDE_OTHERWISE)
+                    && !translateCopy) {
+                // Output nothing
+            } else {
+                super.endElement(uri, name, raw);
+            }
+        }
+    }
+
+    @Override
+    public void characters(final char[] chars, final int start, final int len)
+            throws SAXException {
+
+        if (currentState == TransformerState.OUTSIDE
+                || ((currentState == TransformerState.INSIDE_WHEN
+                || currentState == TransformerState.INSIDE_OTHERWISE)
+                && translateCopy)) {
+
+            super.characters(chars, start, len);
+        } else {
+            // Perform buffering to prevent chunked character data
+            strBuffer.append(chars, start, len);
+        }
+    }
+
+    //
+    // i18n specific event handlers
+    //
+    private void startI18NElement(final String name, final Attributes attr)
+            throws SAXException {
+
+        LOG.debug("Start i18n element: {}", name);
+
+        if (ELEM_TEXT.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE
+                    && currentState != TransformerState.INSIDE_PARAM
+                    && currentState != TransformerState.INSIDE_TRANSLATE) {
+
+                throw new SAXException(
+                        getClass().getName()
+                        + ": nested i18n:text elements are not allowed."
+                        + " Current state: " + currentState);
+            }
+
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_TEXT;
+
+            currentKey = attr.getValue("", KEY_ATTR);
+            if (currentKey == null) {
+                // Try the namespaced attribute
+                currentKey = attr.getValue(NS_I18N, KEY_ATTR);
+            }
+
+            if (prevState != TransformerState.INSIDE_PARAM) {
+                translatedTextRecorder = null;
+            }
+
+            if (currentKey != null) {
+                final SAXBuffer message =
+                        getMessage(currentKey, SAXBuffer.class);
+                translatedTextRecorder = message == null
+                        ? null : new ParamSAXBuffer(message);
+            }
+        } else if (ELEM_TRANSLATE.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:translate element must be used "
+                        + "outside of other i18n elements. Current state: "
+                        + currentState);
+            }
+
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_TRANSLATE;
+        } else if (ELEM_PARAM.equals(name)) {
+            if (currentState != TransformerState.INSIDE_TRANSLATE) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:param element can be used only inside "
+                        + "i18n:translate element. Current state: "
+                        + currentState);
+            }
+
+            paramName = attr.getValue(ATTR_PARAM_NAME);
+            if (paramName == null) {
+                paramName = String.valueOf(paramCount++);
+            }
+
+            paramRecorder = new SAXBuffer();
+            setFormattingParams(attr);
+            currentState = TransformerState.INSIDE_PARAM;
+        } else if (ELEM_CHOOSE.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE) {
+                throw new SAXException(getClass().getName()
+                        + ": i18n:choose elements cannot be used inside of other"
+                        + " i18n elements.");
+            }
+
+            translateCopy = false;
+            translateEnd = false;
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_CHOOSE;
+        } else if (ELEM_WHEN.equals(name)
+                || ELEM_IF.equals(name)) {
+
+            if (ELEM_WHEN.equals(name) && currentState
+                    != TransformerState.INSIDE_CHOOSE) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:when elements are can be used only"
+                        + "inside of i18n:choose elements.");
+            }
+
+            if (ELEM_IF.equals(name) && currentState != TransformerState.OUTSIDE) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:if elements cannot be nested.");
+            }
+
+            final String localeAttrValue = attr.getValue(ATTR_LOCALE);
+            if (localeAttrValue == null) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:" + name
+                        + " element cannot be used without 'locale' attribute.");
+            }
+
+            if ((!translateEnd && currentState == TransformerState.INSIDE_CHOOSE)
+                    || currentState == TransformerState.OUTSIDE) {
+
+                // Perform soft locale matching
+                if (this.locale.toString().startsWith(localeAttrValue)) {
+                    LOG.debug("Locale matching: {}", localeAttrValue);
+                    translateCopy = true;
+                }
+            }
+
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_WHEN;
+
+        } else if (ELEM_OTHERWISE.equals(name)) {
+            if (currentState != TransformerState.INSIDE_CHOOSE) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:otherwise elements are not allowed "
+                        + "only inside i18n:choose.");
+            }
+
+            LOG.debug("Matching any locale");
+            if (!translateEnd) {
+                translateCopy = true;
+            }
+
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_OTHERWISE;
+
+        } else if (ELEM_DATE.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE
+                    && currentState != TransformerState.INSIDE_TEXT
+                    && currentState != TransformerState.INSIDE_PARAM) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:date elements are not allowed "
+                        + "inside of other i18n elements.");
+            }
+
+            setFormattingParams(attr);
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_DATE;
+        } else if (ELEM_DATE_TIME.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE
+                    && currentState != TransformerState.INSIDE_TEXT
+                    && currentState != TransformerState.INSIDE_PARAM) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:date-time elements are not allowed "
+                        + "inside of other i18n elements.");
+            }
+
+            setFormattingParams(attr);
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_DATE_TIME;
+        } else if (ELEM_TIME.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE
+                    && currentState != TransformerState.INSIDE_TEXT
+                    && currentState != TransformerState.INSIDE_PARAM) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:date elements are not allowed "
+                        + "inside of other i18n elements.");
+            }
+
+            setFormattingParams(attr);
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_TIME;
+        } else if (ELEM_NUMBER.equals(name)) {
+            if (currentState != TransformerState.OUTSIDE
+                    && currentState != TransformerState.INSIDE_TEXT
+                    && currentState != TransformerState.INSIDE_PARAM) {
+                throw new SAXException(
+                        getClass().getName()
+                        + ": i18n:number elements are not allowed "
+                        + "inside of other i18n elements.");
+            }
+
+            setFormattingParams(attr);
+            prevState = currentState;
+            currentState = TransformerState.INSIDE_NUMBER;
+        }
+    }
+
+    // Get all possible i18n formatting attribute values and store in a Map
+    private void setFormattingParams(final Attributes attr) {
+        // average number of attributes is 3
+        formattingParams = new HashMap<String, String>(3);
+
+        String attr_value = attr.getValue(ATTR_SRC_PATTERN);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_SRC_PATTERN, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_PATTERN);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_PATTERN, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_VALUE);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_VALUE, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_LOCALE);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_LOCALE, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_CURRENCY);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_CURRENCY, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_SRC_LOCALE);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_SRC_LOCALE, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_TYPE);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_TYPE, attr_value);
+        }
+
+        attr_value = attr.getValue(ATTR_FRACTION_DIGITS);
+        if (attr_value != null) {
+            formattingParams.put(ATTR_FRACTION_DIGITS, attr_value);
+        }
+    }
+
+    private void endI18NElement(final String name)
+            throws SAXException {
+
+        LOG.debug("End i18n element: {}", name);
+
+        switch (currentState) {
+            case INSIDE_TEXT:
+                endTextElement();
+                break;
+
+            case INSIDE_TRANSLATE:
+                endTranslateElement();
+                break;
+
+            case INSIDE_CHOOSE:
+                endChooseElement();
+                break;
+
+            case INSIDE_WHEN:
+            case INSIDE_OTHERWISE:
+                endWhenElement();
+                break;
+
+            case INSIDE_PARAM:
+                endParamElement();
+                break;
+
+            case INSIDE_DATE:
+            case INSIDE_DATE_TIME:
+            case INSIDE_TIME:
+                endDate_TimeElement();
+                break;
+
+            case INSIDE_NUMBER:
+                endNumberElement();
+                break;
+
+            default:
+        }
+    }
+
+    private void i18nCharacters(final String textValue)
+            throws SAXException {
+
+        LOG.debug("i18n message text = '{}'", textValue);
+
+        SAXBuffer buffer;
+        switch (currentState) {
+            case INSIDE_TEXT:
+                buffer = textRecorder;
+                break;
+
+            case INSIDE_PARAM:
+                buffer = paramRecorder;
+                break;
+
+            case INSIDE_WHEN:
+            case INSIDE_OTHERWISE:
+                // Previously handeld to avoid the String() conversion.
+                return;
+
+            case INSIDE_TRANSLATE:
+                if (translatedTextRecorder == null) {
+                    translatedTextRecorder = new ParamSAXBuffer();
+                }
+                buffer = translatedTextRecorder;
+                break;
+
+            case INSIDE_CHOOSE:
+                // No characters allowed. Send an exception ?
+                if (LOG.isDebugEnabled() && !textValue.isEmpty()) {
+                    LOG.debug("No characters allowed inside <i18n:choose> "
+                            + "tag. Received: {}", textValue);
+                }
+                return;
+
+            case INSIDE_DATE:
+            case INSIDE_DATE_TIME:
+            case INSIDE_TIME:
+            case INSIDE_NUMBER:
+                // Trim text values to avoid parsing errors.
+                if (!textValue.isEmpty()
+                        && formattingParams.get(ATTR_VALUE) == null) {
+
+                    formattingParams.put(ATTR_VALUE, textValue);
+                }
+                return;
+
+            default:
+                throw new IllegalStateException(getClass().getName()
+                        + " developer's fault: characters not handled. "
+                        + "Current state: " + currentState);
+        }
+
+        buffer.characters(textValue.toCharArray(),
+                0, textValue.length());
+    }
+
+    /**
+     * Translate all attributes that are listed in i18n:attr attribute
+     *
+     * @param element
+     * @param attr
+     * @return
+     * @throws SAXException 
+     */
+    private Attributes translateAttributes(final String element, Attributes attr)
+            throws SAXException {
+
+        if (attr == null) {
+            return null;
+        }
+
+        AttributesImpl tempAttr = null;
+
+        // Translate all attributes from i18n:attr="name1 name2 ..."
+        // using their values as keys.
+        int attrIndex = attr.getIndex(NS_I18N, ATTR_ATTR);
+
+        if (attrIndex != -1) {
+            final StringTokenizer tokenizer =
+                    new StringTokenizer(attr.getValue(attrIndex));
+
+            // Make a copy which we are going to modify
+            tempAttr = new AttributesImpl(attr);
+            // Remove the i18n:attr attribute - we don't need it anymore
+            tempAttr.removeAttribute(attrIndex);
+
+            int index;
+            String value;
+            // Iterate through listed attributes and translate them
+            while (tokenizer.hasMoreElements()) {
+                final String name = tokenizer.nextToken();
+
+                index = tempAttr.getIndex(name);
+                if (index == -1) {
+                    LOG.warn("Attribute " + name + " not found in element <"
+                            + element + ">");
+                    continue;
+                }
+
+                value = translateAttribute(element, name,
+                        tempAttr.getValue(index));
+                if (value != null) {
+                    // Set the translated value. If null, do nothing.
+                    tempAttr.setValue(index, value);
+                }
+            }
+
+            attr = tempAttr;
+        }
+
+        // Translate all attributes from i18n:expr="name1 name2 ..."
+        // using their values as keys.
+        attrIndex = attr.getIndex(NS_I18N, ATTR_EXPR);
+        if (attrIndex != -1) {
+            final StringTokenizer tokenizer =
+                    new StringTokenizer(attr.getValue(attrIndex));
+
+            if (tempAttr == null) {
+                tempAttr = new AttributesImpl(attr);
+            }
+            tempAttr.removeAttribute(attrIndex);
+
+            int index;
+            TokenReceiver tokenReceiver;
+            // Iterate through listed attributes and evaluate them
+            while (tokenizer.hasMoreElements()) {
+                final String name = tokenizer.nextToken();
+
+                index = tempAttr.getIndex(name);
+                if (index == -1) {
+                    LOG.warn("Attribute " + name + " not found in element <"
+                            + element + ">");
+                    continue;
+                }
+
+                final StringBuilder translated = new StringBuilder();
+
+                // Evaluate {..} expression
+                tokenReceiver = new TokenReceiver() {
+
+                    @Override
+                    public void addToken(final TokenReceiver.Type type,
+                            final String value) {
+
+                        switch (type) {
+                            case MODULE:
+                                break;
+
+                            case VARIABLE:
+                                try {
+                                    translated.append(translateAttribute(
+                                            element, name, value));
+                                } catch (SAXException e) {
+                                    LOG.error("While translating " + element
+                                            + ":" + name + ":" + value, e);
+                                }
+                                break;
+
+                            case TEXT:
+                                if (value != null) {
+                                    translated.append(value);
+                                }
+                                break;
+                        }
+                    }
+                };
+
+                try {
+                    VariableExpressionTokenizer.tokenize(
+                            tempAttr.getValue(index), tokenReceiver);
+                } catch (ParseException e) {
+                    throw new SAXException(e);
+                }
+
+                // Set the translated value.
+                tempAttr.setValue(index, translated.toString());
+            }
+
+            attr = tempAttr;
+        }
+
+        // nothing to translate, just return
+        return attr;
+    }
+
+    /**
+     * Translate attribute value.
+     * @return Translated text, untranslated text, or null.
+     */
+    private String translateAttribute(final String element, final String name,
+            final String key)
+            throws SAXException {
+
+        final SAXBuffer text = getMessage(key, SAXBuffer.class);
+        if (text == null) {
+            LOG.warn("Translation not found for attribute " + name
+                    + " in element <" + element + ">");
+        }
+        return text == null ? untranslated : text.toString();
+    }
+
+    private void endTextElement()
+            throws SAXException {
+
+        switch (prevState) {
+            case OUTSIDE:
+                if (translatedTextRecorder == null) {
+                    if (currentKey == null) {
+                        // Use the text as key. Not recommended for large strings,
+                        // especially if they include markup.
+                        translatedTextRecorder =
+                                getMessage(textRecorder.toString(),
+                                textRecorder);
+                    } else {
+                        // We have the key, but couldn't find a translation
+                        LOG.debug("Translation not found for key '{}'",
+                                currentKey);
+
+                        // Use the untranslated-text only when the content of 
+                        // the i18n:text element was empty
+                        translatedTextRecorder = textRecorder.isEmpty()
+                                && untranslatedRecorder != null
+                                ? untranslatedRecorder : textRecorder;
+                    }
+                }
+
+                if (translatedTextRecorder != null) {
+                    translatedTextRecorder.toSAX(this.getSAXConsumer());
+                }
+
+                textRecorder.recycle();
+                translatedTextRecorder = null;
+                currentKey = null;
+                break;
+
+            case INSIDE_TRANSLATE:
+                if (translatedTextRecorder == null && !textRecorder.isEmpty()) {
+                    translatedTextRecorder = getMessage(
+                            textRecorder.toString(), textRecorder);
+
+                    if (translatedTextRecorder == textRecorder) {
+                        // if the default value was returned, make a copy
+                        translatedTextRecorder = new ParamSAXBuffer(
+                                textRecorder);
+                    }
+                }
+
+                textRecorder.recycle();
+                break;
+
+            case INSIDE_PARAM:
+                // We send the translated text to the param recorder, after 
+                // trying to translate it.
+                // Remember you can't give a key when inside a param, that'll be
+                // nonsense! No need to clone. We just send the events.
+                if (!textRecorder.isEmpty()) {
+                    getMessage(textRecorder.toString(), textRecorder).
+                            toSAX(paramRecorder);
+                    textRecorder.recycle();
+                }
+                break;
+
+            default:
+        }
+
+        currentState = prevState;
+        prevState = TransformerState.OUTSIDE;
+    }
+
+    /**
+     * Process substitution parameter.
+     */
+    private void endParamElement()
+            throws SAXException {
+
+        paramValue = null;
+        currentState = TransformerState.INSIDE_TRANSLATE;
+
+        if (paramRecorder == null) {
+            return;
+        }
+
+        indexedParams.put(paramName, paramRecorder);
+        paramRecorder = null;
+    }
+
+    private void endTranslateElement()
+            throws SAXException {
+
+        if (translatedTextRecorder != null) {
+            LOG.debug("End of translate with params. "
+                    + "Fragment for substitution: {}", translatedTextRecorder);
+
+            translatedTextRecorder.toSAX(this.getSAXConsumer(), indexedParams);
+            translatedTextRecorder = null;
+            textRecorder.recycle();
+        }
+
+        indexedParams.clear();
+        paramCount = 0;
+        currentState = TransformerState.OUTSIDE;
+    }
+
+    private void endChooseElement() {
+        currentState = TransformerState.OUTSIDE;
+    }
+
+    private void endWhenElement() {
+        currentState = prevState;
+        if (translateCopy) {
+            translateCopy = false;
+            translateEnd = true;
+        }
+    }
+
+    private void endDate_TimeElement()
+            throws SAXException {
+
+        final String result = formatDateTime(formattingParams);
+        switch (prevState) {
+            case OUTSIDE:
+                this.getSAXConsumer().characters(result.toCharArray(), 0,
+                        result.length());
+                break;
+            case INSIDE_PARAM:
+                paramRecorder.characters(result.toCharArray(), 0,
+                        result.length());
+                break;
+            case INSIDE_TEXT:
+                textRecorder.characters(result.toCharArray(), 0,
+                        result.length());
+                break;
+
+            default:
+        }
+        currentState = prevState;
+    }
+
+    private String formatDateTime(final Map<String, String> params)
+            throws SAXException {
+
+        // Check that we have not null params
+        if (params == null) {
+            throw new IllegalArgumentException("Nothing to format");
+        }
+
+        // Formatters
+        SimpleDateFormat srcFmt;
+        SimpleDateFormat destFmt;
+
+        // Date formatting styles
+        int srcStyle = DateFormat.DEFAULT;
+        int destStyle = DateFormat.DEFAULT;
+
+        // Date formatting patterns
+        boolean realSrcPattern = false;
+        boolean realDestPattern = false;
+
+        // From locale
+        final Locale srcLocale = parseLocale(params.get(ATTR_SRC_LOCALE));
+        // To locale
+        final Locale destLocale = parseLocale(params.get(ATTR_LOCALE));
+
+        // From pattern
+        final String srcPattern = params.get(ATTR_SRC_PATTERN);
+        // To pattern
+        final String destPattern = params.get(ATTR_PATTERN);
+        // The date value
+        final String value = params.get(ATTR_VALUE);
+
+        // A src-pattern attribute is present
+        if (srcPattern != null) {
+            // Check if we have a real pattern
+            final Integer patternValue =
+                    DATE_PATTERNS.get(srcPattern.toUpperCase(srcLocale));
+            if (patternValue == null) {
+                realSrcPattern = true;
+            } else {
+                srcStyle = patternValue.intValue();
+            }
+        }
+
+        // A pattern attribute is present
+        if (destPattern != null) {
+            final Integer patternValue =
+                    DATE_PATTERNS.get(destPattern.toUpperCase(destLocale));
+            if (patternValue == null) {
+                realDestPattern = true;
+            } else {
+                destStyle = patternValue.intValue();
+            }
+        }
+
+        // Initializing date formatters
+        switch (currentState) {
+            case INSIDE_DATE:
+                destFmt = (SimpleDateFormat) DateFormat.getDateInstance(
+                        destStyle, destLocale);
+                srcFmt = (SimpleDateFormat) DateFormat.getDateInstance(
+                        srcStyle, srcLocale);
+                break;
+
+            case INSIDE_DATE_TIME:
+                destFmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(
+                        destStyle, destStyle, destLocale);
+                srcFmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(
+                        srcStyle, srcStyle, srcLocale);
+                break;
+
+            default:
+                // INSIDE_TIME
+                destFmt = (SimpleDateFormat) DateFormat.getTimeInstance(
+                        destStyle, destLocale);
+                srcFmt = (SimpleDateFormat) DateFormat.getTimeInstance(
+                        srcStyle, srcLocale);
+        }
+
+        // parsed date object
+        Date dateValue;
+
+        // pattern overwrites locale format
+        if (realSrcPattern) {
+            srcFmt.applyPattern(srcPattern);
+        }
+
+        if (realDestPattern) {
+            destFmt.applyPattern(destPattern);
+        }
+
+        // get current date and time by default
+        if (value == null) {
+            dateValue = new Date();
+        } else {
+            try {
+                dateValue = srcFmt.parse(value);
+            } catch (ParseException pe) {
+                throw new SAXException(this.getClass().getName()
+                        + "i18n:date - parsing error.", pe);
+            }
+        }
+
+        // we have all necessary data here: do formatting.
+        LOG.debug("### Formatting date: {}"
+                + " with localized pattern {}"
+                + " for locale: {}", new Object[]{dateValue,
+                    destFmt.toLocalizedPattern(), locale});
+
+        return destFmt.format(dateValue);
+    }
+
+    private void endNumberElement()
+            throws SAXException {
+
+        final String result = formatNumber(formattingParams);
+        switch (prevState) {
+            case OUTSIDE:
+                this.getSAXConsumer().characters(result.toCharArray(),
+                        0, result.length());
+                break;
+            case INSIDE_PARAM:
+                paramRecorder.characters(result.toCharArray(), 0,
+                        result.length());
+                break;
+            case INSIDE_TEXT:
+                textRecorder.characters(result.toCharArray(), 0,
+                        result.length());
+                break;
+
+            default:
+        }
+        currentState = prevState;
+    }
+
+    private String formatNumber(final Map<String, String> params)
+            throws SAXException {
+
+        if (params == null) {
+            throw new SAXException(this.getClass().getName()
+                    + ": i18n:number - error in element attributes.");
+        }
+
+        // from pattern
+        final String srcPattern = params.get(ATTR_SRC_PATTERN);
+        // to pattern
+        final String dstPattern = params.get(ATTR_PATTERN);
+        // the number value
+        final String value = params.get(ATTR_VALUE);
+        if (value == null) {
+            return "";
+        }
+
+        // fraction-digits
+        int fractionDigits = -1;
+        try {
+            if (params.get(ATTR_FRACTION_DIGITS) != null) {
+                fractionDigits = Integer.parseInt(
+                        params.get(ATTR_FRACTION_DIGITS));
+            }
+        } catch (NumberFormatException nfe) {
+            LOG.warn("Error in number format with fraction-digits", nfe);
+        }
+
+        // parsed number
+        Number numberValue;
+
+        // locale, may be switched locale
+        final Locale srcLocale = parseLocale(params.get(ATTR_SRC_LOCALE));
+        Locale dstLocale = parseLocale(params.get(ATTR_LOCALE));
+        // currency locale
+        final Locale currencyLocale = parseLocale(params.get(ATTR_CURRENCY));
+        // decimal and grouping locale
+        Locale decimalLocale = null;
+        if (currencyLocale != null) {
+            // the reasoning here is: if there is a currency locale, then start
+            // from that one but take certain properties (like decimal and 
+            // grouping seperation symbols) from the default locale (this happens
+            // further on).
+            decimalLocale = dstLocale;
+            dstLocale = currencyLocale;
+        }
+
+        // src format
+        final DecimalFormat sourceFormat =
+                (DecimalFormat) NumberFormat.getInstance(srcLocale);
+        int intCurrency = 0;
+
+        // src-pattern overwrites locale format
+        if (srcPattern != null) {
+            sourceFormat.applyPattern(srcPattern);
+        }
+
+        // to format
+        DecimalFormat dstFmt;
+        final char dec = sourceFormat.getDecimalFormatSymbols().
+                getDecimalSeparator();
+        int decAt = 0;
+        boolean appendDec = false;
+
+        // type
+        final String type = (String) params.get(ATTR_TYPE);
+        if (type == null || type.equals(ELEM_NUMBER)) {
+            dstFmt = (DecimalFormat) NumberFormat.getInstance(dstLocale);
+            dstFmt.setMaximumFractionDigits(309);
+            for (int i = value.length() - 1;
+                    i >= 0 && value.charAt(i) != dec; i--, decAt++) {
+            }
+
+            if (decAt < value.length()) {
+                dstFmt.setMinimumFractionDigits(decAt);
+            }
+            decAt = 0;
+            for (int i = 0; i < value.length() && value.charAt(i) != dec; i++) {
+                if (Character.isDigit(value.charAt(i))) {
+                    decAt++;
+                }
+            }
+
+            dstFmt.setMinimumIntegerDigits(decAt);
+            if (value.charAt(value.length() - 1) == dec) {
+                appendDec = true;
+            }
+        } else if (type.equals(ELEM_CURRENCY)) {
+            dstFmt = (DecimalFormat) NumberFormat.getCurrencyInstance(dstLocale);
+        } else if (type.equals(ELEM_INT_CURRENCY)) {
+            dstFmt = (DecimalFormat) NumberFormat.getCurrencyInstance(dstLocale);
+            intCurrency = 1;
+            for (int i = 0; i < dstFmt.getMaximumFractionDigits(); i++) {
+                intCurrency *= 10;
+            }
+        } else if (type.equals(ELEM_CURRENCY_NO_UNIT)) {
+            final DecimalFormat tmp =
+                    (DecimalFormat) NumberFormat.getCurrencyInstance(dstLocale);
+            dstFmt = (DecimalFormat) NumberFormat.getInstance(dstLocale);
+            dstFmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
+            dstFmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
+        } else if (type.equals(ELEM_INT_CURRENCY_NO_UNIT)) {
+            final DecimalFormat tmp =
+                    (DecimalFormat) NumberFormat.getCurrencyInstance(dstLocale);
+            intCurrency = 1;
+            for (int i = 0; i < tmp.getMaximumFractionDigits(); i++) {
+                intCurrency *= 10;
+            }
+            dstFmt = (DecimalFormat) NumberFormat.getInstance(dstLocale);
+            dstFmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits());
+            dstFmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits());
+        } else if (type.equals(ELEM_PERCENT)) {
+            dstFmt = (DecimalFormat) NumberFormat.getPercentInstance(dstLocale);
+        } else {
+            throw new SAXException("<i18n:number>: unknown type: " + type);
+        }
+
+        if (fractionDigits > -1) {
+            dstFmt.setMinimumFractionDigits(fractionDigits);
+            dstFmt.setMaximumFractionDigits(fractionDigits);
+        }
+
+        if (decimalLocale != null) {
+            final DecimalFormat decFormat = (DecimalFormat) NumberFormat.
+                    getCurrencyInstance(decimalLocale);
+            final DecimalFormatSymbols dfsNew = decFormat.
+                    getDecimalFormatSymbols();
+            final DecimalFormatSymbols dfsOrig =
+                    dstFmt.getDecimalFormatSymbols();
+            dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator());
+            dfsOrig.setMonetaryDecimalSeparator(dfsNew.
+                    getMonetaryDecimalSeparator());
+            dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator());
+            dstFmt.setDecimalFormatSymbols(dfsOrig);
+        }
+
+        // pattern overwrites locale format
+        if (dstPattern != null) {
+            dstFmt.applyPattern(dstPattern);
+        }
+
+        try {
+            numberValue = sourceFormat.parse(value);
+            if (intCurrency > 0) {
+                numberValue = new Double(numberValue.doubleValue()
+                        / intCurrency);
+            }
+        } catch (ParseException pe) {
+            throw new SAXException(this.getClass().getName()
+                    + "i18n:number - parsing error.", pe);
+        }
+
+        // we have all necessary data here: do formatting.
+        String result = dstFmt.format(numberValue);
+        if (appendDec) {
+            result += dec;
+        }
+        LOG.debug("i18n:number result: {}", result);
+
+        return result;
+    }
+
+    /**
+     * Parses given locale string to Locale object. If the string is null
+     * or empty then the given locale is returned.
+     *
+     * @param localeString - a string containing locale in
+     *        <code>language_country_variant</code> format.
+     * @param defaultLocale - returned if localeString is <code>null</code>
+     *        or <code>""</code>
+     */
+    public Locale parseLocale(final String localeString) {
+        Locale result;
+
+        if (localeString == null || localeString.isEmpty()) {
+            result = this.locale == null ? Locale.getDefault() : this.locale;
+        } else {
+            final StringTokenizer tokenizer = new StringTokenizer(localeString,
+                    LOCALE_DELIMITER);
+            final String language = tokenizer.hasMoreElements()
+                    ? tokenizer.nextToken() : this.locale == null
+                    ? Locale.getDefault().getLanguage()
+                    : this.locale.getLanguage();
+            final String country = tokenizer.hasMoreElements()
+                    ? tokenizer.nextToken() : "";
+            final String variant = tokenizer.hasMoreElements()
+                    ? tokenizer.nextToken() : "";
+
+            result = new Locale(language, country, variant);
+        }
+
+        return result;
+    }
+
+    /**
+     * Helper method to retrieve a message from the resource bundle.
+     *
+     * @param key
+     * @return SAXBuffer containing message, or null if not found.
+     * @throws SAXException 
+     */
+    private <T extends SAXBuffer> T getMessage(final String key,
+            final Class<T> reference)
+            throws SAXException {
+
+        String message = null;
+        try {
+            message = bundle.getString(key);
+        } catch (MissingResourceException e) {
+            LOG.debug("Could not find any translation for {}", key);
+        }
+
+        T buffer = null;
+        if (message != null && !message.isEmpty()) {
+            try {
+                buffer = reference.newInstance();
+                buffer.characters(message.toCharArray(), 0, message.length());
+            } catch (InstantiationException e) {
+                LOG.error("Could not instantiate {}", reference, e);
+            } catch (IllegalAccessException e) {
+                LOG.error("Could not instantiate {}", reference, e);
+            }
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Helper method to retrieve a message from the current dictionary.
+     * A default value is returned if message is not found.
+     * 
+     * @param key
+     * @param defaultValue
+     * @return ParamSAXBuffer containing message, or defaultValue if not found.
+     * @throws SAXException 
+     */
+    private ParamSAXBuffer getMessage(final String key,
+            final ParamSAXBuffer defaultValue)
+            throws SAXException {
+
+        ParamSAXBuffer result = getMessage(key, ParamSAXBuffer.class);
+        if (result == null) {
+            result = defaultValue;
+        }
+
+        return result;
+    }
+}

Added: cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/util/VariableExpressionTokenizer.java
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/util/VariableExpressionTokenizer.java?rev=1156644&view=auto
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/util/VariableExpressionTokenizer.java (added)
+++ cocoon/cocoon3/trunk/cocoon-sax/src/main/java/org/apache/cocoon/sax/util/VariableExpressionTokenizer.java Thu Aug 11 15:11:04 2011
@@ -0,0 +1,165 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cocoon.sax.util;
+
+import java.text.ParseException;
+
+/**
+ * Parses "Text {module:{module:attribute}} more text {variable}" types of
+ * expressions. Supports escaping of braces with '\' character, and nested
+ * expressions.
+ */
+public final class VariableExpressionTokenizer {
+
+    /**
+     * Callback for tokenizer
+     */
+    public interface TokenReceiver {
+
+        public enum Type {
+
+            OPEN,
+            CLOSE,
+            COLON,
+            TEXT,
+            MODULE,
+            VARIABLE
+
+        }
+
+        /**
+         * Reports parsed tokens.
+         */
+        void addToken(Type type, String value)
+                throws ParseException;
+    }
+
+    private VariableExpressionTokenizer() {
+    }
+
+    /**
+     * Tokenizes specified expression. Passes tokens to the
+     * reciever.
+     *
+     * @throws PatternSyntaxException if expression is not valid
+     */
+    public static void tokenize(String expression, final TokenReceiver receiver)
+            throws ParseException {
+
+        TokenReceiver.Type lastTokenType = null;
+
+        int openCount = 0;
+        int closeCount = 0;
+
+        int pos = 0;
+        int i;
+        boolean escape = false;
+
+        char character;
+        char nextChar;
+        int colonPos;
+        int closePos;
+        int openPos;
+        for (i = 0; i < expression.length(); i++) {
+            character = expression.charAt(i);
+
+            if (escape) {
+                escape = false;
+            } else if (character == '\\' && i < expression.length()) {
+                nextChar = expression.charAt(i + 1);
+                if (nextChar == '{' || nextChar == '}') {
+                    expression = expression.substring(0, i) + expression.
+                            substring(i + 1);
+                    escape = true;
+                    i--;
+                }
+            } else if (character == '{') {
+                if (i > pos) {
+                    lastTokenType = TokenReceiver.Type.TEXT;
+                    receiver.addToken(lastTokenType,
+                            expression.substring(pos, i));
+                }
+
+                openCount++;
+                lastTokenType = TokenReceiver.Type.OPEN;
+                receiver.addToken(lastTokenType, null);
+
+                colonPos = indexOf(expression, ':', i);
+                closePos = indexOf(expression, '}', i);
+                openPos = indexOf(expression, '{', i);
+
+                if (openPos < colonPos && openPos < closePos) {
+                    throw new ParseException(expression, i);
+                }
+
+                if (colonPos < closePos) {
+                    // we've found a module
+                    lastTokenType = TokenReceiver.Type.MODULE;
+                    receiver.addToken(lastTokenType,
+                            expression.substring(i + 1, colonPos));
+                    i = colonPos - 1;
+                } else {
+                    // Unprefixed name: variable
+                    lastTokenType = TokenReceiver.Type.VARIABLE;
+                    receiver.addToken(lastTokenType,
+                            expression.substring(i + 1, closePos));
+                    i = closePos - 1;
+                }
+
+                pos = i + 1;
+            } else if (character == '}') {
+                if (i > 0 && expression.charAt(i - 1) == '\\') {
+                    continue;
+                }
+                if (i > pos) {
+                    lastTokenType = TokenReceiver.Type.TEXT;
+                    receiver.addToken(lastTokenType,
+                            expression.substring(pos, i));
+                }
+
+                closeCount++;
+                lastTokenType = TokenReceiver.Type.CLOSE;
+                receiver.addToken(lastTokenType, null);
+
+                pos = i + 1;
+            } else if (character == ':') {
+                if (lastTokenType != TokenReceiver.Type.MODULE || i != pos) {
+                    // this colon isn't part of a module reference
+                    continue;
+                }
+
+                lastTokenType = TokenReceiver.Type.COLON;
+                receiver.addToken(lastTokenType, null);
+                pos = i + 1;
+            }
+        }
+
+        if (i > pos) {
+            lastTokenType = TokenReceiver.Type.TEXT;
+            receiver.addToken(lastTokenType,
+                    expression.substring(pos, i));
+        }
+
+        if (openCount != closeCount) {
+            throw new ParseException(expression, expression.length());
+        }
+    }
+
+    private static int indexOf(final String expression, final char chr,
+            final int pos) {
+
+        final int location = expression.indexOf(chr, pos + 1);
+        return location == -1 ? expression.length() : location;
+    }
+}



Mime
View raw message