001 package hirondelle.web4j.ui.translate;
002
003 import java.util.Locale;
004 import java.util.logging.Logger;
005 import java.util.regex.*;
006
007 import hirondelle.web4j.BuildImpl;
008 import hirondelle.web4j.request.LocaleSource;
009 import hirondelle.web4j.ui.tag.TagHelper;
010 import hirondelle.web4j.util.Util;
011 import hirondelle.web4j.util.Regex;
012 import hirondelle.web4j.util.EscapeChars;
013
014 /**
015 Custom tag for translating
016 <a href="http://www.w3.org/TR/html4/struct/global.html#adef-title"><tt>TITLE</tt></a>,
017 <a href="http://www.w3.org/TR/html4/struct/objects.html#adef-alt"><tt>ALT</tt></a>
018 and submit-button
019 <a href="http://www.w3.org/TR/html4/interact/forms.html#adef-value-INPUT"><tt>VALUE</tt></a>
020 attributes in markup.
021
022 <P><span class="highlight">By using this custom tag <em>once</em> in a template JSP,
023 it is often possible to translate <em>all</em> of the <tt>TITLE</tt>,
024 <tt>ALT</tt>, and submit-button <tt>VALUE</tt> attributes appearing in an entire application.</span>
025
026 <P>The <tt>VALUE</tt> attribute is translated only for <tt>SUBMIT</tt> controls. (This
027 <tt>VALUE</tt> attribute isn't really a tooltip, of course : it is rendered as the button text.
028 For this custom tag, the distinction is not very important.)
029
030 <P>The <tt>TITLE</tt> attribute applies to a large number of HTML tags, and
031 the <tt>ALT</tt> attribute applies to several tags. In both cases, these
032 items generate pop-up "tool tips", which are visible to the end user. If the application is
033 multilingual, then they require translation.
034
035 <P>This custom tag accepts HTML markup for its body, and will do a search and
036 replace on its content, replacing the values of all <tt>TITLE</tt>, <tt>ALT</tt> and
037 submit-button <tt>VALUE</tt> attributes (that have visible content) with translated values.
038 The translations are provided by the configured implementations of
039 {@link Translator} and {@link LocaleSource}.
040 */
041 public final class Tooltips extends TagHelper {
042
043 /**
044 Control the escaping of special characters.
045
046 <P>By default, this tag will escape any special characters appearing in the
047 <tt>TITLE</tt> or <tt>ALT</tt> attribute, using {@link EscapeChars#forHTML(String)}.
048 To override this default behaviour, set this value to <tt>false</tt>.
049 */
050 public void setEscapeChars(boolean aValue){
051 fEscapeChars = aValue;
052 }
053
054 /**
055 Scan the body of this tag, and translate the values of all <tt>TITLE</tt>, <tt>ALT</tt>,
056 and submit-button <tt>VALUE</tt> attributes.
057
058 <P>Uses the configured {@link Translator} and {@link LocaleSource}.
059
060 <P>In addition, this method uses {@link EscapeChars#forHTML(String)} to ensure that
061 any special characters are escaped. This behavior can be overridden using {@link #setEscapeChars(boolean)}.
062 */
063 @Override protected String getEmittedText(String aOriginalBody) {
064 //fLogger.finest("Original Body: " + aOriginalBody);
065 String result = translateTooltips(aOriginalBody);
066 result = translateSubmitButtons(result);
067 //fLogger.finest("TranslateTooltips translated : " + result.toString());
068 return result;
069 }
070
071 /**
072 Pattern which returns the value of <tt>TITLE</tt> and <tt>ALT</tt> attributes (including any quotes),
073 as group 2, which is to be translated.
074 */
075 static final Pattern TOOLTIP = Pattern.compile(
076 "(<[^>]* (?:title=|alt=))" + Regex.ATTR_VALUE + "([^>]*>)",
077 Pattern.CASE_INSENSITIVE
078 );
079
080 /**
081 Pattern which returns the value of <tt>VALUE</tt> attribute (including any quotes) of a <tt>SUBMIT</tt>
082 control, as group 2, which is to be translated.
083
084 <P>Small nuisance restriction : the general order of items must follow this style, where <tt>type</tt>
085 and <tt>value</tt> precede all other attributes, and <tt>type</tt> precedes <tt>value</tt>:
086 <PRE>
087 <input type="submit" value="Add" [any other attributes go here]>
088 </PRE>
089 */
090 static final Pattern SUBMIT_BUTTON = Pattern.compile(
091 "(<input(?:\\s)* (?:type=\"submit\"|type='submit'|type=submit)(?:\\s)* value=)" + Regex.ATTR_VALUE + "([^>]*>)",
092 Pattern.CASE_INSENSITIVE
093 );
094
095 // PRIVATE //
096
097 private static final Logger fLogger = Util.getLogger(Tooltips.class);
098 private boolean fEscapeChars = true;
099 private LocaleSource fLocaleSource = BuildImpl.forLocaleSource();
100 private Translator fTranslator = BuildImpl.forTranslator();
101
102 private String translateTooltips(String aInput){
103 return scanAndReplace(TOOLTIP, aInput);
104 }
105
106 private String translateSubmitButtons(String aInput){
107 return scanAndReplace(SUBMIT_BUTTON, aInput);
108 }
109
110 private String scanAndReplace(Pattern aPattern, String aInput){
111 StringBuffer result = new StringBuffer();
112 Matcher matcher = aPattern.matcher(aInput);
113 while ( matcher.find() ) {
114 matcher.appendReplacement(result, getReplacement(matcher));
115 }
116 matcher.appendTail(result);
117 return result.toString();
118 }
119
120 private String getReplacement(Matcher aMatcher){
121 String result = null;
122 String baseText = Util.removeQuotes(aMatcher.group(Regex.SECOND_GROUP));
123 if (Util.textHasContent(baseText)){
124 String start = aMatcher.group(Regex.FIRST_GROUP);
125 String end = aMatcher.group(Regex.THIRD_GROUP);
126 Locale locale = fLocaleSource.get(getRequest());
127 String translatedText = fTranslator.get(baseText, locale);
128 if ( fEscapeChars ){
129 translatedText = EscapeChars.forHTML(translatedText);
130 }
131 result = start + Util.quote(translatedText) + end;
132 }
133 else {
134 result = aMatcher.group(Regex.ENTIRE_MATCH);
135 }
136 result = EscapeChars.forReplacementString(result);
137 fLogger.finest("TITLE/ALT base Text: " + Util.quote(baseText) + " has replacement text : " + Util.quote(result));
138 return result;
139 }
140 }