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 }