NIHVIVO-4011 Create the I18n framework for multi-language support.
This commit is contained in:
parent
885bbabdae
commit
caf16c392b
8 changed files with 712 additions and 0 deletions
|
@ -19,6 +19,7 @@ import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
|
|||
import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Route;
|
||||
import edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.EditConfigurationConstants;
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.freemarker.I18nMethodModel;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.directives.IndividualShortViewDirective;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualLocalNameMethod;
|
||||
import edu.cornell.mannlib.vitro.webapp.web.methods.IndividualPlaceholderImageUrlMethod;
|
||||
|
@ -166,6 +167,7 @@ public class FreemarkerConfiguration extends Configuration {
|
|||
map.put("profileUrl", new IndividualProfileUrlMethod());
|
||||
map.put("localName", new IndividualLocalNameMethod());
|
||||
map.put("placeholderImageUrl", new IndividualPlaceholderImageUrlMethod());
|
||||
map.put("i18n", new I18nMethodModel());
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,10 @@ public class TemplateProcessingHelper {
|
|||
env.setCustomAttribute("request", request);
|
||||
env.setCustomAttribute("context", context);
|
||||
|
||||
// Set the Locale from the request into the environment, so date builtins will be
|
||||
// Locale-dependent
|
||||
env.setLocale(request.getLocale());
|
||||
|
||||
// Define a setup template to be included by every page template
|
||||
String templateType = (String) map.get("templateType");
|
||||
if (FreemarkerHttpServlet.PAGE_TEMPLATE_TYPE.equals(templateType)) {
|
||||
|
|
228
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java
Normal file
228
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java
Normal file
|
@ -0,0 +1,228 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.ResourceBundle.Control;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
|
||||
|
||||
/**
|
||||
* Provides access to a bundle of text strings, based on the name of the bundle,
|
||||
* the Locale of the requesting browser, and the current theme directory.
|
||||
*
|
||||
* If the bundle name is not specified, the default name of "all" is used.
|
||||
*
|
||||
* If a requested bundle is not found, no error is thrown. Instead, an empty
|
||||
* bundle is returned that produces error message strings when asked for text.
|
||||
*/
|
||||
public class I18n {
|
||||
private static final Log log = LogFactory.getLog(I18n.class);
|
||||
|
||||
public static final String DEFAULT_BUNDLE_NAME = "all";
|
||||
private static final String PROPERTY_DEVELOPER_DEFEAT_CACHE = "developer.defeatI18nCache";
|
||||
|
||||
/**
|
||||
* This is where the work gets done. Not declared final, so it can be
|
||||
* modified in unit tests.
|
||||
*/
|
||||
private static I18n instance = new I18n();
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Static methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A convenience method to get a bundle and format the text.
|
||||
*/
|
||||
public static String text(String bundleName, HttpServletRequest req,
|
||||
String key, Object... parameters) {
|
||||
return bundle(bundleName, req).text(key, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to get the default bundle and format the text.
|
||||
*/
|
||||
public static String text(HttpServletRequest req, String key,
|
||||
Object... parameters) {
|
||||
return bundle(req).text(key, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a I18nBundle by this name.
|
||||
*/
|
||||
public static I18nBundle bundle(String bundleName, HttpServletRequest req) {
|
||||
return instance.getBundle(bundleName, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default I18nBundle.
|
||||
*/
|
||||
public static I18nBundle bundle(HttpServletRequest req) {
|
||||
return instance.getBundle(DEFAULT_BUNDLE_NAME, req);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// The instance
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** Holds the current theme directory, as far as we know. */
|
||||
private AtomicReference<String> themeDirectory = new AtomicReference<String>(
|
||||
"");
|
||||
|
||||
/**
|
||||
* Get an I18nBundle by this name. The request provides the preferred
|
||||
* Locale, the theme directory and the development mode flag.
|
||||
*
|
||||
* If the request indicates that the system is in development mode, then the
|
||||
* cache is cleared on each request.
|
||||
*
|
||||
* If the theme directory has changed, the cache is cleared.
|
||||
*/
|
||||
private I18nBundle getBundle(String bundleName, HttpServletRequest req) {
|
||||
log.debug("Getting bundle '" + bundleName + "'");
|
||||
|
||||
try {
|
||||
checkDevelopmentMode(req);
|
||||
checkForChangeInThemeDirectory(req);
|
||||
|
||||
String dir = themeDirectory.get();
|
||||
ServletContext ctx = req.getSession().getServletContext();
|
||||
|
||||
ResourceBundle.Control control = getControl(ctx, dir);
|
||||
ResourceBundle rb = ResourceBundle.getBundle(bundleName,
|
||||
req.getLocale(), control);
|
||||
return new I18nBundle(bundleName, rb);
|
||||
} catch (MissingResourceException e) {
|
||||
log.warn("Didn't find text bundle '" + bundleName + "'");
|
||||
return I18nBundle.emptyBundle(bundleName);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create text bundle '" + bundleName + "'", e);
|
||||
return I18nBundle.emptyBundle(bundleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are in development mode, clear the cache on each request.
|
||||
*/
|
||||
private void checkDevelopmentMode(HttpServletRequest req) {
|
||||
ConfigurationProperties bean = ConfigurationProperties.getBean(req);
|
||||
|
||||
String flag = bean
|
||||
.getProperty(PROPERTY_DEVELOPER_DEFEAT_CACHE, "false");
|
||||
if (Boolean.valueOf(flag.trim())) {
|
||||
log.debug("In development mode - clearing the cache.");
|
||||
ResourceBundle.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the theme directory has changed from before, clear the cache of all
|
||||
* ResourceBundles.
|
||||
*/
|
||||
private void checkForChangeInThemeDirectory(HttpServletRequest req) {
|
||||
String currentDir = new VitroRequest(req).getAppBean().getThemeDir();
|
||||
String previousDir = themeDirectory.getAndSet(currentDir);
|
||||
if (!currentDir.equals(previousDir)) {
|
||||
log.debug("Theme directory changed from '" + previousDir + "' to '"
|
||||
+ currentDir + "' - clearing the cache.");
|
||||
ResourceBundle.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method in the unit tests, to return a more testable Control
|
||||
* instance.
|
||||
*/
|
||||
protected Control getControl(ServletContext ctx, String dir) {
|
||||
return new ThemeBasedControl(ctx, dir);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Control classes for instantiating ResourceBundles
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Instead of looking in the classpath, look in the theme directory.
|
||||
*/
|
||||
private static class ThemeBasedControl extends ResourceBundle.Control {
|
||||
private static final String BUNDLE_DIRECTORY = "i18n/";
|
||||
private final ServletContext ctx;
|
||||
private final String themeDirectory;
|
||||
|
||||
public ThemeBasedControl(ServletContext ctx, String themeDirectory) {
|
||||
this.ctx = ctx;
|
||||
this.themeDirectory = themeDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't look for classes to satisfy the request, just property files.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getFormats(String baseName) {
|
||||
return FORMAT_PROPERTIES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't look in the class path, look in the current servlet context, in
|
||||
* the bundle directory under the theme directory.
|
||||
*/
|
||||
@Override
|
||||
public ResourceBundle newBundle(String baseName, Locale locale,
|
||||
String format, ClassLoader loader, boolean reload)
|
||||
throws IllegalAccessException, InstantiationException,
|
||||
IOException {
|
||||
checkArguments(baseName, locale, format);
|
||||
|
||||
log.debug("Creating bundle for '" + baseName + "', " + locale
|
||||
+ ", '" + format + "', " + reload);
|
||||
|
||||
String bundleName = toBundleName(baseName, locale);
|
||||
if (bundleName == null) {
|
||||
throw new NullPointerException("bundleName may not be null.");
|
||||
}
|
||||
|
||||
String themeI18nPath = "/" + themeDirectory + BUNDLE_DIRECTORY;
|
||||
String appI18nPath = "/" + BUNDLE_DIRECTORY;
|
||||
|
||||
log.debug("Paths are '" + themeI18nPath + "' and '" + appI18nPath
|
||||
+ "'");
|
||||
|
||||
return VivoResourceBundle.getBundle(bundleName, ctx, appI18nPath,
|
||||
themeI18nPath, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The documentation for ResourceBundle.Control.newBundle() says I
|
||||
* should throw these exceptions.
|
||||
*/
|
||||
private void checkArguments(String baseName, Locale locale,
|
||||
String format) {
|
||||
if (baseName == null) {
|
||||
throw new NullPointerException("baseName may not be null.");
|
||||
}
|
||||
if (locale == null) {
|
||||
throw new NullPointerException("locale may not be null.");
|
||||
}
|
||||
if (format == null) {
|
||||
throw new NullPointerException("format may not be null.");
|
||||
}
|
||||
if (!FORMAT_DEFAULT.contains(format)) {
|
||||
throw new IllegalArgumentException(
|
||||
"format must be one of these: " + FORMAT_DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java
Normal file
102
webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18nBundle.java
Normal file
|
@ -0,0 +1,102 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* A wrapper for a ResourceBundle that will not throw an exception, no matter
|
||||
* what string you request.
|
||||
*
|
||||
* If the ResourceBundle was not found, or if it doesn't contain the requested
|
||||
* key, an error message string is returned, to help the developer diagnose the
|
||||
* problem.
|
||||
*/
|
||||
public class I18nBundle {
|
||||
private static final Log log = LogFactory.getLog(I18nBundle.class);
|
||||
|
||||
private static final String MESSAGE_BUNDLE_NOT_FOUND = "Text bundle ''{0}'' not found.";
|
||||
private static final String MESSAGE_KEY_NOT_FOUND = "Text bundle ''{0}'' has no text for ''{1}''";
|
||||
|
||||
public static I18nBundle emptyBundle(String bundleName) {
|
||||
return new I18nBundle(bundleName);
|
||||
}
|
||||
|
||||
private final String bundleName;
|
||||
private final ResourceBundle resources;
|
||||
private final String notFoundMessage;
|
||||
|
||||
private I18nBundle(String bundleName) {
|
||||
this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public I18nBundle(String bundleName, ResourceBundle resources) {
|
||||
this(bundleName, resources, MESSAGE_KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
private I18nBundle(String bundleName, ResourceBundle resources, String notFoundMessage) {
|
||||
if (bundleName == null) {
|
||||
throw new IllegalArgumentException("bundleName may not be null");
|
||||
}
|
||||
if (bundleName.isEmpty()) {
|
||||
throw new IllegalArgumentException("bundleName may not be empty");
|
||||
}
|
||||
if (resources == null) {
|
||||
throw new NullPointerException("resources may not be null.");
|
||||
}if (notFoundMessage == null) {
|
||||
throw new NullPointerException("notFoundMessage may not be null.");
|
||||
}
|
||||
this.bundleName = bundleName;
|
||||
this.resources = resources;
|
||||
this.notFoundMessage = notFoundMessage;
|
||||
}
|
||||
|
||||
public String text(String key, Object... parameters) {
|
||||
log.debug("Asking for '" + key + "' from bundle '" + bundleName + "'");
|
||||
|
||||
String textString;
|
||||
if (resources.containsKey(key)) {
|
||||
textString = resources.getString(key);
|
||||
return formatString(textString, parameters);
|
||||
} else {
|
||||
String message = MessageFormat.format(notFoundMessage, bundleName,
|
||||
key);
|
||||
log.warn(message);
|
||||
return "ERROR: " + message;
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatString(String textString, Object... parameters) {
|
||||
if (parameters.length == 0) {
|
||||
return textString;
|
||||
} else {
|
||||
return MessageFormat.format(textString, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A resource bundle that contains no strings.
|
||||
*/
|
||||
public static class EmptyResourceBundle extends ResourceBundle {
|
||||
@Override
|
||||
public Enumeration<String> getKeys() {
|
||||
return Collections.enumeration(Collections.<String> emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handleGetObject(String key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException("key may not be null.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Properties;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Works like a PropertyResourceBundle with two exceptions:
|
||||
*
|
||||
* It looks for the file in both the i18n directory of the theme and in the i18n
|
||||
* directory of the application. Properties found in the theme override those
|
||||
* found in the application.
|
||||
*
|
||||
* It allows a property to take its contents from a file. File paths are
|
||||
* relative to the i18n directory. Again, a file in the theme will override one
|
||||
* in the application.
|
||||
*
|
||||
* If a property has a value (after overriding) of "@@file <filepath>", the
|
||||
* bundle looks for the file relative to the i18n directory of the theme, then
|
||||
* relative to the i18n directory of the application. If the file is not found
|
||||
* in either location, a warning is written to the log and the property will
|
||||
* contain an error message for displayed.
|
||||
*
|
||||
* Note that the filename is not manipulated for Locale, so the author of the
|
||||
* properties files must do it explicitly. For example:
|
||||
*
|
||||
* In all.properties: account_email_html = @@file accountEmail.html
|
||||
*
|
||||
* In all_es.properties: account_email_html = @@file accountEmail_es.html
|
||||
*/
|
||||
public class VivoResourceBundle extends ResourceBundle {
|
||||
private static final Log log = LogFactory.getLog(VivoResourceBundle.class);
|
||||
|
||||
private static final String FILE_FLAG = "@@file ";
|
||||
private static final String MESSAGE_FILE_NOT_FOUND = "File {1} not found for property {0}.";
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Factory method
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
public static VivoResourceBundle getBundle(String bundleName,
|
||||
ServletContext ctx, String appI18nPath, String themeI18nPath,
|
||||
Control control) {
|
||||
try {
|
||||
return new VivoResourceBundle(bundleName, ctx, appI18nPath,
|
||||
themeI18nPath, control);
|
||||
} catch (FileNotFoundException e) {
|
||||
log.debug(e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn(e, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// The instance
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private final String bundleName;
|
||||
private final ServletContext ctx;
|
||||
private final String appI18nPath;
|
||||
private final String themeI18nPath;
|
||||
private final Control control;
|
||||
private final Properties defaults;
|
||||
private final Properties properties;
|
||||
|
||||
private VivoResourceBundle(String bundleName, ServletContext ctx,
|
||||
String appI18nPath, String themeI18nPath, Control control)
|
||||
throws IOException {
|
||||
this.bundleName = bundleName;
|
||||
this.ctx = ctx;
|
||||
this.appI18nPath = appI18nPath;
|
||||
this.themeI18nPath = themeI18nPath;
|
||||
this.control = control;
|
||||
|
||||
this.defaults = new Properties();
|
||||
this.properties = new Properties(this.defaults);
|
||||
|
||||
loadProperties();
|
||||
loadReferencedFiles();
|
||||
}
|
||||
|
||||
private void loadProperties() throws IOException {
|
||||
String resourceName = control.toResourceName(bundleName, "properties");
|
||||
|
||||
String defaultsPath = joinPath(appI18nPath, resourceName);
|
||||
String propertiesPath = joinPath(themeI18nPath, resourceName);
|
||||
File defaultsFile = locateFile(defaultsPath);
|
||||
File propertiesFile = locateFile(propertiesPath);
|
||||
|
||||
if ((defaultsFile == null) && (propertiesFile == null)) {
|
||||
throw new FileNotFoundException("Property file not found at '"
|
||||
+ defaultsPath + "' or '" + propertiesPath + "'");
|
||||
}
|
||||
|
||||
if (defaultsFile != null) {
|
||||
log.debug("Loading bundle '" + bundleName + "' defaults from '"
|
||||
+ defaultsPath + "'");
|
||||
FileInputStream stream = new FileInputStream(defaultsFile);
|
||||
try {
|
||||
this.defaults.load(stream);
|
||||
} finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
if (propertiesFile != null) {
|
||||
log.debug("Loading bundle '" + bundleName + "' overrides from '"
|
||||
+ propertiesPath + "'");
|
||||
FileInputStream stream = new FileInputStream(propertiesFile);
|
||||
try {
|
||||
this.properties.load(stream);
|
||||
} finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadReferencedFiles() throws IOException {
|
||||
for (String key : this.properties.stringPropertyNames()) {
|
||||
String value = this.properties.getProperty(key);
|
||||
if (value.startsWith(FILE_FLAG)) {
|
||||
String filepath = value.substring(FILE_FLAG.length()).trim();
|
||||
loadReferencedFile(key, filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadReferencedFile(String key, String filepath)
|
||||
throws IOException {
|
||||
String appFilePath = joinPath(appI18nPath, filepath);
|
||||
String themeFilePath = joinPath(themeI18nPath, filepath);
|
||||
File appFile = locateFile(appFilePath);
|
||||
File themeFile = locateFile(themeFilePath);
|
||||
|
||||
if (themeFile != null) {
|
||||
this.properties.setProperty(key,
|
||||
FileUtils.readFileToString(themeFile, "UTF-8"));
|
||||
} else if (appFile != null) {
|
||||
this.properties.setProperty(key,
|
||||
FileUtils.readFileToString(appFile, "UTF-8"));
|
||||
} else {
|
||||
String message = MessageFormat.format(MESSAGE_FILE_NOT_FOUND, key,
|
||||
themeFilePath, appFilePath);
|
||||
this.properties.setProperty(key, message);
|
||||
log.warn(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String joinPath(String root, String twig) {
|
||||
if ((root.charAt(root.length() - 1) == File.separatorChar)
|
||||
|| (twig.charAt(0) == File.separatorChar)) {
|
||||
return root + twig;
|
||||
} else {
|
||||
return root + File.separatorChar + twig;
|
||||
}
|
||||
}
|
||||
|
||||
private File locateFile(String path) {
|
||||
String realPath = ctx.getRealPath(path);
|
||||
if (realPath == null) {
|
||||
log.debug("No real path for '" + path + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
File f = new File(realPath);
|
||||
if (!f.isFile()) {
|
||||
log.debug("No file at '" + realPath + "'");
|
||||
return null;
|
||||
}
|
||||
if (!f.canRead()) {
|
||||
log.error("Can't read the file at '" + realPath + "'");
|
||||
return null;
|
||||
}
|
||||
log.debug("Located file '" + path + "' at '" + realPath + "'");
|
||||
return f;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Enumeration<String> getKeys() {
|
||||
return (Enumeration<String>) this.properties.propertyNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handleGetObject(String key) {
|
||||
String value = this.properties.getProperty(key);
|
||||
if (value == null) {
|
||||
log.debug(bundleName + " has no value for '" + key + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n.freemarker;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle;
|
||||
import freemarker.template.TemplateHashModel;
|
||||
import freemarker.template.TemplateModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
||||
/**
|
||||
* For Freemarker, this acts like a bundle of text strings. It is simply a
|
||||
* wrapper around an I18nBundle.
|
||||
*/
|
||||
public class I18nBundleTemplateModel implements TemplateHashModel {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(I18nBundleTemplateModel.class);
|
||||
|
||||
private final String bundleName;
|
||||
private final I18nBundle textBundle;
|
||||
|
||||
public I18nBundleTemplateModel(String bundleName, I18nBundle textBundle) {
|
||||
this.bundleName = bundleName;
|
||||
this.textBundle = textBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemplateModel get(String key) throws TemplateModelException {
|
||||
return new I18nStringTemplateModel(bundleName, key,
|
||||
textBundle.text(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() throws TemplateModelException {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n.freemarker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.I18n;
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle;
|
||||
import freemarker.core.Environment;
|
||||
import freemarker.template.TemplateMethodModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
||||
/**
|
||||
* This Freemarker method will produce a bundle of text strings. It is simply a
|
||||
* wrapper around I18n that produces a wrapped I18nBundle.
|
||||
*
|
||||
* If the bundle name is not provided, the default bundle is assumed.
|
||||
*/
|
||||
public class I18nMethodModel implements TemplateMethodModel {
|
||||
private static final Log log = LogFactory.getLog(I18nMethodModel.class);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public Object exec(List args) throws TemplateModelException {
|
||||
if (args.size() > 1) {
|
||||
throw new TemplateModelException("Too many arguments: "
|
||||
+ "displayText method only requires a bundle name.");
|
||||
}
|
||||
Object arg = args.isEmpty() ? I18n.DEFAULT_BUNDLE_NAME : args.get(0);
|
||||
if (!(arg instanceof String)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Arguments to a TemplateMethodModel are supposed to be Strings!");
|
||||
}
|
||||
|
||||
log.debug("Asking for this bundle: " + arg);
|
||||
String bundleName = (String) arg;
|
||||
|
||||
Environment env = Environment.getCurrentEnvironment();
|
||||
HttpServletRequest request = (HttpServletRequest) env
|
||||
.getCustomAttribute("request");
|
||||
I18nBundle tb = I18n.bundle(bundleName, request);
|
||||
return new I18nBundleTemplateModel(bundleName, tb);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n.freemarker;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import freemarker.template.TemplateMethodModelEx;
|
||||
import freemarker.template.TemplateModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
import freemarker.template.TemplateScalarModel;
|
||||
import freemarker.template.utility.DeepUnwrap;
|
||||
|
||||
/**
|
||||
* A Freemarker representation of a text string. Because it implements
|
||||
* TemplateScalarModel, you can use it as a string value. And because it
|
||||
* implements TemplateMethodModel, you can pass arguments to it for formatting.
|
||||
*
|
||||
* So if the string is "His name is {0}!", then these references could be used:
|
||||
*
|
||||
* ${string} ==> "His name is {0}!"
|
||||
*
|
||||
* ${string("Bozo")} ==> "His name is Bozo!"
|
||||
*
|
||||
* Note that the format of the message is determined by java.text.MessageFormat,
|
||||
* so argument indices start at 0 and you can escape a substring by wrapping it
|
||||
* in apostrophes.
|
||||
*/
|
||||
public class I18nStringTemplateModel implements TemplateMethodModelEx,
|
||||
TemplateScalarModel {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(I18nStringTemplateModel.class);
|
||||
|
||||
private final String bundleName;
|
||||
private final String key;
|
||||
private final String textString;
|
||||
|
||||
public I18nStringTemplateModel(String bundleName, String key,
|
||||
String textString) {
|
||||
this.bundleName = bundleName;
|
||||
this.key = key;
|
||||
this.textString = textString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsString() throws TemplateModelException {
|
||||
return textString;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Override
|
||||
public Object exec(List args) throws TemplateModelException {
|
||||
log.debug("Formatting string '" + key + "' from bundle '" + bundleName
|
||||
+ "' with these arguments: " + args);
|
||||
|
||||
if (args.isEmpty()) {
|
||||
return textString;
|
||||
} else {
|
||||
Object[] unwrappedArgs = new Object[args.size()];
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
unwrappedArgs[i] = DeepUnwrap.unwrap((TemplateModel) args
|
||||
.get(i));
|
||||
}
|
||||
try {
|
||||
return MessageFormat.format(textString, unwrappedArgs);
|
||||
} catch (Exception e) {
|
||||
String message = "Can't format '" + key + "' from bundle '"
|
||||
+ bundleName + "', wrong argument types: " + args
|
||||
+ " for message format'" + textString + "'";
|
||||
log.warn(message);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue