* Router Console translation infrastructure:

- Persistent lang setting with routerconsole.lang=xx
      - Loading any page with ?lang=xx changes the persistent setting
      - Add a custom Jetty handler to load foo_xx.jsp if it
        exists for language xx. This is for jsp files with lots
        of text in them. Otherwise use inline translate methods.
        Not for included jsps.
      - Add a script to create and update messages_xx.po translation
        files, and create ResourceBundles from them
      - Add class to translate strings from cached ResourceBundles
      - Add translate wrappers to HelperBase, FormHandler, and *Renderer,
        so calls can be made from both jsp and java files
      - Add two example translations on configupdate.jsp - one in
        the jsp itself and one in the helper.
      - This is for strings in routerconsole only. Will be expanded
        to other webapps and the router later.
This commit is contained in:
zzz
2009-10-18 14:06:07 +00:00
parent 10b84418c3
commit 4497463778
19 changed files with 311 additions and 6 deletions

View File

@ -73,6 +73,15 @@
<delete dir="./tmpextract" /> <delete dir="./tmpextract" />
<ant target="war" /> <ant target="war" />
<!-- Update the messages_*.po files.
We need to supply something for windows, and then change the fail property to true,
or else not run this on windows -->
<exec executable="sh" failifexecutionfails="false" >
<arg value="./bundle-messages.sh" />
</exec>
<!-- jar again to get the latest messages_*.class files -->
<jar destfile="./build/routerconsole.jar" basedir="./build/obj" includes="**/*.class" update="true" />
</target> </target>
<target name="war" depends="precompilejsp"> <target name="war" depends="precompilejsp">
<!-- Don't include the css in the war, the main build.xml will copy it to docs/themes/console/ --> <!-- Don't include the css in the war, the main build.xml will copy it to docs/themes/console/ -->
@ -115,6 +124,7 @@
</java> </java>
<javac debug="true" deprecation="on" source="1.5" target="1.5" <javac debug="true" deprecation="on" source="1.5" target="1.5"
encoding="UTF-8"
destdir="../jsp/WEB-INF/classes/" destdir="../jsp/WEB-INF/classes/"
srcdir="../jsp/WEB-INF/classes" includes="**/*.java"> srcdir="../jsp/WEB-INF/classes" includes="**/*.java">
<compilerarg line="${javac.compilerargs}" /> <compilerarg line="${javac.compilerargs}" />
@ -133,10 +143,13 @@
<pathelement location="../../../core/java/build/i2p.jar" /> <pathelement location="../../../core/java/build/i2p.jar" />
</classpath> </classpath>
</javac> </javac>
<!-- save these so we can run gettext on the generated java files later
<delete> <delete>
<fileset dir="../jsp/WEB-INF/" includes="**/*.java" /> <fileset dir="../jsp/WEB-INF/" includes="**/*.java" />
<fileset dir="../jsp/WEB-INF/" includes="**/*.jsp" /> <fileset dir="../jsp/WEB-INF/" includes="**/*.jsp" />
</delete> </delete>
-->
<copy file="../jsp/web.xml" tofile="../jsp/web-out.xml" /> <copy file="../jsp/web.xml" tofile="../jsp/web-out.xml" />
<loadfile property="jspc.web.fragment" srcfile="../jsp/web-fragment.xml" /> <loadfile property="jspc.web.fragment" srcfile="../jsp/web-fragment.xml" />
<replace file="../jsp/web-out.xml"> <replace file="../jsp/web-out.xml">

View File

@ -0,0 +1,51 @@
#
# Update messages_xx.po and messages_xx.class files,
# from both java and jsp sources.
# Requires installed programs xgettext, msgfmt, and find.
# zzz - public domain
#
CLASS=net.i2p.router.web.messages
TMPFILE=build/javafiles.txt
for i in ../locale/messages_*.po
do
# get language
LG=${i#../locale/messages_}
LG=${LG%.po}
# make list of java files newer than the .po file
find src ../jsp/WEB-INF -name *.java -newer $i > $TMPFILE
if [ -s build/obj/net/i2p/router/web/messages_$LG.class -a ! -s $TMPFILE ]
then
continue
fi
echo "Generating ${CLASS}_$LG ResourceBundle..."
# extract strings from java and jsp files, and update messages.po files
# translate calls must be one of the forms:
# _("foo")
# cssHelper._("foo")
# handler._("foo")
# formhandler._("foo")
# In a jsp, you must use a helper or handler that has the context set.
# To start a new translation, copy the header from an old translation to the new .po file,
# then ant distclean updater.
xgettext -f build/javafiles.txt -F -L java --keyword=_ --keyword=cssHelper._ --keyword=handler._ --keyword=formhandler._ -o $i -j
if [ $? -ne 0 ]
then
echo 'Warning - xgettext failed, not updating translations'
break
fi
# convert to class files in build/obj
msgfmt --java -r $CLASS -l $LG -d build/obj $i
if [ $? -ne 0 ]
then
echo 'Warning - xgettext failed, not updating translations'
break
fi
done
rm -f $TMPFILE
# todo: return failure
exit 0

View File

@ -22,4 +22,10 @@ public class CSSHelper extends HelperBase {
} }
return url; return url;
} }
/** change default language for the router but don't save it */
public void setLang(String lang) {
if (lang != null && lang.length() > 0)
_context.router().setConfigSetting(Messages.PROP_LANG, lang);
}
} }

View File

@ -87,7 +87,7 @@ public class ConfigUpdateHelper extends HelperBase {
buf.append("<select name=\"updatePolicy\">"); buf.append("<select name=\"updatePolicy\">");
if ("notify".equals(policy)) if ("notify".equals(policy))
buf.append("<option value=\"notify\" selected=\"true\">Notify only</option>"); buf.append("<option value=\"notify\" selected=\"true\">").append(_("Notify only")).append("</option>");
else else
buf.append("<option value=\"notify\">Notify only</option>"); buf.append("<option value=\"notify\">Notify only</option>");

View File

@ -62,7 +62,8 @@ public class ContentHelper extends HelperBase {
/** /**
* Convert file.ext to file_lang.ext if it exists. * Convert file.ext to file_lang.ext if it exists.
* Get lang from either the cgi lang param or from the default locale. * Get lang from the cgi lang param, then properties, then from the default locale.
* _context must be set to check the property.
*/ */
private String filename() { private String filename() {
int lastdot = _page.lastIndexOf('.'); int lastdot = _page.lastIndexOf('.');
@ -70,9 +71,13 @@ public class ContentHelper extends HelperBase {
return _page; return _page;
String lang = _lang; String lang = _lang;
if (lang == null || lang.length() <= 0) { if (lang == null || lang.length() <= 0) {
lang = Locale.getDefault().getLanguage(); if (_context != null)
if (lang == null || lang.length() <= 0) lang = _context.getProperty(Messages.PROP_LANG);
return _page; if (lang == null || lang.length() <= 0) {
lang = Locale.getDefault().getLanguage();
if (lang == null || lang.length() <= 0)
return _page;
}
} }
if (lang.equals("en")) if (lang.equals("en"))
return _page; return _page;

View File

@ -190,4 +190,8 @@ public class FormHandler {
} }
} }
/** translate a string */
public String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -29,4 +29,9 @@ public abstract class HelperBase {
//public RouterContext getContext() { return _context; } //public RouterContext getContext() { return _context; }
public void setWriter(Writer out) { _out = out; } public void setWriter(Writer out) { _out = out; }
/** translate a string */
public String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -0,0 +1,69 @@
package net.i2p.router.web;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import net.i2p.I2PAppContext;
import org.mortbay.http.HttpRequest;
import org.mortbay.http.HttpResponse;
import org.mortbay.jetty.servlet.WebApplicationHandler;
/**
* Convert foo.jsp to foo_xx.jsp for language xx.
* This is appropriate for jsps with large amounts of text.
* This does not work for included jsps (e.g. summary*)
*
* @author zzz
*/
public class LocaleWebAppHandler extends WebApplicationHandler
{
private I2PAppContext _context;
public LocaleWebAppHandler(I2PAppContext ctx) {
super();
_context = ctx;
}
/**
* Handle foo.jsp by converting to foo_xx.jsp
* for language xx, where xx is the language for the default locale,
* or as specified in the routerconsole.lang property.
* Unless language==="en".
*/
public void handle(String pathInContext,
String pathParams,
HttpRequest httpRequest,
HttpResponse httpResponse)
throws IOException
{
//System.err.println("Path: " + pathInContext);
String newPath = pathInContext;
if (pathInContext.endsWith(".jsp")) {
int len = pathInContext.length();
// ...but leave foo_xx.jsp alone
if (len < 8 || pathInContext.charAt(len - 7) != '_') {
String lang = _context.getProperty(Messages.PROP_LANG);
if (lang == null || lang.length() <= 0)
lang = Locale.getDefault().getLanguage();
if (lang != null && lang.length() > 0 && !lang.equals("en")) {
String testPath = pathInContext.substring(0, len - 4) + '_' + lang + ".jsp";
// Do we have a servlet for the new path that isn't the catchall *.jsp?
Map.Entry servlet = getHolderEntry(testPath);
if (servlet != null) {
String servletPath = (String) servlet.getKey();
if (servletPath != null && !servletPath.startsWith("*")) {
// success!!
//System.err.println("Servlet is: " + servletPath);
newPath = testPath;
}
}
}
}
}
//System.err.println("New path: " + newPath);
super.handle(newPath, pathParams, httpRequest, httpResponse);
//System.err.println("Was handled? " + httpRequest.isHandled());
}
}

View File

@ -0,0 +1,78 @@
package net.i2p.router.web;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.I2PAppContext;
import net.i2p.util.ConcurrentHashSet;
/**
* Translate strings efficiently.
* We don't include an English or default ResourceBundle, we simply check
* for "en" and return the original string.
* Support real-time language changing with the routerconsole.lang property.
*
* @author zzz, from a base generated by eclipse.
*/
public class Messages {
public static final String PROP_LANG = "routerconsole.lang";
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
private static final String _localeLang = Locale.getDefault().getLanguage();
private static final Map<String, ResourceBundle> _bundles = new ConcurrentHashMap(2);
private static final Set<String> _missing = new ConcurrentHashSet(2);
/** current locale **/
public static String getString(String key) {
if (_localeLang.equals("en"))
return key;
ResourceBundle bundle = findBundle(_localeLang);
if (bundle == null)
return key;
try {
return bundle.getString(key);
} catch (MissingResourceException e) {
return key;
}
}
/** lang in routerconsole.lang property, else current locale */
public static String getString(String key, I2PAppContext ctx) {
String lang = getLanguage(ctx);
if (lang.equals("en"))
return key;
ResourceBundle bundle = findBundle(lang);
if (bundle == null)
return key;
try {
return bundle.getString(key);
} catch (MissingResourceException e) {
return key;
}
}
private static String getLanguage(I2PAppContext ctx) {
String lang = ctx.getProperty(PROP_LANG);
if (lang == null || lang.length() <= 0)
lang = _localeLang;
return lang;
}
/** cache both found and not found for speed */
private static ResourceBundle findBundle(String lang) {
ResourceBundle rv = _bundles.get(lang);
if (rv == null && !_missing.contains(lang)) {
try {
// Would it be faster to specify a class loader?
// No matter we only do this once per lang.
rv = ResourceBundle.getBundle(BUNDLE_NAME, new Locale(lang));
} catch (MissingResourceException e) {
_missing.add(lang);
}
}
return rv;
}
}

View File

@ -268,4 +268,8 @@ public class NetDbRenderer {
buf.append("</td></tr>\n"); buf.append("</td></tr>\n");
} }
/** translate a string */
private String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -319,4 +319,9 @@ class ProfileOrganizerRenderer {
long c = r.getCurrentEventCount() + r.getLastEventCount(); long c = r.getCurrentEventCount() + r.getLastEventCount();
return "" + c; return "" + c;
} }
/** translate a string */
private String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -111,6 +111,7 @@ public class RouterConsoleRunner {
File tmpdir = new File(workDir, ROUTERCONSOLE + "-" + _listenPort); File tmpdir = new File(workDir, ROUTERCONSOLE + "-" + _listenPort);
tmpdir.mkdir(); tmpdir.mkdir();
wac.setTempDirectory(tmpdir); wac.setTempDirectory(tmpdir);
wac.addHandler(0, new LocaleWebAppHandler(I2PAppContext.getGlobalContext()));
initialize(wac); initialize(wac);
File dir = new File(_webAppsDir); File dir = new File(_webAppsDir);
String fileNames[] = dir.list(WarFilenameFilter.instance()); String fileNames[] = dir.list(WarFilenameFilter.instance());

View File

@ -235,4 +235,9 @@ public class StatsGenerator {
private final static DecimalFormat _pct = new DecimalFormat("#0.00%"); private final static DecimalFormat _pct = new DecimalFormat("#0.00%");
private final static String pct(double num) { synchronized (_pct) { return _pct.format(num); } } private final static String pct(double num) { synchronized (_pct) { return _pct.format(num); } }
/** translate a string */
private String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -310,4 +310,9 @@ public class TunnelRenderer {
private String netDbLink(Hash peer) { private String netDbLink(Hash peer) {
return _context.commSystem().renderPeerHTML(peer); return _context.commSystem().renderPeerHTML(peer);
} }
/** translate a string */
private String _(String s) {
return Messages.getString(s, _context);
}
} }

View File

@ -36,7 +36,7 @@
<td><input type="text" size="60" name="newsURL" value="<jsp:getProperty name="updatehelper" property="newsURL" />"></td> <td><input type="text" size="60" name="newsURL" value="<jsp:getProperty name="updatehelper" property="newsURL" />"></td>
</tr><tr><td class= "mediumtags" align="right"><b>Refresh frequency:</b> </tr><tr><td class= "mediumtags" align="right"><b>Refresh frequency:</b>
<td><jsp:getProperty name="updatehelper" property="refreshFrequencySelectBox" /></td><tr> <td><jsp:getProperty name="updatehelper" property="refreshFrequencySelectBox" /></td><tr>
<td class= "mediumtags" align="right"><b>Update policy:</b></td> <td class= "mediumtags" align="right"><b><%=formhandler._("Update policy")%>:</b></td>
<td><jsp:getProperty name="updatehelper" property="updatePolicySelectBox" /></td> <td><jsp:getProperty name="updatehelper" property="updatePolicySelectBox" /></td>
<tr><td class= "mediumtags" align="right"><b>Update through the eepProxy?</b></td> <tr><td class= "mediumtags" align="right"><b>Update through the eepProxy?</b></td>
<td><jsp:getProperty name="updatehelper" property="updateThroughProxy" /></td> <td><jsp:getProperty name="updatehelper" property="updateThroughProxy" /></td>

View File

@ -25,5 +25,8 @@
<link rel="shortcut icon" href="/themes/console/images/favicon.ico"> <link rel="shortcut icon" href="/themes/console/images/favicon.ico">
<jsp:useBean class="net.i2p.router.web.CSSHelper" id="cssHelper" scope="request" /> <jsp:useBean class="net.i2p.router.web.CSSHelper" id="cssHelper" scope="request" />
<jsp:setProperty name="cssHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" /> <jsp:setProperty name="cssHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
<%
cssHelper.setLang(request.getParameter("lang"));
%>
<link href="<%=cssHelper.getTheme(request.getHeader("User-Agent"))%>console.css" rel="stylesheet" type="text/css"> <link href="<%=cssHelper.getTheme(request.getHeader("User-Agent"))%>console.css" rel="stylesheet" type="text/css">
<!--[if IE]><link href="/themes/console/classic/ieshim.css" rel="stylesheet" type="text/css" /><![endif]--> <!--[if IE]><link href="/themes/console/classic/ieshim.css" rel="stylesheet" type="text/css" /><![endif]-->

View File

@ -29,5 +29,6 @@ if (System.getProperty("router.consoleNonce") == null) {
<jsp:setProperty name="contenthelper" property="page" value="<%=fpath.getAbsolutePath()%>" /> <jsp:setProperty name="contenthelper" property="page" value="<%=fpath.getAbsolutePath()%>" />
<jsp:setProperty name="contenthelper" property="maxLines" value="300" /> <jsp:setProperty name="contenthelper" property="maxLines" value="300" />
<jsp:setProperty name="contenthelper" property="lang" value="<%=request.getParameter("lang")%>" /> <jsp:setProperty name="contenthelper" property="lang" value="<%=request.getParameter("lang")%>" />
<jsp:setProperty name="contenthelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
<jsp:getProperty name="contenthelper" property="content" /> <jsp:getProperty name="contenthelper" property="content" />
</div></body></html> </div></body></html>

View File

@ -0,0 +1,25 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-10-18 10:03-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../jsp/WEB-INF/classes/net/i2p/router/web/jsp/configupdate_jsp.java:441
msgid "Update policy"
msgstr "Update policy in german foobarbaz"
#: src/net/i2p/router/web/ConfigUpdateHelper.java:90
msgid "Notify only"
msgstr "Notify only in german"

View File

@ -0,0 +1,25 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-10-18 10:03-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../jsp/WEB-INF/classes/net/i2p/router/web/jsp/configupdate_jsp.java:441
msgid "Update policy"
msgstr ""
#: src/net/i2p/router/web/ConfigUpdateHelper.java:90
msgid "Notify only"
msgstr ""