YesNoTranslator.MoneyTranslator.TranslatorAlternatesSource 
	service - see AppModule source below.
	Alternatively, a translator can be made available to the whole application and selected automatically based on the type 
	that it translates, by contributing it to the TranslatorSource service.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like   
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
    <h3>Contributing Translators</h3>
    Here we create two Translators and make them available to the whole application <strong>by name</strong>.
    <div class="eg">
        <t:form class="form-horizontal" t:id="inputs">
            <div class="form-group">
                <t:label for="newToTapestry" class="col-sm-3"/>
                <div class="col-sm-3">
                    <t:textfield t:id="newToTapestry" t:translate="yesno"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${newToTapestry}
                          
                        <span class="text-muted">(y, n, yes, no)</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <t:label for="price" class="col-sm-3"/>
                <div class="col-sm-3">
                    <t:textfield t:id="price" t:translate="money2"/>
                </div>
                <div class="col-sm-6">
                    <p class="form-control-static">
                        ${price}
                          
                        <span class="text-muted">(maximum 2 decimal places)</span>
                    </p>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-3 col-sm-offset-3">
                    <t:submit/>
                </div>
            </div>
        </t:form>
    </div>
    
    The first translator is called <em>yesno</em> and its class is <code>YesNoTranslator</code>.<br/>
    It parses "y", "n", "yes", and "no" (ignoring case), client-side, to Boolean, server-side, and back to "yes" or "no", client-side.<br/><br/>
    The second translator is called <em>money2</em> and its class is <code>MoneyTranslator</code>.<br/>
    It parses a String, client-side, to a BigDecimal with maximum of 2 decimal places, server-side, and vice-versa.<br/><br/>
    We made both translators available to the whole application by contributing them to the <code>TranslatorAlternatesSource</code> 
    service - see AppModule source below.
    Alternatively, a translator can be made available to the whole application and selected <strong>automatically</strong> based on the type 
    that it translates, by contributing it to the <code>TranslatorSource</code> service.<br/><br/>
    
    This example has also used localization, which is explained later in the Localization examples.<br/><br/> 
    References: 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/Translator.html">Translator</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/TranslatorSource.html">TranslatorSource</a>,  
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/TranslatorAlternatesSource.html">TranslatorAlternatesSource</a>,  
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/messages/ComponentMessagesSource.html">ComponentMessagesSource</a>, 
    <a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">Defining Tapestry IoC Services</a>.<br/><br/>
    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
        
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators_fr.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/ContributingTranslators.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/YesNoTranslator.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/TranslationMessages.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/TranslationMessages_fr.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/translators/MoneyTranslator.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
    </t:tabgroup>
</body>
</html>
newToTapestry-label=Are you new to Tapestry?
price-label=A price
newToTapestry-label=Êtes-vous familier avec Tapestry?
price-label=Une prix
package jumpstart.web.pages.examples.input;
import java.math.BigDecimal;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
@Import(stylesheet="css/examples/plain.css")
public class ContributingTranslators {
    // Screen fields
    @Property
    @Persist(PersistenceConstants.FLASH)
    private Boolean newToTapestry;
    @Property
    @Persist(PersistenceConstants.FLASH)
    private BigDecimal price;
}
.eg {
                margin: 20px 0;
                padding: 14px;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}
package jumpstart.web.translators;
import org.apache.tapestry5.Field;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.services.FormSupport;
public class YesNoTranslator implements Translator<Boolean> {
    private final String name;
    private final Class<Boolean> type;
    private final String messageKey;
    public YesNoTranslator(String name) {
        this.name = name;
        this.type = Boolean.class;
        this.messageKey = "yesno-format-exception";
    }
    @Override
    public String toClient(Boolean value) {
        return (value == null ? null : value == true ? "yes" : "no");
    }
    @Override
    public Boolean parseClient(Field field, String clientValue, String message) throws ValidationException {
        if (clientValue == null) {
            return null;
        }
        else {
            String s = clientValue.toLowerCase();
            if (s.equals("y") || s.equals("yes")) {
                return Boolean.TRUE;
            }
            else if (s.equals("n") || s.equals("no")) {
                return Boolean.FALSE;
            }
            else {
                throw new ValidationException(message);
            }
        }
    }
    @Override
    public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport) {
        // Do nothing; we don't yet support client-side validation.
        // formSupport.addValidation(field, "yesno", message, null);
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public Class<Boolean> getType() {
        return type;
    }
    @Override
    public String getMessageKey() {
        return messageKey;
    }
    
}
yesno-format-exception=Answer must be y, n, yes, or no for %s.  Upper case is permitted
money-format-exception=You must provide a valid money amount for %s.  
yesno-format-exception=Réponse doit être y, n, yes, ou no pour %s. Majuscules est autorisée.
money-format-exception=Vous devez donner une monnaie valide pour %s.
// Based on an example kindly provided by George Christman. See http://tapestry.1045711.n5.nabble.com/Currency-Converter-tp5719990.html .
package jumpstart.web.translators;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import org.apache.tapestry5.Field;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.FormSupport;
// This class represents money with BigDecimal.
public class MoneyTranslator implements Translator<BigDecimal> {
    private final String name;
    private final int maxDecimalPlaces;
    private final ThreadLocale threadLocale;
    private final Class<BigDecimal> type;
    private final String messageKey;
    private ParsePosition parsePosition;
    public MoneyTranslator(String name, int maxDecimalPlaces, ThreadLocale threadLocale) {
        this.name = name;
        this.maxDecimalPlaces = maxDecimalPlaces;
        this.threadLocale = threadLocale;
        type = BigDecimal.class;
        messageKey = "money-format-exception";
        parsePosition = new ParsePosition(0);
    }
    @Override
    public String toClient(BigDecimal value) {
        // We want the client locale's currency format...
        
        Locale locale = threadLocale.getLocale();
        DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
        
        // ...with the right number of decimal places...
        
        decimalFormat.setMaximumFractionDigits(maxDecimalPlaces);
        // ...without a currency symbol
        
        DecimalFormatSymbols decimalFormatSymbols = decimalFormat.getDecimalFormatSymbols();
        decimalFormatSymbols.setCurrencySymbol("");
        decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
        
        return decimalFormat.format(value).trim();
    }
    @Override
    public BigDecimal parseClient(Field field, String clientValue, String message) throws ValidationException {
        
        if (clientValue == null) {
            return null;
        }
        else {
            BigDecimal money = parseMoney(clientValue.trim(), message);
            
            if (money.scale() > maxDecimalPlaces) {
                throw new ValidationException(message);
            }
            
            return money;
        }
        
    }
    private BigDecimal parseMoney(String clientValue, String message) throws ValidationException {
        // Based on the techniques described in http://www.ibm.com/developerworks/java/library/j-numberformat/ .
        Locale locale = threadLocale.getLocale();
        // Try parsing as a decimal number...
        DecimalFormat numberFormat = (DecimalFormat) NumberFormat.getInstance(locale);
        numberFormat.setParseBigDecimal(true);
        // ...in the format of the client locale (eg. 12,345.67 or 12345.67 or -12,345.67 or -12 345,67)
        numberFormat.setNegativePrefix("-");
        numberFormat.setNegativeSuffix("");
        parsePosition.setIndex(0);
        BigDecimal bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);
        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }
        // ...with trailing negative sign (eg. 12,345.67-)
        numberFormat.setNegativePrefix("");
        numberFormat.setNegativeSuffix("-");
        parsePosition.setIndex(0);
        bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);
        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }
        // ...with brackets for negative (eg. (12,345.67))
        numberFormat.setNegativePrefix("(");
        numberFormat.setNegativeSuffix(")");
        parsePosition.setIndex(0);
        bigDecimal = (BigDecimal) numberFormat.parse(clientValue, parsePosition);
        if (parsePosition.getIndex() == clientValue.length() && bigDecimal != null) {
            return bigDecimal;
        }
        throw new ValidationException(message);
    }
    @Override
    public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport) {
        // Do nothing; we don't yet support client-side validation.
        // formSupport.addValidation(field, name, message, null);
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public Class<BigDecimal> getType() {
        return type;
    }
    @Override
    public String getMessageKey() {
        return messageKey;
    }
}
package jumpstart.web.services;
import java.util.Map;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
import org.apache.tapestry5.internal.beanvalidator.BaseCCD;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.DisplayBlockContribution;
import org.apache.tapestry5.services.EditBlockContribution;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.DataConstants;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import jumpstart.business.validation.constraints.Letters;
import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;
/**
 * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to configure and extend
 * Tapestry, or to place your own service definitions. See http://tapestry.apache.org/5.3.4/tapestry-ioc/module.html
 */
public class AppModule {
    private static final String UPLOADS_PATH = "jumpstart.upload-path";
    @Inject
    @Symbol(SymbolConstants.PRODUCTION_MODE)
    @Property(write = false)
    private static boolean productionMode;
    // Add 2 services to those provided by Tapestry.
    // - CountryNames, and SelectIdModelFactory are used by pages which ask Tapestry to @Inject them.
    public static void bind(ServiceBinder binder) {
        binder.bind(CountryNames.class);
        binder.bind(SelectIdModelFactory.class, SelectIdModelFactoryImpl.class);
    }
    // Tell Tapestry about our custom translators and their message file.
    // We do this by contributing configuration to Tapestry's TranslatorAlternatesSource service, FieldValidatorSource
    // service, and ComponentMessagesSource service.
    @SuppressWarnings("rawtypes")
    public static void contributeTranslatorAlternatesSource(MappedConfiguration<String, Translator> configuration,
            ThreadLocale threadLocale) {
        configuration.add("yesno", new YesNoTranslator("yesno"));
        configuration.add("money2", new MoneyTranslator("money2", 2, threadLocale));
    }
    public void contributeComponentMessagesSource(OrderedConfiguration<String> configuration) {
        configuration.add("myTranslationMessages", "jumpstart/web/translators/TranslationMessages");
    }
    // Tell Tapestry about the client-side (javascript) validators that corresponds to each server-side Bean Validator.
    public static void contributeClientConstraintDescriptorSource(final JavaScriptSupport javaScriptSupport,
            final Configuration<ClientConstraintDescriptor> configuration) {
        configuration.add(new BaseCCD(Letters.class) {
            public void applyClientValidation(MarkupWriter writer, String message, Map<String, Object> attributes) {
                javaScriptSupport.require("beanvalidation/letters");
                writer.attributes(DataConstants.VALIDATION_ATTRIBUTE, true, "data-validate-letters", true,
                        "data-letters-message", message);
            }
        });
    }
    // Tell Tapestry about our custom ValueEncoders.
    // We do this by contributing configuration to Tapestry's ValueEncoderSource service.
    // @SuppressWarnings("rawtypes")
    // public static void contributeValueEncoderSource(MappedConfiguration<Class, Object> configuration) {
    // configuration.addInstance(Person.class, PersonEncoder.class);
    // }
    // Tell Tapestry which locales we support, and tell Tapestry to use its jQuery implementation for its JavaScript.
    // We do this by contributing configuration to Tapestry's ApplicationDefaults service.
    public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en_US,en_GB,fr");
        configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
    }
    // Tell Tapestry how to detect and protect pages that require security.
    // We do this by contributing a custom ComponentRequestFilter to Tapestry's ComponentRequestHandler service.
    // - ComponentRequestHandler is shown in
    // http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview
    // - Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html
    public void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration) {
        configuration.addInstance("PageProtectionFilter", PageProtectionFilter.class);
    }
    // Tell Tapestry how to handle WildFly 11's classpath URLs - WildFly uses a "virtual file system".
    // We do this by overriding Tapestry's ClasspathURLConverter service.
    // See "Running Tapestry on JBoss" (sic) in http://wiki.apache.org/tapestry/Tapestry5HowTos .
    @SuppressWarnings("rawtypes")
    public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration) {
        // configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterJBoss7());
        configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterWildFly11());
    }
    // Tell Tapestry how to handle @EJB in page and component classes.
    // We do this by contributing configuration to Tapestry's ComponentClassTransformWorker service.
    // - Based on http://wiki.apache.org/tapestry/JEE-Annotation.
    @Primary
    public static void contributeComponentClassTransformWorker(
            OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
        configuration.addInstance("EJB", EJBAnnotationWorker.class, "before:Property");
    }
    // Tell Tapestry how to handle pages annotated with @WhitelistAccessOnly, eg. Tapestry's ServiceStatus and
    // PageCatalog.
    // The default WhitelistAnalyzer allows localhost only and only in non-production mode.
    // Our aim is to make the servicestatus page available to ALL clients when not in production mode.
    // We do this by contributing our own WhitelistAnalyzer to Tapestry's ClientWhitelist service.
    public static void contributeClientWhitelist(OrderedConfiguration<WhitelistAnalyzer> configuration) {
        if (!productionMode) {
            configuration.add("NonProductionWhitelistAnalyzer", new WhitelistAnalyzer() {
                @Override
                public boolean isRequestOnWhitelist(Request request) {
                    if (request.getPath().startsWith("/core/servicestatus")) {
                        return true;
                    }
                    else {
                        // This is copied from org.apache.tapestry5.internal.services.security.LocalhostOnly
                        String remoteHost = request.getRemoteHost();
                        return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1")
                                || remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
                    }
                }
            }, "before:*");
        }
    }
    // Tell Tapestry how to build our Filer service (used in the FileUpload example).
    // Annotate it with EagerLoad to force resolution of symbols at startup rather than when it is first used.
    @EagerLoad
    public static IFiler buildFiler(Logger logger, @Inject @Symbol(UPLOADS_PATH) final String uploadsPath,
            @Inject @Symbol(UploadSymbols.FILESIZE_MAX) final long fileSizeMax) {
        return new Filer(logger, UPLOADS_PATH, uploadsPath, UploadSymbols.FILESIZE_MAX, fileSizeMax);
    }
    // Tell Tapestry how to coerce Joda Time types to and from Java Date types for the TypeCoercers example.
    // We do this by contributing configuration to Tapestry's TypeCoercer service.
    // - Based on http://tapestry.apache.org/typecoercer-service.html
    @SuppressWarnings("rawtypes")
    public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {
        // From java.util.Date to DateMidnight
        Coercion<java.util.Date, DateMidnight> toDateMidnight = new Coercion<java.util.Date, DateMidnight>() {
            public DateMidnight coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toDateMidnight(input);
            }
        };
        configuration.add(new CoercionTuple<>(java.util.Date.class, DateMidnight.class, toDateMidnight));
        // From DateMidnight to java.util.Date
        Coercion<DateMidnight, java.util.Date> fromDateMidnight = new Coercion<DateMidnight, java.util.Date>() {
            public java.util.Date coerce(DateMidnight input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };
        configuration.add(new CoercionTuple<>(DateMidnight.class, java.util.Date.class, fromDateMidnight));
        // From java.util.Date to LocalDate
        Coercion<java.util.Date, LocalDate> toLocalDate = new Coercion<java.util.Date, LocalDate>() {
            public LocalDate coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toLocalDate(input);
            }
        };
        configuration.add(new CoercionTuple<>(java.util.Date.class, LocalDate.class, toLocalDate));
        // From LocalDate to java.util.Date
        Coercion<LocalDate, java.util.Date> fromLocalDate = new Coercion<LocalDate, java.util.Date>() {
            public java.util.Date coerce(LocalDate input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };
        configuration.add(new CoercionTuple<>(LocalDate.class, java.util.Date.class, fromLocalDate));
    }
    // Tell Tapestry how its BeanDisplay and BeanEditor can handle the JodaTime types.
    // We do this by contributing configuration to Tapestry's DefaultDataTypeAnalyzer and BeanBlockSource services.
    // - Based on http://tapestry.apache.org/beaneditform-guide.html .
    public static void contributeDefaultDataTypeAnalyzer(
            @SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
        configuration.add(DateTime.class, "dateTime");
        configuration.add(DateMidnight.class, "dateMidnight");
        configuration.add(LocalDateTime.class, "localDateTime");
        configuration.add(LocalDate.class, "localDate");
        configuration.add(LocalTime.class, "localTime");
    }
    public static void contributeBeanBlockSource(Configuration<BeanBlockContribution> configuration) {
        configuration.add(new DisplayBlockContribution("dateTime", "infra/AppPropertyDisplayBlocks", "dateTime"));
        configuration
                .add(new DisplayBlockContribution("dateMidnight", "infra/AppPropertyDisplayBlocks", "dateMidnight"));
        configuration
                .add(new DisplayBlockContribution("localDateTime", "infra/AppPropertyDisplayBlocks", "localDateTime"));
        configuration.add(new DisplayBlockContribution("localDate", "infra/AppPropertyDisplayBlocks", "localDate"));
        configuration.add(new DisplayBlockContribution("localTime", "infra/AppPropertyDisplayBlocks", "localTime"));
        configuration.add(new EditBlockContribution("dateMidnight", "infra/AppPropertyEditBlocks", "dateMidnight"));
        configuration.add(new EditBlockContribution("localDate", "infra/AppPropertyEditBlocks", "localDate"));
    }
}