package hirondelle.web4j.ui.translate;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Locale;
import java.util.regex.*;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.request.LocaleSource;
import hirondelle.web4j.ui.tag.TagHelper;
import hirondelle.web4j.util.EscapeChars;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Regex;
/**
Custom tag for translating regular text flow in large sections of a web page.
<P><span class="highlight">This tag treats every piece of free flow text delimited by a
tag as a unit of translatable base text</span>, and passes it to {@link Translator}.
That is, all tags are treated as <em>delimiters</em> of units of translatable text.
<P>This tag is suitable for translating most, but not all, of the
regular text flow in a web page. <span class="highlight">It is suitable for translating
markup that contains short, isolated snippets of text, that have no "structure", and
no dynamic data</span>, such as the labels in a form, the column headers in a listing,
and so on. (For many intranet applications, this
makes up most of the free flow text appearing in the application.) Instead of using many
separate <tt><w:txt></tt> {@link Text} tags to translate each item one by one,
a single <tt><w:txtFlow></tt> tag can often be used to do the same thing in a single step.
<P><span class="highlight">Using this class has two strong advantages</span> :
<ul>
<li>the effort needed to internationalize a page is greatly reduced
<li>the markup will be significantly easier to read and maintain, since most of the free flow text
remains unchanged from the single-language case
</ul>
<P>
This tag is <em>not suitable</em> when the base text to be translated :
<ul>
<li>contains markup
<li>has dynamic data of any sort
<li>contains a <tt>TEXTAREA</tt> with a <em>non-empty</em> body. Such text will be seen as a translatable
unit, which is usually undesired, since such text is usually not fixed, but dynamic (that is, from the database).
(To avoid this, simply nest this tag <em>inside</em> the <tt><w:populate></tt> tag surrounding the
form that contains the <tt>TEXTAREA</tt>. This ensures that the population is not affected by the action of this
tag.)
</ul>
<P>For example, given this text containing markup :
<PRE>The <EM>raison-d'etre</EM> for this...</PRE>
then this tag will split the text into three separate pieces, delimited by the <tt>EM</tt> tags.
Then, each piece will be translated. For such items, this is almost always undesirable. Instead,
one must use a <tt><w:txt></tt> {@link Text} tag, which can treat such items as
a single unit of translatable text, without chopping it up into three pieces.
<P><b>Example</b><br>
Here, all of the <tt>LABEL</tt> tags in this form will have their content translated by the
<tt><w:txtFlow></tt> tag :
<PRE>
<w:populate style='edit' using="myUser">
<w:txtFlow>
<form action='blah.do' method='post' class="user-input">
<table align="center">
<tr>
<td>
<label class="mandatory">Email</label>
</td>
<td>
<input type="text" name="Email Address" size='30'>
</td>
</tr>
<tr>
<td>
<label>Age</label>
</td>
<td>
<input type="text" name="Age" size="30">
</td>
</tr>
<tr>
<td>
<label>Desired Salary</label>
</td>
<td>
<input type="text" name="Desired Salary" size="30">
</td>
</tr>
<tr>
<td>
<label> Birth Date </label>
</td>
<td>
<input type="text" name="Birth Date" size="30">
</td>
</tr>
<tr>
<td>
<input type='submit' value='UPDATE'>
</td>
</tr>
</table>
</form>
</w:txtFlow>
</w:populate>
</PRE>
*/
public final class TextFlow extends TagHelper {
/**
By default, this tag will escape any special characters appearing in the
text flow, using {@link EscapeChars#forHTML(String)}. To change that default
behaviour, set this value to <tt>false</tt>.
<P><span class="highlight">Exercise care that text is not doubly escaped.</span>
For instance, if the text already contains
character entities, and <tt>setEscapeChars</tt> is true, then the text <tt>&amp;</tt>
will be emitted by this tag as <tt>&amp;amp;</tt>, for example.
*/
public void setEscapeChars(boolean aValue){
fEscapeChars = aValue;
}
/**
Translate each piece of free flow text appearing in <tt>aOriginalBody</tt>.
<P>Each piece of text is delimited by one or more tags, and is translated using the configured
{@link Translator}. Leading or trailing white space is preserved.
*/
@Override protected String getEmittedText(String aOriginalBody) {
final StringBuffer result = new StringBuffer();
final StringBuffer snippet = new StringBuffer();
boolean isInsideTag = false;
final StringCharacterIterator iterator = new StringCharacterIterator(aOriginalBody);
char character = iterator.current();
while (character != CharacterIterator.DONE ){
if (character == '<') {
doStartTag(result, snippet, character);
isInsideTag = true;
}
else if (character == '>') {
doEndTag(result, character);
isInsideTag = false;
}
else {
doRegularCharacter(result, snippet, isInsideTag, character);
}
character = iterator.next();
}
if( Util.textHasContent(snippet.toString()) ) {
appendTranslation(snippet, result);
}
return result.toString();
}
// PRIVATE //
static Pattern TRIMMED_TEXT = Pattern.compile("((?:\\S(?:.)*\\S)|(?:\\S))");
private boolean fEscapeChars = true;
private LocaleSource fLocaleSource = BuildImpl.forLocaleSource();
private Translator fTranslator = BuildImpl.forTranslator();
private void doStartTag(StringBuffer aResult, StringBuffer aSnippet, char aCharacter) {
if (Util.textHasContent(aSnippet.toString()) ){
appendTranslation(aSnippet, aResult);
}
else {
//often contains just spaces and/or new lines, which are just appended
aResult.append(aSnippet.toString());
}
aSnippet.setLength(0);
aResult.append(aCharacter);
}
private void doEndTag(StringBuffer aResult, char aCharacter) {
aResult.append(aCharacter);
}
private void doRegularCharacter(StringBuffer aResult, StringBuffer aSnippet, boolean aIsInsideTag, char aCharacter) {
if( aIsInsideTag ){
aResult.append(aCharacter);
}
else {
aSnippet.append(aCharacter);
}
//fLogger.fine("Snippet : " + aSnippet);
}
/**
The snippet may contain leading or trailing white space, or control chars (new lines),
which must be preserved.
*/
private void appendTranslation(StringBuffer aSnippet, StringBuffer aResult){
if( Util.textHasContent(aSnippet.toString()) ) {
StringBuffer translatedSnippet = new StringBuffer();
Matcher matcher = TRIMMED_TEXT.matcher(aSnippet.toString());
while ( matcher.find() ) {
matcher.appendReplacement(translatedSnippet, getReplacement(matcher));
}
matcher.appendTail(translatedSnippet);
if( fEscapeChars ) {
aResult.append(EscapeChars.forHTML(translatedSnippet.toString()));
}
else {
aResult.append(translatedSnippet);
}
}
else {
aResult.append(aSnippet.toString());
}
}
private String getReplacement(Matcher aMatcher){
String result = null;
String baseText = aMatcher.group(Regex.FIRST_GROUP);
if (Util.textHasContent(baseText)){
Locale locale = fLocaleSource.get(getRequest());
result = fTranslator.get(baseText, locale);
}
else {
result = baseText;
}
return EscapeChars.forReplacementString(result);
}
}