/* * 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. */ package org.apache.cocoon.transformation; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.treeprocessor.variables.VariableExpressionTokenizer; import org.apache.cocoon.components.treeprocessor.variables.VariableResolver; import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.i18n.Bundle; import org.apache.cocoon.i18n.BundleFactory; import org.apache.cocoon.i18n.I18nUtils; import org.apache.cocoon.sitemap.PatternException; import org.apache.cocoon.xml.ParamSaxBuffer; import org.apache.cocoon.xml.SaxBuffer; import org.apache.excalibur.source.SourceValidity; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import java.io.IOException; 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.Iterator; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import java.util.StringTokenizer; /** * @cocoon.sitemap.component.documentation * Internationalization transformer is used to transform i18n markup into text * based on a particular locale. * * @cocoon.sitemap.component.name i18n * @cocoon.sitemap.component.documentation.caching TBD * @cocoon.sitemap.component.logger sitemap.transformer.i18n * * <h3>I18n Transformer</h3> * <p>The i18n transformer works by finding a translation for the user's locale * in the configured catalogues. Locale is passed as parameter to the transformer, * and it can be determined based on the request, session, or a cookie data by * the {@link org.apache.cocoon.acting.LocaleAction}.</p> * * <p>For the passed local it then attempts to find a message catalogue that * satisifies the locale, and uses it for for processing text replacement * directed by i18n markup.</p> * * <p>Message catalogues are maintained in separate files, with a naming * convention similar to that of {@link java.util.ResourceBundle}. I.e. * <code>basename_locale</code>, where <i>basename</i> can be any name, * and <i>locale</i> can be any locale specified using ISO 639/3166 * characters (eg. <code>en_AU</code>, <code>de_AT</code>, <code>es</code>).</p> * * <p><strong>NOTE:</strong> ISO 639 is not a stable standard; some of the * language codes it defines (specifically, iw, ji, and in) have changed * (see {@link java.util.Locale} for details). * * <h3>Message Catalogues</h3> * <p>Catalogues are of the following format: * <pre> * <?xml version="1.0"?> * <!-- message catalogue file for locale ... --> * <catalogue xml:lang="locale"> * <message key="key">text <i>or</i> markup</message> * .... * </catalogue> * </pre> * Where <code>key</code> specifies a particular message for that * language. * * <h3>Usage</h3> * <p>Files to be translated contain the following markup: * <pre> * <?xml version="1.0"?> * ... some text, translate <i18n:text>key</i18n:text> * </pre> * At runtime, the i18n transformer will find a message catalogue for the * user's locale, and will appropriately replace the text between the * <code><i18n:text></code> markup, using the value between the tags as * the lookup key.</p> * * <p>If the i18n transformer cannot find an appropriate message catalogue for * the user's given locale, it will recursively try to locate a <i>parent</i> * message catalogue, until a valid catalogue can be found. * ie: * <ul> * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>_<i>variant</i>.xml * <li><strong>catalogue</strong>_<i>language</i>_<i>country</i>.xml * <li><strong>catalogue</strong>_<i>language</i>.xml * <li><strong>catalogue</strong>.xml * </ul> * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i> * (no variant), the following search will occur: * <ul> * <li><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml * <li><strong>messages</strong>_<i>en</i>.xml * <li><strong>messages</strong>.xml * </ul> * This allows the developer to write a hierarchy of message catalogues, * at each defining messages with increasing depth of variation.</p> * * <p>In addition, catalogues can be split across multiple locations. For example, * there can be a default catalogue in one directory with a user or client specific * catalogue in another directory. The catalogues will be searched in the order of * the locations specified still following the locale ordering specified above. * eg: Assuming a basename of <i>messages</i> and a locale of <i>en_AU</i> * (no variant) and locations of <i>translations/client</i> and <i>translations</i>, * the following search will occur: * <ul> * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml * <li><i>translations/</i><strong>messages</strong>_<i>en</i>_<i>AU</i>.xml * <li><i>translations/client/</i><strong>messages</strong>_<i>en</i>.xml * <li><i>translations/</i><strong>messages</strong>_<i>en</i>.xml * <li><i>translations/client/</i><strong>messages</strong>.xml * <li><i>translations/</i><strong>messages</strong>.xml * </ul> * </p> * * <p>The <code>i18n:text</code> element can optionally take an attribute * <code>i18n:catalogue</code> to indicate which specific catalogue to use. * The value of this attribute should be the id of the catalogue to use * (see sitemap configuration). * * <h3>Sitemap Configuration</h3> * <pre> * <map:transformer name="i18n" * src="org.apache.cocoon.transformation.I18nTransformer"> * * <catalogues default="someId"> * <catalogue id="someId" name="messages" [location="translations"]> * [<location>translations/client</location>] * [<location>translations</location>] * </catalogue> * ... * </catalogues> * <untranslated-text>untranslated</untranslated-text> * <preload>en_US</preload> * <preload catalogue="someId">fr_CA</preload> * </map:transformer> * </pre> * Where: * <ul> * <li><strong>catalogues</strong>: container element in which the catalogues * are defined. It must have an attribute 'default' whose value is one * of the id's of the catalogue elements. (<i>mandatory</i>). * <li><strong>catalogue</strong>: specifies a catalogue. It takes 2 required * attributes: id (can be wathever you like) and name (base name of the catalogue). * The location (location of the message catalogue) is also required, but can be * specified either as an attribute or as one or more subelements, but not both. * If more than one location is specified the catalogues will be searched in the * order they appear in the configuration. The name and location can contain * references to inputmodules (same syntax as in other places in the * sitemap). They are resolved on each usage of the transformer, so they can * refer to e.g. request parameters. (<i>at least 1 catalogue * element required</i>). After input module references are resolved the location * string can be the root of a URI. For example, specifying a location of * cocoon:/test with a name of messages and a locale of en_GB will cause the * sitemap to try to process cocoon:/test/messages_en_GB.xml, * cocoon:/test/messages_en.xml and cocoon:/test/messages.xml. * <li><strong>untranslated-text</strong>: text used for * untranslated keys (default is to output the key name). * <li><strong>preload</strong>: locale of the catalogue to preload. Will attempt * to resolve all configured catalogues for specified locale. If optional * <code>catalogue</code> attribute is present, will preload only specified * catalogue. Multiple <code>preload</code> elements can be specified. * </ul> * * <h3>Pipeline Usage</h3> * <p>To use the transformer in a pipeline, simply specify it in a particular * transform, and pass locale parameter: * <pre> * <map:match pattern="file"> * <map:generate src="file.xml"/> * <map:transform type="i18n"> * <map:parameter name="locale" value="..."/> * </map:transform> * <map:serialize/> * </map:match> * </pre> * You can use {@link org.apache.cocoon.acting.LocaleAction} or any other * way to provide transformer with a locale.</p> * * <p>If in certain pipeline, you want to use a different catalogue as the * default catalogue, you can do so by specifying a parameter called * <strong>default-catalogue-id</strong>. * * <p>The <strong>untranslated-text</strong> can also be overridden at the * pipeline level by specifying it as a parameter.</p> * * * <h3>i18n markup</h3> * * <p>For date, time and number formatting use the following tags: * <ul> * <li><strong><i18n:date/></strong> gives localized date.</li> * <li><strong><i18n:date-time/></strong> gives localized date and time.</li> * <li><strong><i18n:time/></strong> gives localized time.</li> * <li><strong><i18n:number/></strong> gives localized number.</li> * <li><strong><i18n:currency/></strong> gives localized currency.</li> * <li><strong><i18n:percent/></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> * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"> * 12/24/01 * </i18n:date> * </pre> * Will result in 24.12.2001.</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> * * <p>Future work coming: * <ul> * <li>Introduce new <get-locale/> element * <li>Move all formatting routines to I18nUtils * </ul> * * @author <a href="mailto:kpiroumian@apache.org">Konstantin Piroumian</a> * @author <a href="mailto:mattam@netcourrier.com">Matthieu Sozeau</a> * @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a> * @author <a href="mailto:Michael.Enke@wincor-nixdorf.com">Michael Enke</a> * @version $Id$ */ public class I18nTransformer extends AbstractTransformer implements CacheableProcessingComponent, Serviceable, Configurable, Disposable { /** * The namespace for i18n is "http://apache.org/cocoon/i18n/2.1". */ public static final String I18N_NAMESPACE_URI = I18nUtils.NAMESPACE_URI; /** * The old namespace for i18n is "http://apache.org/cocoon/i18n/2.0". */ public static final String I18N_OLD_NAMESPACE_URI = I18nUtils.OLD_NAMESPACE_URI; // // i18n elements // /** * <code>i18n:text</code> element is used to translate any text, with * or without markup. Example: * <pre> * <i18n:text> * This is <strong>translated</strong> string. * </i18n:text> * </pre> */ public static final String I18N_TEXT_ELEMENT = "text"; /** * <code>i18n:translate</code> element is used to translate text with * parameter substitution. Example: * <pre> * <i18n:translate> * <i18n:text>This is translated string with {0} param</i18n:text> * <i18n:param>1</i18n:param> * </i18n:translate> * </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 #I18N_TEXT_ELEMENT * @see #I18N_PARAM_ELEMENT */ public static final String I18N_TRANSLATE_ELEMENT = "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 <i18n:choose>.</em></p> * <pre> * <i18n:choose> * <i18n:when locale="en"> * Good Morning * </en> * <i18n:when locale="fr"> * Bonjour * </jp> * <i18n:when locale="jp"> * Aligato? * </jp> * <i18n:otherwise> * Sorry, i don't know how to say hello in your language * </jp> * <i18n:translate> * </pre> * <p>You can include any markup within <code>i18n:when</code> elements, * with the exception of other <code>i18n:*</code> elements.</p> * * @see #I18N_IF_ELEMENT * @see #I18N_LOCALE_ATTRIBUTE * @since 2.1 */ public static final String I18N_CHOOSE_ELEMENT = "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> * <greeting> * <i18n:when locale="en">Hello</i18n:when> * <i18n:when locale="fr">Bonjour</i18n:when> * </greeting> * </pre> * * @see #I18N_LOCALE_ATTRIBUTE * @see #I18N_CHOOSE_ELEMENT * @since 2.1 */ public static final String I18N_WHEN_ELEMENT = "when"; /** * <code>i18n:if</code> is used to test a locale. Example: * <pre> * <greeting> * <i18n:if locale="en">Hello</i18n:when> * <i18n:if locale="fr">Bonjour</i18n:when> * </greeting> * </pre> * * @see #I18N_LOCALE_ATTRIBUTE * @see #I18N_CHOOSE_ELEMENT * @see #I18N_WHEN_ELEMENT * @since 2.1 */ public static final String I18N_IF_ELEMENT = "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 #I18N_CHOOSE_ELEMENT * @see #I18N_WHEN_ELEMENT * @since 2.1 */ public static final String I18N_OTHERWISE_ELEMENT = "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 #I18N_TRANSLATE_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_PARAM_ELEMENT = "param"; /** * This attribute affects a name to the param that could be used * for substitution. * * @since 2.1 */ public static final String I18N_PARAM_NAME_ATTRIBUTE = "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> * <i18n:date src-pattern="short" src-locale="en_US" locale="de_DE"> * 12/24/01 * </i18n:date> * * <i18n:date pattern="dd/MM/yyyy" /> * </pre> * * If no value is specified then the current date will be used. E.g.: * <pre> * <i18n:date /> * </pre> * Displays the current date formatted with default pattern for * the current locale. * * @see #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_DATE_ELEMENT = "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> * <i18n:date-time src-pattern="short" src-locale="en_US" locale="de_DE"> * 12/24/01 1:00 AM * </i18n:date> * * <i18n:date-time pattern="dd/MM/yyyy hh:mm" /> * </pre> * * If no value is specified then the current date and time will be used. * E.g.: * <pre> * <i18n:date-time /> * </pre> * Displays the current date formatted with default pattern for * the current locale. * * @see #I18N_PARAM_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_DATE_TIME_ELEMENT = "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> * <i18n:time src-pattern="short" src-locale="en_US" locale="de_DE"> * 1:00 AM * </i18n:time> * * <i18n:time pattern="hh:mm:ss" /> * </pre> * * If no value is specified then the current time will be used. E.g.: * <pre> * <i18n:time /> * </pre> * Displays the current time formatted with default pattern for * the current locale. * * @see #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_TIME_ELEMENT = "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> * <i18n:number src-pattern="short" src-locale="en_US" locale="de_DE"> * 1000.0 * </i18n:number> * * <i18n:number type="currency" /> * </pre> * * If no value is specifies then 0 will be used. * * @see #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_DATE_ELEMENT */ public static final String I18N_NUMBER_ELEMENT = "number"; /** * Currency element name */ public static final String I18N_CURRENCY_ELEMENT = "currency"; /** * Percent element name */ public static final String I18N_PERCENT_ELEMENT = "percent"; /** * Integer currency element name */ public static final String I18N_INT_CURRENCY_ELEMENT = "int-currency"; /** * Currency without unit element name */ public static final String I18N_CURRENCY_NO_UNIT_ELEMENT = "currency-no-unit"; /** * Integer currency without unit element name */ public static final String I18N_INT_CURRENCY_NO_UNIT_ELEMENT = "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> * <i18n:text i18n:key="a_key">article_text1</i18n:text> * </pre> */ public static final String I18N_KEY_ATTRIBUTE = "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> * <para title="first" name="article" i18n:attr="title name"/> * </pre> * Attribute value considered as key in message catalogue. */ public static final String I18N_ATTR_ATTRIBUTE = "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> * <para title="first" name="{one} {two}" i18n:attr="name"/> * </pre> * Attribute value considered as expression containing text and catalogue * keys in curly braces. */ public static final String I18N_EXPR_ATTRIBUTE = "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 #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_SRC_PATTERN_ATTRIBUTE = "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 #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_PATTERN_ATTRIBUTE = "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 #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT * @see #I18N_WHEN_ELEMENT */ public static final String I18N_LOCALE_ATTRIBUTE = "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 #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_SRC_LOCALE_ATTRIBUTE = "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 #I18N_PARAM_ELEMENT * @see #I18N_DATE_TIME_ELEMENT * @see #I18N_DATE_ELEMENT * @see #I18N_TIME_ELEMENT * @see #I18N_NUMBER_ELEMENT */ public static final String I18N_VALUE_ATTRIBUTE = "value"; /** * This attribute is used with <code>i18:param</code> to * indicate the parameter type: <code>date, time, date-time</code> or * <code>number, currency, percent, int-currency, currency-no-unit, * int-currency-no-unit</code>. * Also used with <code>i18:translate</code> to indicate inplace * translations: <code>inplace</code> * @deprecated since 2.1. Use nested tags instead, e.g.: * <i18n:param><i18n:date/></i18n:param> */ public static final String I18N_TYPE_ATTRIBUTE = "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 CURRENCY_LOCALE_ATTRIBUTE = "currency"; /** * This attribute can be used on <code>i18n:text</code> to indicate the catalogue * from which the key should be retrieved. This attribute is optional, * if it is not mentioned the default catalogue is used. */ public static final String I18N_CATALOGUE_ATTRIBUTE = "catalogue"; // // Configuration parameters // /** * This configuration parameter specifies the default locale to be used. */ public static final String I18N_LOCALE = "locale"; /** * This configuration parameter specifies the id of the catalogue to be used as * default catalogue, allowing to redefine the default catalogue on the pipeline * level. */ public static final String I18N_DEFAULT_CATALOGUE_ID = "default-catalogue-id"; /** * This configuration parameter specifies the message that should be * displayed in case of a not translated text (message not found). */ public static final String I18N_UNTRANSLATED = "untranslated-text"; /** * This configuration parameter specifies locale for which catalogues should * be preloaded. */ public static final String I18N_PRELOAD = "preload"; /** * <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 I18N_FRACTION_DIGITS_ATTRIBUTE = "fraction-digits"; // // States of the transformer // private static final int STATE_OUTSIDE = 0; private static final int STATE_INSIDE_TEXT = 10; private static final int STATE_INSIDE_PARAM = 20; private static final int STATE_INSIDE_TRANSLATE = 30; private static final int STATE_INSIDE_CHOOSE = 50; private static final int STATE_INSIDE_WHEN = 51; private static final int STATE_INSIDE_OTHERWISE = 52; private static final int STATE_INSIDE_DATE = 60; private static final int STATE_INSIDE_DATE_TIME = 61; private static final int STATE_INSIDE_TIME = 62; private static final int STATE_INSIDE_NUMBER = 63; // All date-time related parameter types and element names private static final Set dateTypes; // All number related parameter types and element names private static final Set numberTypes; // Date pattern types map: short, medium, long, full private static final Map datePatterns; static { // initialize date types set HashSet set = new HashSet(5); set.add(I18N_DATE_ELEMENT); set.add(I18N_TIME_ELEMENT); set.add(I18N_DATE_TIME_ELEMENT); dateTypes = Collections.unmodifiableSet(set); // initialize number types set set = new HashSet(9); set.add(I18N_NUMBER_ELEMENT); set.add(I18N_PERCENT_ELEMENT); set.add(I18N_CURRENCY_ELEMENT); set.add(I18N_INT_CURRENCY_ELEMENT); set.add(I18N_CURRENCY_NO_UNIT_ELEMENT); set.add(I18N_INT_CURRENCY_NO_UNIT_ELEMENT); numberTypes = Collections.unmodifiableSet(set); // Initialize date patterns map Map map = new HashMap(7); map.put("SHORT", new Integer(DateFormat.SHORT)); map.put("MEDIUM", new Integer(DateFormat.MEDIUM)); map.put("LONG", new Integer(DateFormat.LONG)); map.put("FULL", new Integer(DateFormat.FULL)); datePatterns = Collections.unmodifiableMap(map); } // // Global configuration variables // /** * Component (service) manager */ protected ServiceManager manager; /** * Message bundle loader factory component (service) */ protected BundleFactory factory; /** * All catalogues (keyed by catalogue id). The values are instances * of {@link CatalogueInfo}. */ private Map catalogues; /** * Default (global) catalogue */ private CatalogueInfo defaultCatalogue; /** * Default (global) untranslated message value */ private String defaultUntranslated; // // Local configuration variables // protected Map objectModel; /** * Locale */ protected Locale locale; /** * Catalogue (local) */ private CatalogueInfo catalogue; /** * 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 STATE_OUTSIDE. */ private int current_state; /** * Previous state of the transformer. * Used in text translation inside params and translate elements. */ private int prev_state; /** * 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; /** * Contains the id of the current catalogue if it was explicitely mentioned * on an i18n:text element, otherwise it is null. */ private String currentCatalogueId; /** * Character data buffer. used to concat chunked character data */ private StringBuffer strBuffer; /** * A flag for copying the node when doing in-place translation */ private boolean translate_copy; // A flag for copying the _GOOD_ node and not others // when doing in-place translation within i18n:choose private boolean translate_end; // Translated text. Inside i18n:translate, collects character events. private ParamSaxBuffer tr_text_recorder; // Current "i18n:text" events private ParamSaxBuffer text_recorder; // Current parameter events private SaxBuffer param_recorder; // Param count when not using i18n:param name="..." private int param_count; // Param name attribute for substitution. private String param_name; // i18n:param's hashmap for substitution private HashMap indexedParams; // Current parameter value (translated or not) private String param_value; // Date and number elements and params formatting attributes with values. private HashMap formattingParams; // The namespaces and their prefixes private Map namespaces; /** * Returns the current locale setting of this transformer instance. * @return current Locale object */ public Locale getLocale() { return this.locale; } /** * Implemenation of CacheableProcessingComponents. * Generates unique key for the current locale. */ public java.io.Serializable getKey() { // TODO: Key should be composed out of used catalogues locations, and locale. // Right now it is hardcoded only to default catalogue location. StringBuffer key = new StringBuffer(); if (catalogue != null) { key.append(catalogue.getLocation()[0]); } key.append("?"); if (locale != null) { key.append(locale.getLanguage()); key.append("_"); key.append(locale.getCountry()); key.append("_"); key.append(locale.getVariant()); } return key.toString(); } /** * Implementation of CacheableProcessingComponent. * Generates validity object for this transformer or <code>null</code> * if this instance is not cacheable. */ public SourceValidity getValidity() { // FIXME (KP): Cache validity should be generated by // Bundle implementations. return org.apache.excalibur.source.impl.validity.NOPValidity.SHARED_INSTANCE; } /** * Look up the {@link BundleFactory} to be used. */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; try { this.factory = (BundleFactory) manager.lookup(BundleFactory.ROLE); } catch (ServiceException e) { getLogger().debug("Failed to lookup <" + BundleFactory.ROLE + ">", e); throw e; } } /** * Implementation of Configurable interface. * Configure this transformer. */ public void configure(Configuration conf) throws ConfigurationException { // Read in the config options from the transformer definition Configuration cataloguesConf = conf.getChild("catalogues", false); if (cataloguesConf == null) { throw new ConfigurationException("Required <catalogues> configuration is missing.", conf); } // new configuration style Configuration[] catalogueConfs = cataloguesConf.getChildren("catalogue"); catalogues = new HashMap(catalogueConfs.length + 3); for (int i = 0; i < catalogueConfs.length; i++) { String id = catalogueConfs[i].getAttribute("id"); String name = catalogueConfs[i].getAttribute("name"); String[] locations; String location = catalogueConfs[i].getAttribute("location", null); Configuration[] locationConf = catalogueConfs[i].getChildren("location"); if (location != null) { if (locationConf.length > 0) { String msg = "Location attribute cannot be " + "specified with location elements"; getLogger().error(msg); throw new ConfigurationException(msg, catalogueConfs[i]); } if (getLogger().isDebugEnabled()) { getLogger().debug("name=" + name + ", location=" + location); } locations = new String[1]; locations[0] = location; } else { if (locationConf.length == 0) { String msg = "A location attribute or location " + "elements must be specified"; getLogger().error(msg); throw new ConfigurationException(msg, catalogueConfs[i]); } locations = new String[locationConf.length]; for (int j=0; j < locationConf.length; ++j) { locations[j] = locationConf[j].getValue(); if (getLogger().isDebugEnabled()) { getLogger().debug("name=" + name + ", location=" + locations[j]); } } } CatalogueInfo catalogueInfo; try { catalogueInfo = new CatalogueInfo(name, locations); } catch (PatternException e) { throw new ConfigurationException("Error in name or location attribute on catalogue " + "element with id " + id, catalogueConfs[i], e); } catalogues.put(id, catalogueInfo); } String defaultCatalogueId = cataloguesConf.getAttribute("default"); defaultCatalogue = (CatalogueInfo) catalogues.get(defaultCatalogueId); if (defaultCatalogue == null) { throw new ConfigurationException("Default catalogue id '" + defaultCatalogueId + "' denotes a nonexisting catalogue", cataloguesConf); } // Obtain default text to use for untranslated messages defaultUntranslated = conf.getChild(I18N_UNTRANSLATED).getValue(null); if (getLogger().isDebugEnabled()) { getLogger().debug("Default untranslated text is '" + defaultUntranslated + "'"); } // Preload specified catalogues (if any) Configuration[] preloadConfs = conf.getChildren(I18N_PRELOAD); for (int i = 0; i < preloadConfs.length; i++) { String localeStr = preloadConfs[i].getValue(); this.locale = I18nUtils.parseLocale(localeStr); String id = preloadConfs[i].getAttribute("catalogue", null); if (id != null) { CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(id); if (catalogueInfo == null) { throw new ConfigurationException("Invalid catalogue id '" + id + "' in preload element.", preloadConfs[i]); } try { catalogueInfo.getCatalogue(); } finally { catalogueInfo.releaseCatalog(); } } else { for (Iterator j = catalogues.values().iterator(); j.hasNext(); ) { CatalogueInfo catalogueInfo = (CatalogueInfo) j.next(); try { catalogueInfo.getCatalogue(); } finally { catalogueInfo.releaseCatalog(); } } } } this.locale = null; } /** * Setup current instance of transformer. */ public void setup(SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws ProcessingException, SAXException, IOException { this.objectModel = objectModel; untranslated = parameters.getParameter(I18N_UNTRANSLATED, defaultUntranslated); if (untranslated != null) { untranslatedRecorder = new ParamSaxBuffer(); untranslatedRecorder.characters(untranslated.toCharArray(), 0, untranslated.length()); } // Get current locale String lc = parameters.getParameter(I18N_LOCALE, null); Locale locale = I18nUtils.parseLocale(lc); if (getLogger().isDebugEnabled()) { getLogger().debug("Using locale '" + locale + "'"); } // Initialize instance state variables this.locale = locale; this.current_state = STATE_OUTSIDE; this.prev_state = STATE_OUTSIDE; this.currentKey = null; this.currentCatalogueId = null; this.translate_copy = false; this.tr_text_recorder = null; this.text_recorder = new ParamSaxBuffer(); this.param_count = 0; this.param_name = null; this.param_value = null; this.param_recorder = null; this.indexedParams = new HashMap(3); this.formattingParams = null; this.strBuffer = null; this.namespaces = new HashMap(5); // give the catalogue variable its value -- first look if it's locally overridden // and otherwise use the component-wide defaults. String catalogueId = parameters.getParameter(I18N_DEFAULT_CATALOGUE_ID, null); if (catalogueId != null) { CatalogueInfo catalogueInfo = (CatalogueInfo) catalogues.get(catalogueId); if (catalogueInfo == null) { throw new ProcessingException("I18nTransformer: '" + catalogueId + "' is not an existing catalogue id."); } catalogue = catalogueInfo; } else { catalogue = defaultCatalogue; } if (getLogger().isDebugEnabled()) { getLogger().debug("Default catalogue is " + catalogue.getName()); } } // // Standard SAX event handlers // /** * Process the SAX event. * @see org.xml.sax.ContentHandler#startPrefixMapping */ public void startPrefixMapping(String prefix, String uri) throws SAXException { // consume i18n prefix mappings namespaces.put(prefix,uri); if (!I18nUtils.matchesI18nNamespace(uri)) { super.startPrefixMapping(prefix, uri); } } /** * Process the SAX event. * @see org.xml.sax.ContentHandler#endPrefixMapping */ public void endPrefixMapping(String prefix) throws SAXException { if (!I18nUtils.matchesI18nNamespace((String)namespaces.get(prefix))) { super.endPrefixMapping(prefix); } namespaces.remove(prefix); } public void startElement(String uri, String name, String raw, Attributes attr) throws SAXException { // Handle previously buffered characters if (current_state != STATE_OUTSIDE && strBuffer != null) { i18nCharacters(strBuffer.toString()); strBuffer = null; } // Process start element event if (I18nUtils.matchesI18nNamespace(uri)) { if (getLogger().isDebugEnabled()) { getLogger().debug("Starting i18n element: " + name); } startI18NElement(name, attr); } else { // We have a non i18n element event if (current_state == STATE_OUTSIDE) { super.startElement(uri, name, raw, translateAttributes(name, attr)); } else if (current_state == STATE_INSIDE_PARAM) { param_recorder.startElement(uri, name, raw, attr); } else if (current_state == STATE_INSIDE_TEXT) { text_recorder.startElement(uri, name, raw, attr); } else if ((current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE) && translate_copy) { super.startElement(uri, name, raw, attr); } } } public void endElement(String uri, String name, String raw) throws SAXException { // Handle previously buffered characters if (current_state != STATE_OUTSIDE && strBuffer != null) { i18nCharacters(strBuffer.toString()); strBuffer = null; } if (I18nUtils.matchesI18nNamespace(uri)) { endI18NElement(name); } else if (current_state == STATE_INSIDE_PARAM) { param_recorder.endElement(uri, name, raw); } else if (current_state == STATE_INSIDE_TEXT) { text_recorder.endElement(uri, name, raw); } else if (current_state == STATE_INSIDE_CHOOSE || (current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE) && !translate_copy) { // Output nothing } else { super.endElement(uri, name, raw); } } public void characters(char[] ch, int start, int len) throws SAXException { if (current_state == STATE_OUTSIDE || ((current_state == STATE_INSIDE_WHEN || current_state == STATE_INSIDE_OTHERWISE) && translate_copy)) { super.characters(ch, start, len); } else { // Perform buffering to prevent chunked character data if (strBuffer == null) { strBuffer = new StringBuffer(); } strBuffer.append(ch, start, len); } } // // i18n specific event handlers // private void startI18NElement(String name, Attributes attr) throws SAXException { if (getLogger().isDebugEnabled()) { getLogger().debug("Start i18n element: " + name); } if (I18N_TEXT_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE && current_state != STATE_INSIDE_PARAM && current_state != STATE_INSIDE_TRANSLATE) { throw new SAXException( getClass().getName() + ": nested i18n:text elements are not allowed." + " Current state: " + current_state); } prev_state = current_state; current_state = STATE_INSIDE_TEXT; currentKey = attr.getValue("", I18N_KEY_ATTRIBUTE); if (currentKey == null) { // Try the namespaced attribute currentKey = attr.getValue(I18N_NAMESPACE_URI, I18N_KEY_ATTRIBUTE); if (currentKey == null) { // Try the old namespace currentKey = attr.getValue(I18N_OLD_NAMESPACE_URI, I18N_KEY_ATTRIBUTE); } } currentCatalogueId = attr.getValue("", I18N_CATALOGUE_ATTRIBUTE); if (currentCatalogueId == null) { // Try the namespaced attribute currentCatalogueId = attr.getValue(I18N_NAMESPACE_URI, I18N_CATALOGUE_ATTRIBUTE); } if (prev_state != STATE_INSIDE_PARAM) { tr_text_recorder = null; } if (currentKey != null) { tr_text_recorder = getMessage(currentKey, (ParamSaxBuffer)null); } } else if (I18N_TRANSLATE_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE) { throw new SAXException( getClass().getName() + ": i18n:translate element must be used " + "outside of other i18n elements. Current state: " + current_state); } prev_state = current_state; current_state = STATE_INSIDE_TRANSLATE; } else if (I18N_PARAM_ELEMENT.equals(name)) { if (current_state != STATE_INSIDE_TRANSLATE) { throw new SAXException( getClass().getName() + ": i18n:param element can be used only inside " + "i18n:translate element. Current state: " + current_state); } param_name = attr.getValue(I18N_PARAM_NAME_ATTRIBUTE); if (param_name == null) { param_name = String.valueOf(param_count++); } param_recorder = new SaxBuffer(); setFormattingParams(attr); current_state = STATE_INSIDE_PARAM; } else if (I18N_CHOOSE_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE) { throw new SAXException( getClass().getName() + ": i18n:choose elements cannot be used" + "inside of other i18n elements."); } translate_copy = false; translate_end = false; prev_state = current_state; current_state = STATE_INSIDE_CHOOSE; } else if (I18N_WHEN_ELEMENT.equals(name) || I18N_IF_ELEMENT.equals(name)) { if (I18N_WHEN_ELEMENT.equals(name) && current_state != STATE_INSIDE_CHOOSE) { throw new SAXException( getClass().getName() + ": i18n:when elements are can be used only" + "inside of i18n:choose elements."); } if (I18N_IF_ELEMENT.equals(name) && current_state != STATE_OUTSIDE) { throw new SAXException( getClass().getName() + ": i18n:if elements cannot be nested."); } String locale = attr.getValue(I18N_LOCALE_ATTRIBUTE); if (locale == null) throw new SAXException( getClass().getName() + ": i18n:" + name + " element cannot be used without 'locale' attribute."); if ((!translate_end && current_state == STATE_INSIDE_CHOOSE) || current_state == STATE_OUTSIDE) { // Perform soft locale matching if (this.locale.toString().startsWith(locale)) { if (getLogger().isDebugEnabled()) { getLogger().debug("Locale matching: " + locale); } translate_copy = true; } } prev_state = current_state; current_state = STATE_INSIDE_WHEN; } else if (I18N_OTHERWISE_ELEMENT.equals(name)) { if (current_state != STATE_INSIDE_CHOOSE) { throw new SAXException( getClass().getName() + ": i18n:otherwise elements are not allowed " + "only inside i18n:choose."); } getLogger().debug("Matching any locale"); if (!translate_end) { translate_copy = true; } prev_state = current_state; current_state = STATE_INSIDE_OTHERWISE; } else if (I18N_DATE_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE && current_state != STATE_INSIDE_TEXT && current_state != STATE_INSIDE_PARAM) { throw new SAXException( getClass().getName() + ": i18n:date elements are not allowed " + "inside of other i18n elements."); } setFormattingParams(attr); prev_state = current_state; current_state = STATE_INSIDE_DATE; } else if (I18N_DATE_TIME_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE && current_state != STATE_INSIDE_TEXT && current_state != STATE_INSIDE_PARAM) { throw new SAXException( getClass().getName() + ": i18n:date-time elements are not allowed " + "inside of other i18n elements."); } setFormattingParams(attr); prev_state = current_state; current_state = STATE_INSIDE_DATE_TIME; } else if (I18N_TIME_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE && current_state != STATE_INSIDE_TEXT && current_state != STATE_INSIDE_PARAM) { throw new SAXException( getClass().getName() + ": i18n:date elements are not allowed " + "inside of other i18n elements."); } setFormattingParams(attr); prev_state = current_state; current_state = STATE_INSIDE_TIME; } else if (I18N_NUMBER_ELEMENT.equals(name)) { if (current_state != STATE_OUTSIDE && current_state != STATE_INSIDE_TEXT && current_state != STATE_INSIDE_PARAM) { throw new SAXException( getClass().getName() + ": i18n:number elements are not allowed " + "inside of other i18n elements."); } setFormattingParams(attr); prev_state = current_state; current_state = STATE_INSIDE_NUMBER; } } // Get all possible i18n formatting attribute values and store in a Map private void setFormattingParams(Attributes attr) { // average number of attributes is 3 formattingParams = new HashMap(3); String attr_value = attr.getValue(I18N_SRC_PATTERN_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_SRC_PATTERN_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_PATTERN_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_PATTERN_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_VALUE_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_VALUE_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_LOCALE_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_LOCALE_ATTRIBUTE, attr_value); } attr_value = attr.getValue(CURRENCY_LOCALE_ATTRIBUTE); if (attr_value != null) { formattingParams.put(CURRENCY_LOCALE_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_SRC_LOCALE_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_SRC_LOCALE_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_TYPE_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_TYPE_ATTRIBUTE, attr_value); } attr_value = attr.getValue(I18N_FRACTION_DIGITS_ATTRIBUTE); if (attr_value != null) { formattingParams.put(I18N_FRACTION_DIGITS_ATTRIBUTE, attr_value); } } private void endI18NElement(String name) throws SAXException { if (getLogger().isDebugEnabled()) { getLogger().debug("End i18n element: " + name); } switch (current_state) { case STATE_INSIDE_TEXT: endTextElement(); break; case STATE_INSIDE_TRANSLATE: endTranslateElement(); break; case STATE_INSIDE_CHOOSE: endChooseElement(); break; case STATE_INSIDE_WHEN: case STATE_INSIDE_OTHERWISE: endWhenElement(); break; case STATE_INSIDE_PARAM: endParamElement(); break; case STATE_INSIDE_DATE: case STATE_INSIDE_DATE_TIME: case STATE_INSIDE_TIME: endDate_TimeElement(); break; case STATE_INSIDE_NUMBER: endNumberElement(); break; } } private void i18nCharacters(String textValue) throws SAXException { if (getLogger().isDebugEnabled()) { getLogger().debug("i18n message text = '" + textValue + "'"); } SaxBuffer buffer; switch (current_state) { case STATE_INSIDE_TEXT: buffer = text_recorder; break; case STATE_INSIDE_PARAM: buffer = param_recorder; break; case STATE_INSIDE_WHEN: case STATE_INSIDE_OTHERWISE: // Previously handeld to avoid the String() conversion. return; case STATE_INSIDE_TRANSLATE: if (tr_text_recorder == null) { tr_text_recorder = new ParamSaxBuffer(); } buffer = tr_text_recorder; break; case STATE_INSIDE_CHOOSE: // No characters allowed. Send an exception ? if (getLogger().isDebugEnabled()) { textValue = textValue.trim(); if (textValue.length() > 0) { getLogger().debug("No characters allowed inside <i18n:choose> tag. Received: " + textValue); } } return; case STATE_INSIDE_DATE: case STATE_INSIDE_DATE_TIME: case STATE_INSIDE_TIME: case STATE_INSIDE_NUMBER: // Trim text values to avoid parsing errors. textValue = textValue.trim(); if (textValue.length() > 0) { if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null) { formattingParams.put(I18N_VALUE_ATTRIBUTE, textValue); } else { // ignore the text inside of date element } } return; default: throw new IllegalStateException(getClass().getName() + " developer's fault: characters not handled. " + "Current state: " + current_state); } char[] ch = textValue.toCharArray(); buffer.characters(ch, 0, ch.length); } // Translate all attributes that are listed in i18n:attr attribute 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(I18N_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE); if (attrIndex == -1) { // Try the old namespace attrIndex = attr.getIndex(I18N_OLD_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE); } if (attrIndex != -1) { StringTokenizer st = 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); // Iterate through listed attributes and translate them while (st.hasMoreElements()) { final String name = st.nextToken(); int index = tempAttr.getIndex(name); if (index == -1) { getLogger().warn("Attribute " + name + " not found in element <" + element + ">"); continue; } String 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(I18N_NAMESPACE_URI, I18N_EXPR_ATTRIBUTE); if (attrIndex != -1) { StringTokenizer st = new StringTokenizer(attr.getValue(attrIndex)); if (tempAttr == null) { tempAttr = new AttributesImpl(attr); } tempAttr.removeAttribute(attrIndex); // Iterate through listed attributes and evaluate them while (st.hasMoreElements()) { final String name = st.nextToken(); int index = tempAttr.getIndex(name); if (index == -1) { getLogger().warn("Attribute " + name + " not found in element <" + element + ">"); continue; } final StringBuffer translated = new StringBuffer(); // Evaluate {..} expression VariableExpressionTokenizer.TokenReciever tr = new VariableExpressionTokenizer.TokenReciever () { private String catalogueName; public void addToken(int type, String value) { if (type == MODULE) { this.catalogueName = value; } else if (type == VARIABLE) { translated.append(translateAttribute(element, name, value)); } else if (type == TEXT) { if (this.catalogueName != null) { translated.append(translateAttribute(element, name, this.catalogueName + ":" + value)); this.catalogueName = null; } else if (value != null) { translated.append(value); } } } }; try { VariableExpressionTokenizer.tokenize(tempAttr.getValue(index), tr); } catch (PatternException 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. * Value can be prefixed with catalogue ID and semicolon. * @return Translated text, untranslated text, or null. */ private String translateAttribute(String element, String name, String key) { // Check if the key contains a colon, if so the text before // the colon denotes a catalogue ID. int colonPos = key.indexOf(":"); String catalogueID = null; if (colonPos != -1) { catalogueID = key.substring(0, colonPos); key = key.substring(colonPos + 1, key.length()); } final SaxBuffer text = getMessage(catalogueID, key); if (text == null) { getLogger().warn("Translation not found for attribute " + name + " in element <" + element + ">"); return untranslated; } return text.toString(); } private void endTextElement() throws SAXException { switch (prev_state) { case STATE_OUTSIDE: if (tr_text_recorder == null) { if (currentKey == null) { // Use the text as key. Not recommended for large strings, // especially if they include markup. tr_text_recorder = getMessage(text_recorder.toString(), text_recorder); } else { // We have the key, but couldn't find a translation if (getLogger().isDebugEnabled()) { getLogger().debug("Translation not found for key '" + currentKey + "'"); } // Use the untranslated-text only when the content of the i18n:text // element was empty if (text_recorder.isEmpty() && untranslatedRecorder != null) { tr_text_recorder = untranslatedRecorder; } else { tr_text_recorder = text_recorder; } } } if (tr_text_recorder != null) { tr_text_recorder.toSAX(this.contentHandler); } text_recorder.recycle(); tr_text_recorder = null; currentKey = null; currentCatalogueId = null; break; case STATE_INSIDE_TRANSLATE: if (tr_text_recorder == null) { if (!text_recorder.isEmpty()) { tr_text_recorder = getMessage(text_recorder.toString(), text_recorder); if (tr_text_recorder == text_recorder) { // If the default value was returned, make a copy tr_text_recorder = new ParamSaxBuffer(text_recorder); } } } text_recorder.recycle(); break; case STATE_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 (!text_recorder.isEmpty()) { getMessage(text_recorder.toString(), text_recorder).toSAX(param_recorder); text_recorder.recycle(); } break; } current_state = prev_state; prev_state = STATE_OUTSIDE; } // Process substitution parameter private void endParamElement() throws SAXException { String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE); if (paramType != null) { // We have a typed parameter if (getLogger().isDebugEnabled()) { getLogger().debug("Param type: " + paramType); } if (formattingParams.get(I18N_VALUE_ATTRIBUTE) == null && param_value != null) { if (getLogger().isDebugEnabled()) { getLogger().debug("Put param value: " + param_value); } formattingParams.put(I18N_VALUE_ATTRIBUTE, param_value); } // Check if we have a date or a number parameter if (dateTypes.contains(paramType)) { if (getLogger().isDebugEnabled()) { getLogger().debug("Formatting date_time param: " + formattingParams); } param_value = formatDate_Time(formattingParams); } else if (numberTypes.contains(paramType)) { if (getLogger().isDebugEnabled()) { getLogger().debug("Formatting number param: " + formattingParams); } param_value = formatNumber(formattingParams); } if (getLogger().isDebugEnabled()) { getLogger().debug("Added substitution param: " + param_value); } } param_value = null; current_state = STATE_INSIDE_TRANSLATE; if(param_recorder == null) { return; } indexedParams.put(param_name, param_recorder); param_recorder = null; } private void endTranslateElement() throws SAXException { if (tr_text_recorder != null) { if (getLogger().isDebugEnabled()) { getLogger().debug("End of translate with params. " + "Fragment for substitution : " + tr_text_recorder); } tr_text_recorder.toSAX(super.contentHandler, indexedParams); tr_text_recorder = null; text_recorder.recycle(); } indexedParams.clear(); param_count = 0; current_state = STATE_OUTSIDE; } private void endChooseElement() { current_state = STATE_OUTSIDE; } private void endWhenElement() { current_state = prev_state; if (translate_copy) { translate_copy = false; translate_end = true; } } private void endDate_TimeElement() throws SAXException { String result = formatDate_Time(formattingParams); switch(prev_state) { case STATE_OUTSIDE: super.contentHandler.characters(result.toCharArray(), 0, result.length()); break; case STATE_INSIDE_PARAM: param_recorder.characters(result.toCharArray(), 0, result.length()); break; case STATE_INSIDE_TEXT: text_recorder.characters(result.toCharArray(), 0, result.length()); break; } current_state = prev_state; } // Helper method: creates Locale object from a string value in a map private Locale getLocale(Map params, String attribute) { // the specific locale value String lc = (String)params.get(attribute); return I18nUtils.parseLocale(lc, this.locale); } private String formatDate_Time(Map params) throws SAXException { // Check that we have not null params if (params == null) { throw new IllegalArgumentException("Nothing to format"); } // Formatters SimpleDateFormat to_fmt; SimpleDateFormat from_fmt; // Date formatting styles int srcStyle = DateFormat.DEFAULT; int style = DateFormat.DEFAULT; // Date formatting patterns boolean realPattern = false; boolean realSrcPattern = false; // From locale Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE); // To locale Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE); // From pattern String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE); // To pattern String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE); // The date value String value = (String)params.get(I18N_VALUE_ATTRIBUTE); // A src-pattern attribute is present if (srcPattern != null) { // Check if we have a real pattern Integer patternValue = (Integer)datePatterns.get(srcPattern.toUpperCase()); if (patternValue != null) { srcStyle = patternValue.intValue(); } else { realSrcPattern = true; } } // A pattern attribute is present if (pattern != null) { Integer patternValue = (Integer)datePatterns.get(pattern.toUpperCase()); if (patternValue != null) { style = patternValue.intValue(); } else { realPattern = true; } } // If we are inside of a typed param String paramType = (String)formattingParams.get(I18N_TYPE_ATTRIBUTE); // Initializing date formatters if (current_state == STATE_INSIDE_DATE || I18N_DATE_ELEMENT.equals(paramType)) { to_fmt = (SimpleDateFormat)DateFormat.getDateInstance(style, loc); from_fmt = (SimpleDateFormat)DateFormat.getDateInstance( srcStyle, srcLoc ); } else if (current_state == STATE_INSIDE_DATE_TIME || I18N_DATE_TIME_ELEMENT.equals(paramType)) { to_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance( style, style, loc ); from_fmt = (SimpleDateFormat)DateFormat.getDateTimeInstance( srcStyle, srcStyle, srcLoc ); } else { // STATE_INSIDE_TIME or param type='time' to_fmt = (SimpleDateFormat)DateFormat.getTimeInstance(style, loc); from_fmt = (SimpleDateFormat)DateFormat.getTimeInstance( srcStyle, srcLoc ); } // parsed date object Date dateValue; // pattern overwrites locale format if (realSrcPattern) { from_fmt.applyPattern(srcPattern); } if (realPattern) { to_fmt.applyPattern(pattern); } // get current date and time by default if (value == null) { dateValue = new Date(); } else { try { dateValue = from_fmt.parse(value); } catch (ParseException pe) { throw new SAXException( this.getClass().getName() + "i18n:date - parsing error.", pe ); } } // we have all necessary data here: do formatting. if (getLogger().isDebugEnabled()) { getLogger().debug("### Formatting date: " + dateValue + " with localized pattern " + to_fmt.toLocalizedPattern() + " for locale: " + locale); } return to_fmt.format(dateValue); } private void endNumberElement() throws SAXException { String result = formatNumber(formattingParams); switch(prev_state) { case STATE_OUTSIDE: super.contentHandler.characters(result.toCharArray(), 0, result.length()); break; case STATE_INSIDE_PARAM: param_recorder.characters(result.toCharArray(), 0, result.length()); break; case STATE_INSIDE_TEXT: text_recorder.characters(result.toCharArray(), 0, result.length()); break; } current_state = prev_state; } private String formatNumber(Map params) throws SAXException { if (params == null) { throw new SAXException( this.getClass().getName() + ": i18n:number - error in element attributes." ); } // from pattern String srcPattern = (String)params.get(I18N_SRC_PATTERN_ATTRIBUTE); // to pattern String pattern = (String)params.get(I18N_PATTERN_ATTRIBUTE); // the number value String value = (String)params.get(I18N_VALUE_ATTRIBUTE); if (value == null) return ""; // type String type = (String)params.get(I18N_TYPE_ATTRIBUTE); // fraction-digits int fractionDigits = -1; try { String fd = (String)params.get(I18N_FRACTION_DIGITS_ATTRIBUTE); if (fd != null) fractionDigits = Integer.parseInt(fd); } catch (NumberFormatException nfe) { getLogger().warn("Error in number format with fraction-digits", nfe); } // parsed number Number numberValue; // locale, may be switched locale Locale loc = getLocale(params, I18N_LOCALE_ATTRIBUTE); Locale srcLoc = getLocale(params, I18N_SRC_LOCALE_ATTRIBUTE); // currency locale Locale currencyLoc = getLocale(params, CURRENCY_LOCALE_ATTRIBUTE); // decimal and grouping locale Locale dgLoc = null; if (currencyLoc != 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). dgLoc = loc; loc = currencyLoc; } // src format DecimalFormat from_fmt = (DecimalFormat)NumberFormat.getInstance(srcLoc); int int_currency = 0; // src-pattern overwrites locale format if (srcPattern != null) { from_fmt.applyPattern(srcPattern); } // to format DecimalFormat to_fmt; char dec = from_fmt.getDecimalFormatSymbols().getDecimalSeparator(); int decAt = 0; boolean appendDec = false; if (type == null || type.equals( I18N_NUMBER_ELEMENT )) { to_fmt = (DecimalFormat)NumberFormat.getInstance(loc); to_fmt.setMaximumFractionDigits(309); for (int i = value.length() - 1; i >= 0 && value.charAt(i) != dec; i--, decAt++) { } if (decAt < value.length())to_fmt.setMinimumFractionDigits(decAt); decAt = 0; for (int i = 0; i < value.length() && value.charAt(i) != dec; i++) { if (Character.isDigit(value.charAt(i))) { decAt++; } } to_fmt.setMinimumIntegerDigits(decAt); if (value.charAt(value.length() - 1) == dec) { appendDec = true; } } else if (type.equals( I18N_CURRENCY_ELEMENT )) { to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc); } else if (type.equals( I18N_INT_CURRENCY_ELEMENT )) { to_fmt = (DecimalFormat)NumberFormat.getCurrencyInstance(loc); int_currency = 1; for (int i = 0; i < to_fmt.getMaximumFractionDigits(); i++) { int_currency *= 10; } } else if ( type.equals( I18N_CURRENCY_NO_UNIT_ELEMENT ) ) { DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc ); to_fmt = (DecimalFormat) NumberFormat.getInstance( loc ); to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits()); to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits()); } else if ( type.equals( I18N_INT_CURRENCY_NO_UNIT_ELEMENT ) ) { DecimalFormat tmp = (DecimalFormat) NumberFormat.getCurrencyInstance( loc ); int_currency = 1; for ( int i = 0; i < tmp.getMaximumFractionDigits(); i++ ) int_currency *= 10; to_fmt = (DecimalFormat) NumberFormat.getInstance( loc ); to_fmt.setMinimumFractionDigits(tmp.getMinimumFractionDigits()); to_fmt.setMaximumFractionDigits(tmp.getMaximumFractionDigits()); } else if (type.equals( I18N_PERCENT_ELEMENT )) { to_fmt = (DecimalFormat)NumberFormat.getPercentInstance(loc); } else { throw new SAXException("<i18n:number>: unknown type: " + type); } if(fractionDigits > -1) { to_fmt.setMinimumFractionDigits(fractionDigits); to_fmt.setMaximumFractionDigits(fractionDigits); } if(dgLoc != null) { DecimalFormat df = (DecimalFormat)NumberFormat.getCurrencyInstance(dgLoc); DecimalFormatSymbols dfsNew = df.getDecimalFormatSymbols(); DecimalFormatSymbols dfsOrig = to_fmt.getDecimalFormatSymbols(); dfsOrig.setDecimalSeparator(dfsNew.getDecimalSeparator()); dfsOrig.setMonetaryDecimalSeparator(dfsNew.getMonetaryDecimalSeparator()); dfsOrig.setGroupingSeparator(dfsNew.getGroupingSeparator()); to_fmt.setDecimalFormatSymbols(dfsOrig); } // pattern overwrites locale format if (pattern != null) { to_fmt.applyPattern(pattern); } try { numberValue = from_fmt.parse(value); if (int_currency > 0) { numberValue = new Double(numberValue.doubleValue() / int_currency); } else { // what? } } catch (ParseException pe) { throw new SAXException(this.getClass().getName() + "i18n:number - parsing error.", pe); } // we have all necessary data here: do formatting. String result = to_fmt.format(numberValue); if (appendDec) result = result + dec; if (getLogger().isDebugEnabled()) { getLogger().debug("i18n:number result: " + result); } return result; } //-- Dictionary handling routines /** * Helper method to retrieve a message from the dictionary. * * @param catalogueID if not null, this catalogue will be used instead of the default one. * @return SaxBuffer containing message, or null if not found. */ protected ParamSaxBuffer getMessage(String catalogueID, String key) { if (getLogger().isDebugEnabled()) { getLogger().debug("Getting key " + key + " from catalogue " + catalogueID); } CatalogueInfo catalogue = this.catalogue; if (catalogueID != null) { catalogue = (CatalogueInfo)catalogues.get(catalogueID); if (catalogue == null) { if (getLogger().isWarnEnabled()) { getLogger().warn("Catalogue not found: " + catalogueID + ", will not translate key " + key); } return null; } } Bundle bundle = catalogue.getCatalogue(); if (bundle == null) { // Can't translate getLogger().debug("Untranslated key: '" + key + "'"); return null; } try { return (ParamSaxBuffer) bundle.getObject(key); } catch (MissingResourceException e) { getLogger().debug("Untranslated key: '" + key + "'"); } return null; } /** * Helper method to retrieve a message from the current dictionary. * A default value is returned if message is not found. * * @return SaxBuffer containing message, or defaultValue if not found. */ private ParamSaxBuffer getMessage(String key, ParamSaxBuffer defaultValue) { SaxBuffer value = getMessage(currentCatalogueId, key); if (value == null) { return defaultValue; } return new ParamSaxBuffer(value); } public void recycle() { this.untranslatedRecorder = null; this.catalogue = null; this.objectModel = null; this.namespaces.clear(); // Release catalogues which were selected for current locale Iterator i = catalogues.values().iterator(); while (i.hasNext()) { CatalogueInfo catalogueInfo = (CatalogueInfo) i.next(); catalogueInfo.releaseCatalog(); } super.recycle(); } public void dispose() { if (manager != null) { manager.release(factory); } factory = null; manager = null; catalogues = null; } /** * Holds information about one catalogue. The location and name of the catalogue * can contain references to input modules, and are resolved upon each transformer * usage. It is important that releaseCatalog is called when the transformer is recycled. */ public final class CatalogueInfo { VariableResolver name; VariableResolver[] locations; String resolvedName; String[] resolvedLocations; Bundle catalogue; public CatalogueInfo(String name, String[] locations) throws PatternException { this.name = VariableResolverFactory.getResolver(name, manager); this.locations = new VariableResolver[locations.length]; for (int i=0; i < locations.length; ++i) { this.locations[i] = VariableResolverFactory.getResolver(locations[i], manager); } } public String getName() { try { if (resolvedName == null) { resolve(); } } catch (Exception e) { // Ignore the error for now } return resolvedName; } public String[] getLocation() { try { if (resolvedName == null) { resolve(); } } catch (Exception e) { // Ignore the error for now } return resolvedLocations; } private void resolve() throws Exception { if (resolvedLocations == null) { resolvedLocations = new String[locations.length]; for (int i=0; i < resolvedLocations.length; ++i) { resolvedLocations[i] = locations[i].resolve(null, objectModel); } } if (resolvedName == null) { resolvedName = name.resolve(null, objectModel); } } public Bundle getCatalogue() { if (catalogue == null) { try { resolve(); catalogue = factory.select(resolvedLocations, resolvedName, locale); } catch (Exception e) { getLogger().error("Error obtaining catalogue '" + getName() + "' from <" + getLocation() + "> for locale " + locale, e); } } return catalogue; } public void releaseCatalog() { if (catalogue != null) { factory.release(catalogue); } catalogue = null; resolvedName = null; resolvedLocations = null; } } }