[i18b sprint] 3760 translations loading (#341)
* renamed I18nBundle * added I18nBundle interface * Added translation provider * prototype of TranslationConverter * convert all properties * fixes * added caching * Removed obsolete code * Improved logging * fixed getting already existing label * Fix to get RDF Service for configuration models * fix translation request query * added INTERFACE_I18N_FIRSTTIME_BACKUP model * converter test added * formatting fixes * Translation provider tests added * cleanups, added cache test for translation provider * fix: get theme info from web app dao factory as sparql queries on both content and configuration models not supported
This commit is contained in:
parent
8ddbc8fc00
commit
9e3a3f7451
30 changed files with 1041 additions and 718 deletions
|
@ -41,7 +41,7 @@ public class RevisionInfoBean {
|
|||
new Date(0), Collections.singleton(LevelRevisionInfo.DUMMY_LEVEL));
|
||||
|
||||
/** The bean is attached to the session by this name. */
|
||||
static final String ATTRIBUTE_NAME = RevisionInfoBean.class.getName();
|
||||
public static final String ATTRIBUTE_NAME = RevisionInfoBean.class.getName();
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// static methods
|
||||
|
|
|
@ -2,16 +2,10 @@
|
|||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
@ -39,8 +33,6 @@ import edu.cornell.mannlib.vitro.webapp.utils.developer.Key;
|
|||
public class I18n {
|
||||
private static final Log log = LogFactory.getLog(I18n.class);
|
||||
|
||||
public static final String DEFAULT_BUNDLE_NAME = "all";
|
||||
|
||||
/**
|
||||
* If this attribute is present on the request, then the cache has already
|
||||
* been cleared.
|
||||
|
@ -73,14 +65,6 @@ public class I18n {
|
|||
I18n.instance = new I18n(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -89,25 +73,18 @@ public class I18n {
|
|||
return bundle(req).text(key, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a request I18nBundle by this name.
|
||||
*/
|
||||
public static I18nBundle bundle(String bundleName, HttpServletRequest req) {
|
||||
return instance.getBundle(bundleName, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default request I18nBundle.
|
||||
*/
|
||||
public static I18nBundle bundle(HttpServletRequest req) {
|
||||
return instance.getBundle(DEFAULT_BUNDLE_NAME, req);
|
||||
return instance.getBundle(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default context I18nBundle for preferred locales.
|
||||
*/
|
||||
public static I18nBundle bundle(List<Locale> preferredLocales) {
|
||||
return instance.getBundle(DEFAULT_BUNDLE_NAME, preferredLocales);
|
||||
return instance.getBundle(preferredLocales);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
@ -130,15 +107,11 @@ public class I18n {
|
|||
*
|
||||
* Declared 'protected' so it can be overridden in unit tests.
|
||||
*/
|
||||
protected I18nBundle getBundle(String bundleName, HttpServletRequest req) {
|
||||
log.debug("Getting request bundle '" + bundleName + "'");
|
||||
|
||||
protected I18nBundle getBundle(HttpServletRequest req) {
|
||||
checkDevelopmentMode(req);
|
||||
checkForChangeInThemeDirectory(req);
|
||||
|
||||
Locale locale = req.getLocale();
|
||||
|
||||
return getBundle(bundleName, locale);
|
||||
return new I18nSemanticBundle(Collections.singletonList(locale));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,38 +127,11 @@ public class I18n {
|
|||
*
|
||||
* Declared 'protected' so it can be overridden in unit tests.
|
||||
*/
|
||||
protected I18nBundle getBundle(String bundleName, List<Locale> preferredLocales) {
|
||||
log.debug("Getting context bundle '" + bundleName + "'");
|
||||
|
||||
protected I18nBundle getBundle( List<Locale> preferredLocales) {
|
||||
checkDevelopmentMode();
|
||||
checkForChangeInThemeDirectory(ctx);
|
||||
|
||||
Locale locale = SelectedLocale.getPreferredLocale(ctx, preferredLocales);
|
||||
|
||||
return getBundle(bundleName, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an I18nBundle by this name, context, and locale.
|
||||
*/
|
||||
private I18nBundle getBundle(String bundleName, Locale locale) {
|
||||
I18nLogger i18nLogger = new I18nLogger();
|
||||
try {
|
||||
String dir = themeDirectory.get();
|
||||
ResourceBundle.Control control = new ThemeBasedControl(ctx, dir);
|
||||
ResourceBundle rb = ResourceBundle.getBundle(bundleName,
|
||||
locale, control);
|
||||
|
||||
return new I18nBundle(bundleName, rb, i18nLogger);
|
||||
} catch (MissingResourceException e) {
|
||||
log.warn("Didn't find text bundle '" + bundleName + "'");
|
||||
|
||||
return I18nBundle.emptyBundle(bundleName, i18nLogger);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create text bundle '" + bundleName + "'", e);
|
||||
|
||||
return I18nBundle.emptyBundle(bundleName, i18nLogger);
|
||||
}
|
||||
return new I18nSemanticBundle(Collections.singletonList(locale));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,7 +150,7 @@ public class I18n {
|
|||
private void checkDevelopmentMode() {
|
||||
if (DeveloperSettings.getInstance().getBoolean(Key.I18N_DEFEAT_CACHE)) {
|
||||
log.debug("In development mode - clearing the cache.");
|
||||
ResourceBundle.clearCache();
|
||||
clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,158 +187,24 @@ public class I18n {
|
|||
if (!currentDir.equals(previousDir)) {
|
||||
log.debug("Theme directory changed from '" + previousDir + "' to '"
|
||||
+ currentDir + "' - clearing the cache.");
|
||||
ResourceBundle.clearCache();
|
||||
clearCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
TranslationProvider.getInstance().clearCache();
|
||||
}
|
||||
|
||||
/** Only clear the cache one time per request. */
|
||||
private void clearCacheOnRequest(HttpServletRequest req) {
|
||||
if (req.getAttribute(ATTRIBUTE_CACHE_CLEARED) != null) {
|
||||
log.debug("Cache was already cleared on this request.");
|
||||
} else {
|
||||
ResourceBundle.clearCache();
|
||||
clearCache();
|
||||
log.debug("Cache cleared.");
|
||||
req.setAttribute(ATTRIBUTE_CACHE_CLEARED, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Control classes for instantiating ResourceBundles
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Instead of looking in the classpath, look in the theme i18n directory and
|
||||
* the application i18n directory.
|
||||
*/
|
||||
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 and in the bundle
|
||||
* directory under the application 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 VitroResourceBundle.getBundle(bundleName, ctx, appI18nPath,
|
||||
themeI18nPath, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* When creating the chain of acceptable Locales, include approximate
|
||||
* matches before giving up and using the root Locale.
|
||||
*
|
||||
* Check the list of supported Locales to see if any have the same
|
||||
* language but different region. If we find any, sort them and insert
|
||||
* them into the usual result list, just before the root Locale.
|
||||
*/
|
||||
@Override
|
||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||
// Find the list of Locales that would normally be returned.
|
||||
List<Locale> usualList = super
|
||||
.getCandidateLocales(baseName, locale);
|
||||
|
||||
// If our "selectable locales" include no approximate matches that
|
||||
// are not already in the list, we're done.
|
||||
SortedSet<Locale> approximateMatches = findApproximateMatches(locale);
|
||||
approximateMatches.removeAll(usualList);
|
||||
if (approximateMatches.isEmpty()) {
|
||||
return usualList;
|
||||
}
|
||||
|
||||
// Otherwise, insert those approximate matches into the list just
|
||||
// before the ROOT locale.
|
||||
List<Locale> mergedList = new LinkedList<>(usualList);
|
||||
int rootLocaleHere = mergedList.indexOf(Locale.ROOT);
|
||||
if (rootLocaleHere == -1) {
|
||||
mergedList.addAll(approximateMatches);
|
||||
} else {
|
||||
mergedList.addAll(rootLocaleHere, approximateMatches);
|
||||
}
|
||||
return mergedList;
|
||||
}
|
||||
|
||||
private SortedSet<Locale> findApproximateMatches(Locale locale) {
|
||||
SortedSet<Locale> set = new TreeSet<>(new LocaleComparator());
|
||||
|
||||
for (Locale l : SelectedLocale.getSelectableLocales(ctx)) {
|
||||
if (locale.getLanguage().equals(l.getLanguage())) {
|
||||
set.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class LocaleComparator implements Comparator<Locale> {
|
||||
@Override
|
||||
public int compare(Locale o1, Locale o2) {
|
||||
int c = o1.getLanguage().compareTo(o2.getLanguage());
|
||||
if (c == 0) {
|
||||
c = o1.getCountry().compareTo(o2.getCountry());
|
||||
if (c == 0) {
|
||||
c = o1.getVariant().compareTo(o2.getVariant());
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,133 +1,10 @@
|
|||
/* $This file is distributed under the terms of the license in LICENSE$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.ResourceBundle;
|
||||
public interface I18nBundle {
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.developer.Key;
|
||||
|
||||
/**
|
||||
* 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 START_SEP = "\u25a4";
|
||||
private static final String END_SEP = "\u25a5";
|
||||
public static final String START_SEP = "\u25a4";
|
||||
public static final String END_SEP = "\u25a5";
|
||||
public static final String INT_SEP = "\u25a6";
|
||||
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,
|
||||
I18nLogger i18nLogger) {
|
||||
return new I18nBundle(bundleName, i18nLogger);
|
||||
}
|
||||
|
||||
private final String bundleName;
|
||||
private final ResourceBundle resources;
|
||||
private final String notFoundMessage;
|
||||
private final I18nLogger i18nLogger;
|
||||
|
||||
private I18nBundle(String bundleName, I18nLogger i18nLogger) {
|
||||
this(bundleName, new EmptyResourceBundle(), MESSAGE_BUNDLE_NOT_FOUND,
|
||||
i18nLogger);
|
||||
}
|
||||
|
||||
public I18nBundle(String bundleName, ResourceBundle resources,
|
||||
I18nLogger i18nLogger) {
|
||||
this(bundleName, resources, MESSAGE_KEY_NOT_FOUND, i18nLogger);
|
||||
}
|
||||
|
||||
private I18nBundle(String bundleName, ResourceBundle resources,
|
||||
String notFoundMessage, I18nLogger i18nLogger) {
|
||||
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;
|
||||
this.i18nLogger = i18nLogger;
|
||||
}
|
||||
|
||||
public String text(String key, Object... parameters) {
|
||||
String textString;
|
||||
if (resources.containsKey(key)) {
|
||||
textString = resources.getString(key);
|
||||
log.debug("In '" + bundleName + "', " + key + "='" + textString
|
||||
+ "')");
|
||||
} else {
|
||||
String message = MessageFormat.format(notFoundMessage, bundleName,
|
||||
key);
|
||||
log.warn(message);
|
||||
textString = "ERROR: " + message;
|
||||
}
|
||||
String message = formatString(textString, parameters);
|
||||
|
||||
if (i18nLogger != null) {
|
||||
i18nLogger.log(bundleName, key, parameters, textString, message);
|
||||
}
|
||||
if (isNeedExportInfo()) {
|
||||
String separatedArgs = "";
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
separatedArgs += parameters[i] + INT_SEP;
|
||||
}
|
||||
|
||||
return START_SEP + key + INT_SEP + textString + INT_SEP + separatedArgs + message + END_SEP;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static boolean isNeedExportInfo() {
|
||||
return DeveloperSettings.getInstance().getBoolean(Key.I18N_ONLINE_TRANSLATION);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
public String text(String key, Object... parameters);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletContextListener;
|
||||
|
||||
public class I18nContextListener implements ServletContextListener{
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
initializeTranslationProvider(sce);
|
||||
initializeTranslationConverter(sce);
|
||||
}
|
||||
|
||||
private void initializeTranslationConverter(ServletContextEvent sce) {
|
||||
ServletContext ctx = sce.getServletContext();
|
||||
TranslationConverter.getInstance().initialize(ctx);
|
||||
|
||||
}
|
||||
|
||||
private void initializeTranslationProvider(ServletContextEvent sce) {
|
||||
ServletContext ctx = sce.getServletContext();
|
||||
TranslationProvider.getInstance().initialize(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {
|
||||
}
|
||||
|
||||
}
|
|
@ -13,32 +13,29 @@ import edu.cornell.mannlib.vitro.webapp.utils.developer.Key;
|
|||
/**
|
||||
* If enabled in developer mode, write a message to the log each time someone
|
||||
* asks for a language string.
|
||||
*
|
||||
* The I18nBundle has a life span of one HTTP request, and so does this.
|
||||
*/
|
||||
public class I18nLogger {
|
||||
private static final Log log = LogFactory.getLog(I18nLogger.class);
|
||||
|
||||
private final boolean isLogging;
|
||||
private DeveloperSettings settings;
|
||||
|
||||
public I18nLogger() {
|
||||
DeveloperSettings settings = DeveloperSettings.getInstance();
|
||||
this.isLogging = settings.getBoolean(Key.I18N_LOG_STRINGS)
|
||||
&& log.isInfoEnabled();
|
||||
settings = DeveloperSettings.getInstance();
|
||||
}
|
||||
|
||||
public void log(String bundleName, String key, Object[] parameters,
|
||||
String rawText, String formattedText) {
|
||||
if (isLogging) {
|
||||
public void log(String key, Object[] parameters, String rawText, String formattedText) {
|
||||
if (isI18nLoggingTurnedOn()) {
|
||||
String message = String.format(
|
||||
"Retrieved from %s.%s with %s: '%s'", bundleName, key,
|
||||
"Retrieved from %s with %s: '%s'", key,
|
||||
Arrays.toString(parameters), rawText);
|
||||
|
||||
if (!rawText.equals(formattedText)) {
|
||||
message += String.format(" --> '%s'", formattedText);
|
||||
}
|
||||
|
||||
log.info(message);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isI18nLoggingTurnedOn() {
|
||||
return settings.getBoolean(Key.I18N_LOG_STRINGS) && log.isInfoEnabled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class I18nSemanticBundle implements I18nBundle {
|
||||
|
||||
private List<String> preferredLocales = Collections.emptyList();
|
||||
|
||||
public I18nSemanticBundle(List<Locale> preferredLocales){
|
||||
this.preferredLocales = convertToStrings(preferredLocales);
|
||||
}
|
||||
|
||||
private static List<String> convertToStrings(List<Locale> preferredLocales) {
|
||||
return preferredLocales.stream().map(Locale::toLanguageTag).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String text(String key, Object... parameters) {
|
||||
final TranslationProvider provider = TranslationProvider.getInstance();
|
||||
return provider.getTranslation(preferredLocales, key, parameters);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.DirectoryFileFilter;
|
||||
import org.apache.commons.io.filefilter.RegexFileFilter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.jena.ontology.OntModel;
|
||||
import org.apache.jena.ontology.OntModelSpec;
|
||||
import org.apache.jena.query.ParameterizedSparqlString;
|
||||
import org.apache.jena.query.Query;
|
||||
import org.apache.jena.query.QueryExecution;
|
||||
import org.apache.jena.query.QueryExecutionFactory;
|
||||
import org.apache.jena.query.QueryFactory;
|
||||
import org.apache.jena.query.QuerySolution;
|
||||
import org.apache.jena.query.QuerySolutionMap;
|
||||
import org.apache.jena.query.ResultSet;
|
||||
import org.apache.jena.rdf.model.ModelFactory;
|
||||
import org.apache.jena.rdf.model.RDFNode;
|
||||
import org.apache.jena.rdf.model.Literal;
|
||||
import org.apache.jena.rdf.model.ResourceFactory;
|
||||
import org.apache.jena.shared.Lock;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent;
|
||||
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
|
||||
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
|
||||
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONFIGURATION;
|
||||
|
||||
|
||||
public class TranslationConverter {
|
||||
|
||||
protected OntModel memModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM);
|
||||
protected ServletContext ctx;
|
||||
private static final boolean BEGIN = true;
|
||||
private static final boolean END = !BEGIN;
|
||||
private static final int SUFFIX_LENGTH = ".properties".length();
|
||||
private static final Log log = LogFactory.getLog(TranslationConverter.class);
|
||||
private static final TranslationConverter INSTANCE = new TranslationConverter();
|
||||
private static final String THEMES = "themes";
|
||||
private static final String ALL = "all";
|
||||
protected static final String APP_I18N_PATH = "/i18n/";
|
||||
protected static final String LOCAL_I18N_PATH = "/local/i18n/";
|
||||
protected static final String THEMES_PATH = "/themes/";
|
||||
private static final String TEMPLATE_BODY = ""
|
||||
+ "?uri <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#NamedIndividual> .\n"
|
||||
+ "?uri <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> .\n"
|
||||
+ "?uri <http://vivoweb.org/ontology/core/properties/vocabulary#hasApp> ?application .\n"
|
||||
+ "?uri <http://vivoweb.org/ontology/core/properties/vocabulary#hasKey> ?key .\n";
|
||||
private static final String TEMPLATE_LABEL = ""
|
||||
+ "?uri <http://www.w3.org/2000/01/rdf-schema#label> ?label .\n";
|
||||
private static final String TEMPLATE_THEME = ""
|
||||
+ "?uri <http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme> ?theme .\n";
|
||||
|
||||
private static final String queryWithTheme(String langTag) {
|
||||
return
|
||||
"SELECT ?uri ?label WHERE {"
|
||||
+ TEMPLATE_BODY
|
||||
+ optionalLabel(langTag)
|
||||
+ TEMPLATE_THEME
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private static final String queryNoTheme(String langTag) {
|
||||
return
|
||||
"SELECT ?uri ?label WHERE {"
|
||||
+ TEMPLATE_BODY
|
||||
+ optionalLabel(langTag)
|
||||
+ "FILTER NOT EXISTS {"
|
||||
+ TEMPLATE_THEME
|
||||
+ "}"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private static final String optionalLabel(String langTag) {
|
||||
return
|
||||
"OPTIONAL {"
|
||||
+ "?uri <http://www.w3.org/2000/01/rdf-schema#label> ?label .\n "
|
||||
+ "FILTER (LANG(?label)=\"" + langTag + "\")"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
public static TranslationConverter getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void initialize(ServletContext ctx) {
|
||||
this.ctx = ctx;
|
||||
OntModel tdbModel = ModelAccess.on(ctx).getOntModel(ModelNames.INTERFACE_I18N);
|
||||
RDFService rdfService = ModelAccess.on(ctx).getRDFService(CONFIGURATION);
|
||||
memModel.add(tdbModel);
|
||||
convertAll();
|
||||
cleanTdbModel(tdbModel, rdfService);
|
||||
updateTDBModel(rdfService);
|
||||
}
|
||||
|
||||
private void cleanTdbModel(OntModel storedModel, RDFService rdfService) {
|
||||
ChangeSet cs = makeChangeSet(rdfService);
|
||||
ByteArrayOutputStream removeOS = new ByteArrayOutputStream();
|
||||
storedModel.write(removeOS, "N3");
|
||||
InputStream removeIS = new ByteArrayInputStream(removeOS.toByteArray());
|
||||
cs.addRemoval(removeIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N);
|
||||
try {
|
||||
rdfService.changeSetUpdate(cs);
|
||||
} catch (RDFServiceException e) {
|
||||
log.error(e,e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTDBModel(RDFService rdfService) {
|
||||
ChangeSet cs = makeChangeSet(rdfService);
|
||||
ByteArrayOutputStream addOS = new ByteArrayOutputStream();
|
||||
memModel.write(addOS, "N3");
|
||||
InputStream addIS = new ByteArrayInputStream(addOS.toByteArray());
|
||||
cs.addAddition(addIS, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.INTERFACE_I18N);
|
||||
try {
|
||||
rdfService.changeSetUpdate(cs);
|
||||
} catch (RDFServiceException e) {
|
||||
log.error(e,e);
|
||||
}
|
||||
}
|
||||
|
||||
public void convertAll() {
|
||||
List<String> i18nDirs = new LinkedList<>(Arrays.asList(APP_I18N_PATH, LOCAL_I18N_PATH, THEMES_PATH));
|
||||
List<String> prefixes = VitroResourceBundle.getAppPrefixes();
|
||||
prefixes.add("");
|
||||
String prefixesRegex = "(" + StringUtils.join(prefixes, ALL + "|") + ALL + ")";
|
||||
log.debug("prefixesRegex " + prefixesRegex);
|
||||
for (String dir : i18nDirs) {
|
||||
File realDir = new File(ctx.getRealPath(dir));
|
||||
Collection<File> files = FileUtils.listFiles(realDir, new RegexFileFilter(prefixesRegex + ".*\\.properties"), DirectoryFileFilter.DIRECTORY);
|
||||
for (File file : files) {
|
||||
convert(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void convert(File file) {
|
||||
Properties props = new Properties();
|
||||
try (Reader reader = new InputStreamReader( new FileInputStream(file), "UTF-8")) {
|
||||
props.load(reader);
|
||||
} catch (Exception e) {
|
||||
log.error(e,e);
|
||||
}
|
||||
if (props == null || props.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("Converting properties " + file.getAbsolutePath());
|
||||
String theme = getTheme(file);
|
||||
String application = getApplication(file);
|
||||
String language = getLanguage(file);
|
||||
String langTag = getLanguageTag(language);
|
||||
StringWriter additions = new StringWriter();
|
||||
StringWriter retractionsN3 = new StringWriter();
|
||||
for (Object key : props.keySet()) {
|
||||
Object value = props.get(key);
|
||||
QueryExecution queryExecution = getQueryExecution(key.toString(), theme, application, langTag);
|
||||
ResultSet results = queryExecution.execSelect();
|
||||
String uri = null;
|
||||
if (results.hasNext()) {
|
||||
QuerySolution solution = results.nextSolution();
|
||||
uri = solution.get("uri").toString();
|
||||
String label = getLabel(solution);
|
||||
if (labelAreadyExists(value, label)) {
|
||||
continue;
|
||||
}
|
||||
if (!StringUtils.isBlank(label)) {
|
||||
String retraction = fillOutLabelTemplate(uri, label, langTag);
|
||||
retractionsN3.append(retraction);
|
||||
}
|
||||
}
|
||||
String addition = fillOutTemplate(uri, key.toString(), value.toString(), theme, application, langTag);
|
||||
additions.append(addition);
|
||||
}
|
||||
log.debug("Remove from model" + retractionsN3.toString());
|
||||
log.debug("Add to model" + additions.toString());
|
||||
OntModel addModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM);
|
||||
OntModel removeModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM);
|
||||
addModel.read(new StringReader(additions.toString()), null, "n3");
|
||||
removeModel.read(new StringReader(retractionsN3.toString()), null, "n3");
|
||||
memModel.enterCriticalSection(Lock.WRITE);
|
||||
try {
|
||||
memModel.remove(removeModel);
|
||||
memModel.add(addModel);
|
||||
} finally {
|
||||
memModel.leaveCriticalSection();
|
||||
}
|
||||
log.info("Conversion finished for properties " + file.getAbsolutePath());
|
||||
|
||||
}
|
||||
|
||||
private String getLanguageTag(String language) {
|
||||
return language.replaceAll("_","-");
|
||||
}
|
||||
|
||||
private String getLabel(QuerySolution solution) {
|
||||
final RDFNode label = solution.get("label");
|
||||
if (label == null) {
|
||||
return "";
|
||||
}
|
||||
return ((Literal)label).getLexicalForm();
|
||||
}
|
||||
|
||||
private boolean labelAreadyExists(Object value, String label) {
|
||||
return label.equals(value.toString());
|
||||
}
|
||||
|
||||
private String fillOutTemplate(String uri, String key, String newLabel, String theme, String application, String langTag) {
|
||||
if (StringUtils.isBlank(uri)) {
|
||||
return fillOutFullTemplate(key, newLabel, theme, application, langTag);
|
||||
} else {
|
||||
return fillOutLabelTemplate(uri, newLabel, langTag);
|
||||
}
|
||||
}
|
||||
|
||||
private String fillOutLabelTemplate(String uri, String label, String langTag) {
|
||||
ParameterizedSparqlString pss = new ParameterizedSparqlString();
|
||||
pss.setCommandText(TEMPLATE_LABEL);
|
||||
pss.setIri("uri", uri);
|
||||
pss.setLiteral("label", label, langTag);
|
||||
return pss.toString();
|
||||
}
|
||||
|
||||
private String fillOutFullTemplate(String key, String label, String theme, String application, String langTag) {
|
||||
String template = getBodyTemplate(theme);
|
||||
ParameterizedSparqlString pss = new ParameterizedSparqlString();
|
||||
pss.setCommandText(template);
|
||||
pss.setIri("uri", createUUID());
|
||||
pss.setLiteral("label", label, langTag);
|
||||
pss.setLiteral("key", key);
|
||||
pss.setLiteral("application", application);
|
||||
if (!StringUtils.isBlank(theme)) {
|
||||
pss.setLiteral("theme", theme);
|
||||
}
|
||||
return pss.toString();
|
||||
}
|
||||
|
||||
private QueryExecution getQueryExecution(String key, String theme, String application, String langTag) {
|
||||
Query query;
|
||||
QuerySolutionMap bindings = new QuerySolutionMap();
|
||||
bindings.add("application", ResourceFactory.createStringLiteral(application));
|
||||
bindings.add("key", ResourceFactory.createStringLiteral(key));
|
||||
if (StringUtils.isBlank(theme)) {
|
||||
query = QueryFactory.create(queryNoTheme(langTag));
|
||||
} else {
|
||||
query = QueryFactory.create(queryWithTheme(langTag));
|
||||
bindings.add("theme", ResourceFactory.createStringLiteral(theme));
|
||||
}
|
||||
QueryExecution qexec = QueryExecutionFactory.create(query, memModel, bindings);
|
||||
return qexec;
|
||||
}
|
||||
|
||||
private String createUUID() {
|
||||
return "urn:uuid:" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
private String getBodyTemplate(String theme) {
|
||||
if (StringUtils.isBlank(theme)) {
|
||||
return TEMPLATE_BODY + TEMPLATE_LABEL;
|
||||
}
|
||||
return TEMPLATE_BODY + TEMPLATE_LABEL + TEMPLATE_THEME;
|
||||
}
|
||||
|
||||
private String getLanguage(File file) {
|
||||
String name = file.getName();
|
||||
if (!name.contains("_")) {
|
||||
return "en_US";
|
||||
}
|
||||
int startIndex;
|
||||
if (name.contains("_all")) {
|
||||
startIndex = name.indexOf("_all_") + 5;
|
||||
} else {
|
||||
startIndex = name.indexOf("_") + 1;
|
||||
}
|
||||
int endIndex = name.length() - SUFFIX_LENGTH;
|
||||
|
||||
return name.substring(startIndex,endIndex);
|
||||
}
|
||||
|
||||
private String getApplication(File file) {
|
||||
String name = file.getName();
|
||||
if (name.toLowerCase().contains("vivo")) {
|
||||
return "VIVO";
|
||||
}
|
||||
return "Vitro";
|
||||
}
|
||||
|
||||
private String getTheme(File file) {
|
||||
File parent = file.getParentFile();
|
||||
if (parent == null) {
|
||||
return "";
|
||||
}
|
||||
if (THEMES.equals(parent.getName())) {
|
||||
return file.getName();
|
||||
}
|
||||
return getTheme(parent);
|
||||
}
|
||||
|
||||
private ChangeSet makeChangeSet(RDFService rdfService) {
|
||||
ChangeSet cs = rdfService.manufactureChangeSet();
|
||||
cs.addPreChangeEvent(new BulkUpdateEvent(null, BEGIN));
|
||||
cs.addPostChangeEvent(new BulkUpdateEvent(null, END));
|
||||
return cs;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.jena.query.QuerySolution;
|
||||
import org.apache.jena.rdf.model.Literal;
|
||||
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONFIGURATION;
|
||||
import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean;
|
||||
import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean.LevelRevisionInfo;
|
||||
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
|
||||
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LanguageFilteringRDFService;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.developer.DeveloperSettings;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.developer.Key;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder;
|
||||
|
||||
public class TranslationProvider {
|
||||
|
||||
private static final String MESSAGE_KEY_NOT_FOUND = "ERROR: Translation not found ''{0}''";
|
||||
private static final TranslationProvider INSTANCE = new TranslationProvider();
|
||||
private static final Log log = LogFactory.getLog(TranslationProvider.class);
|
||||
private static final I18nLogger i18nLogger = new I18nLogger();
|
||||
private static final String QUERY = ""
|
||||
+ "PREFIX : <http://vivoweb.org/ontology/core/properties/vocabulary#>\n"
|
||||
+ "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n"
|
||||
+ "PREFIX vitro: <http://vitro.mannlib.cornell.edu/ns/vitro/0.7#> \n"
|
||||
+ "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> \n"
|
||||
+ "SELECT ?translation \n" + "WHERE {\n"
|
||||
+ " GRAPH <http://vitro.mannlib.cornell.edu/default/interface-i18n> {\n"
|
||||
+ " ?uri :hasKey ?key .\n"
|
||||
+ " ?uri rdfs:label ?translation .\n"
|
||||
+ " OPTIONAL { \n"
|
||||
+ " ?uri :hasTheme ?found_theme .\n"
|
||||
+ " }\n"
|
||||
+ " OPTIONAL { \n"
|
||||
+ " ?uri :hasApp ?found_application .\n"
|
||||
+ " }\n"
|
||||
+ " BIND(COALESCE(?found_theme, \"none\") as ?theme ) .\n"
|
||||
+ " FILTER(?theme = \"none\" || ?theme = ?current_theme) . "
|
||||
+ " BIND(COALESCE(?found_application, \"none\") as ?application ) .\n"
|
||||
+ " BIND(IF(?current_application = ?application && ?current_theme = ?theme, 3, "
|
||||
+ " IF(?current_theme = ?theme, 2, "
|
||||
+ " IF(?current_application = ?application, 1, 0)) ) AS ?order ) .\n"
|
||||
+ " }\n" + "} \n"
|
||||
+ "ORDER by DESC(?order)";
|
||||
|
||||
protected RDFService rdfService;
|
||||
protected String application = "Vitro";
|
||||
private Map<TranslationKey, String> cache = new ConcurrentHashMap<>();
|
||||
private String theme = "vitro";
|
||||
private int prefixLen = "themes/".length();
|
||||
private int suffixLen = "/".length();
|
||||
private WebappDaoFactory wdf;
|
||||
|
||||
public static TranslationProvider getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void initialize(ServletContext ctx) {
|
||||
RevisionInfoBean info = (RevisionInfoBean) ctx.getAttribute(RevisionInfoBean.ATTRIBUTE_NAME);
|
||||
List<LevelRevisionInfo> levelInfos = info.getLevelInfos();
|
||||
setApplication(levelInfos);
|
||||
rdfService = ModelAccess.on(ctx).getRDFService(CONFIGURATION);
|
||||
wdf = ModelAccess.on(ctx).getWebappDaoFactory();
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
private void updateTheme() {
|
||||
final String themeDir = wdf.getApplicationDao().getApplicationBean().getThemeDir();
|
||||
final int length = themeDir.length();
|
||||
theme = themeDir.substring(prefixLen, length - suffixLen);
|
||||
}
|
||||
|
||||
public void setTheme(String theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
private void setApplication(List<LevelRevisionInfo> levelInfos) {
|
||||
if (levelInfos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
application = levelInfos.get(0).getName();
|
||||
}
|
||||
|
||||
public String getTranslation(List<String> preferredLocales, String key, Object[] parameters) {
|
||||
TranslationKey tk = new TranslationKey(preferredLocales, key, parameters);
|
||||
if (cache.containsKey(tk) && !needExportInfo()) {
|
||||
log.debug("Returned value from cache for " + key);
|
||||
return cache.get(tk);
|
||||
}
|
||||
String text = getText(preferredLocales, key);
|
||||
String formattedText = formatString(text, parameters);
|
||||
i18nLogger.log(key, parameters, text, formattedText);
|
||||
if (needExportInfo()) {
|
||||
return prepareExportInfo(key, parameters, text, formattedText);
|
||||
} else {
|
||||
cache.put(tk, formattedText);
|
||||
log.debug("Added to cache " + key);
|
||||
log.debug("Returned value from request for " + key);
|
||||
return formattedText;
|
||||
}
|
||||
}
|
||||
|
||||
private String prepareExportInfo(String key, Object[] parameters, String text, String message) {
|
||||
String separatedArgs = "";
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
separatedArgs += parameters[i] + I18nBundle.INT_SEP;
|
||||
}
|
||||
log.debug("Returned value with export info for " + key );
|
||||
return I18nBundle.START_SEP + key + I18nBundle.INT_SEP + text + I18nBundle.INT_SEP + separatedArgs
|
||||
+ message + I18nBundle.END_SEP;
|
||||
}
|
||||
|
||||
private String getText(List<String> preferredLocales, String key) {
|
||||
String textString;
|
||||
QueryHolder queryHolder = new QueryHolder(QUERY)
|
||||
.bindToPlainLiteral("current_application", application)
|
||||
.bindToPlainLiteral("key", key)
|
||||
.bindToPlainLiteral("current_theme", theme)
|
||||
.bindToPlainLiteral("locale", preferredLocales.get(0));
|
||||
|
||||
LanguageFilteringRDFService lfrs = new LanguageFilteringRDFService(rdfService, preferredLocales);
|
||||
List<String> list = new LinkedList<>();
|
||||
try {
|
||||
lfrs.sparqlSelectQuery(queryHolder.getQueryString(), new ResultSetConsumer() {
|
||||
@Override
|
||||
protected void processQuerySolution(QuerySolution qs) {
|
||||
Literal translation = qs.getLiteral("translation");
|
||||
if (translation != null) {
|
||||
list.add(translation.getLexicalForm());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (RDFServiceException e) {
|
||||
log.error(e,e);
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
textString = notFound(key);
|
||||
} else {
|
||||
textString = list.get(0);
|
||||
}
|
||||
return textString;
|
||||
}
|
||||
|
||||
private static boolean needExportInfo() {
|
||||
return DeveloperSettings.getInstance().getBoolean(Key.I18N_ONLINE_TRANSLATION);
|
||||
}
|
||||
|
||||
private static String formatString(String textString, Object... parameters) {
|
||||
if (parameters.length == 0) {
|
||||
return textString;
|
||||
} else {
|
||||
return MessageFormat.format(textString, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
private String notFound(String key) {
|
||||
return MessageFormat.format(MESSAGE_KEY_NOT_FOUND, key);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
if (wdf != null) {
|
||||
updateTheme();
|
||||
}
|
||||
cache.clear();
|
||||
log.info("Translation cache cleared");
|
||||
}
|
||||
|
||||
private class TranslationKey {
|
||||
|
||||
private List<String> preferredLocales;
|
||||
private String key;
|
||||
private Object[] parameters;
|
||||
|
||||
public TranslationKey(List<String> preferredLocales, String key, Object[] parameters) {
|
||||
this.preferredLocales = preferredLocales;
|
||||
this.key = key;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (! (obj instanceof TranslationKey)) {
|
||||
return false;
|
||||
}
|
||||
TranslationKey other = (TranslationKey) obj;
|
||||
return new EqualsBuilder()
|
||||
.append(preferredLocales, other.preferredLocales)
|
||||
.append(key, other.key)
|
||||
.append(parameters, other.parameters)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode(){
|
||||
return new HashCodeBuilder()
|
||||
.append(preferredLocales)
|
||||
.append(key)
|
||||
.append(parameters)
|
||||
.toHashCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,54 +2,15 @@
|
|||
|
||||
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.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
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:
|
||||
* If you use 3 tier architecture with custom prefix for properties files
|
||||
* you can add it with {@link #addAppPrefix(String)}
|
||||
*
|
||||
* 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 VitroResourceBundle extends ResourceBundle {
|
||||
private static final Log log = LogFactory.getLog(VitroResourceBundle.class);
|
||||
|
||||
private static final String FILE_FLAG = "@@file ";
|
||||
private static final String MESSAGE_FILE_NOT_FOUND = "File {1} not found for property {0}.";
|
||||
public class VitroResourceBundle {
|
||||
|
||||
private static final List<String> appPrefixes = new ArrayList<>();
|
||||
|
||||
|
@ -57,6 +18,10 @@ public class VitroResourceBundle extends ResourceBundle {
|
|||
addAppPrefix("vitro");
|
||||
}
|
||||
|
||||
public static List<String> getAppPrefixes(){
|
||||
return appPrefixes;
|
||||
}
|
||||
|
||||
public static void addAppPrefix(String prefix) {
|
||||
if (!prefix.endsWith("-") && !prefix.endsWith("_")) {
|
||||
prefix = prefix + "_";
|
||||
|
@ -67,188 +32,4 @@ public class VitroResourceBundle extends ResourceBundle {
|
|||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Factory method
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the bundle for the for foo_ba_RR, providing that
|
||||
* foo_ba_RR.properties exists in the I18n area of either the theme or the
|
||||
* application.
|
||||
*
|
||||
* If the desired file doesn't exist in either location, return null.
|
||||
* Usually, this does not indicate a problem but only that we were looking
|
||||
* for too specific a bundle. For example, if the base name of the bundle is
|
||||
* "all" and the locale is "en_US", we will likely return null on the search
|
||||
* for all_en_US.properties, and all_en.properties, but will return a full
|
||||
* bundle for all.properties.
|
||||
*
|
||||
* Of course, if all.properties doesn't exist either, then we have a
|
||||
* problem, but that will be reported elsewhere.
|
||||
*
|
||||
* @return the populated bundle or null.
|
||||
*/
|
||||
public static VitroResourceBundle getBundle(String bundleName,
|
||||
ServletContext ctx, String appI18nPath, String themeI18nPath,
|
||||
Control control) {
|
||||
try {
|
||||
return new VitroResourceBundle(bundleName, ctx, appI18nPath,
|
||||
themeI18nPath, control);
|
||||
} catch (FileNotFoundException e) {
|
||||
log.debug(e.getMessage());
|
||||
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 properties;
|
||||
|
||||
private VitroResourceBundle(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.properties = loadProperties();
|
||||
loadReferencedFiles();
|
||||
}
|
||||
|
||||
private Properties loadProperties() throws IOException {
|
||||
String resourceName = control.toResourceName(bundleName, "properties");
|
||||
Properties props = null;
|
||||
|
||||
File defaultsPath = locateFile(joinPath(appI18nPath, resourceName));
|
||||
File propertiesPath = locateFile(joinPath(themeI18nPath, resourceName));
|
||||
|
||||
props = loadProperties(props, defaultsPath);
|
||||
if (appPrefixes != null && appPrefixes.size() > 0) {
|
||||
for (String appPrefix : appPrefixes) {
|
||||
props = loadProperties(props, locateFile(joinPath(appI18nPath, (appPrefix + resourceName))));
|
||||
}
|
||||
}
|
||||
props = loadProperties(props, propertiesPath);
|
||||
if (props == null) {
|
||||
throw new FileNotFoundException("Property file not found at '" + defaultsPath + "' or '" + propertiesPath + "'");
|
||||
}
|
||||
props = loadProperties(props, locateFile(joinPath("/local/i18n/", resourceName)));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
private Properties loadProperties(Properties defProps, File file) throws IOException {
|
||||
if (file == null || !file.isFile()) {
|
||||
return defProps;
|
||||
}
|
||||
|
||||
Properties props = null;
|
||||
if (defProps != null) {
|
||||
props = new Properties(defProps);
|
||||
} else {
|
||||
props = new Properties();
|
||||
}
|
||||
|
||||
log.debug("Loading bundle '" + bundleName + "' defaults from '" + file + "'");
|
||||
FileInputStream stream = new FileInputStream(file);
|
||||
Reader reader = new InputStreamReader(stream, "UTF-8");
|
||||
try {
|
||||
props.load(reader);
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
if (props.size() > 0) {
|
||||
return props;
|
||||
}
|
||||
|
||||
return defProps;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,21 +15,17 @@ import freemarker.template.TemplateModelException;
|
|||
* wrapper around an I18nBundle.
|
||||
*/
|
||||
public class I18nBundleTemplateModel implements TemplateHashModel {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(I18nBundleTemplateModel.class);
|
||||
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;
|
||||
public I18nBundleTemplateModel( I18nBundle textBundle) {
|
||||
this.textBundle = textBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemplateModel get(String key) throws TemplateModelException {
|
||||
return new I18nStringTemplateModel(bundleName, key,
|
||||
textBundle.text(key));
|
||||
return new I18nStringTemplateModel(key, textBundle.text(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,7 +12,7 @@ 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.TemplateMethodModelEx;
|
||||
import freemarker.template.TemplateModelException;
|
||||
|
||||
/**
|
||||
|
@ -21,30 +21,15 @@ import freemarker.template.TemplateModelException;
|
|||
*
|
||||
* If the bundle name is not provided, the default bundle is assumed.
|
||||
*/
|
||||
public class I18nMethodModel implements TemplateMethodModel {
|
||||
public class I18nMethodModel implements TemplateMethodModelEx {
|
||||
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);
|
||||
HttpServletRequest request = (HttpServletRequest) env.getCustomAttribute("request");
|
||||
I18nBundle tb = I18n.bundle(request);
|
||||
return new I18nBundleTemplateModel(tb);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,13 +37,10 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx,
|
|||
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;
|
||||
public I18nStringTemplateModel( String key, String textString) {
|
||||
this.key = key;
|
||||
this.textString = textString;
|
||||
}
|
||||
|
@ -56,8 +53,7 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx,
|
|||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Override
|
||||
public Object exec(List args) throws TemplateModelException {
|
||||
log.debug("Formatting string '" + key + "' from bundle '" + bundleName
|
||||
+ "' with these arguments: " + args);
|
||||
log.debug("Formatting string '" + key + "' with these arguments: " + args);
|
||||
|
||||
if (args.isEmpty()) {
|
||||
return textString;
|
||||
|
@ -74,8 +70,7 @@ public class I18nStringTemplateModel implements TemplateMethodModelEx,
|
|||
return MessageFormat.format(textString, unwrappedArgs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String message = "Can't format '" + key + "' from bundle '"
|
||||
+ bundleName + "', wrong argument types: " + args
|
||||
String message = "Can't format '" + key + "', wrong argument types: " + args
|
||||
+ " for message format'" + textString + "'";
|
||||
log.warn(message);
|
||||
return message;
|
||||
|
|
|
@ -34,6 +34,10 @@ public class ModelNames {
|
|||
public static final String DISPLAY_TBOX_FIRSTTIME_BACKUP = DISPLAY_TBOX + "FirsttimeBackup";
|
||||
public static final String DISPLAY_DISPLAY = "http://vitro.mannlib.cornell.edu/default/vitro-kb-displayMetadata-displayModel";
|
||||
public static final String DISPLAY_DISPLAY_FIRSTTIME_BACKUP = DISPLAY_DISPLAY + "FirsttimeBackup";
|
||||
public static final String INTERFACE_I18N = "http://vitro.mannlib.cornell.edu/default/interface-i18n";
|
||||
public static final String INTERFACE_I18N_FIRSTTIME_BACKUP = INTERFACE_I18N + "FirsttimeBackup";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A map of the URIS, keyed by their short names, intended only for display
|
||||
|
@ -64,6 +68,9 @@ public class ModelNames {
|
|||
map.put("DISPLAY_TBOX_FIRSTTIME_BACKUP", DISPLAY_TBOX_FIRSTTIME_BACKUP);
|
||||
map.put("DISPLAY_DISPLAY", DISPLAY_DISPLAY);
|
||||
map.put("DISPLAY_DISPLAY_FIRSTTIME_BACKUP", DISPLAY_DISPLAY_FIRSTTIME_BACKUP);
|
||||
map.put("INTERFACE_I18N", INTERFACE_I18N);
|
||||
map.put("INTERFACE_I18N_FIRSTTIME_BACKUP", INTERFACE_I18N_FIRSTTIME_BACKUP);
|
||||
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.USER_ACCOU
|
|||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_FIRSTTIME_BACKUP;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_TBOX_FIRSTTIME_BACKUP;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_DISPLAY_FIRSTTIME_BACKUP;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N ;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N_FIRSTTIME_BACKUP;
|
||||
|
||||
|
||||
|
||||
import org.apache.jena.rdf.model.ModelMaker;
|
||||
|
@ -25,11 +28,21 @@ public abstract class ConfigurationTripleSource implements TripleSource {
|
|||
* A list of all Configuration models, in case the implementation wants to
|
||||
* add memory-mapping.
|
||||
*/
|
||||
protected static final String[] CONFIGURATION_MODELS = { DISPLAY,
|
||||
DISPLAY_TBOX, DISPLAY_DISPLAY, USER_ACCOUNTS, ABOX_ASSERTIONS_FIRSTTIME_BACKUP,
|
||||
TBOX_ASSERTIONS_FIRSTTIME_BACKUP, APPLICATION_METADATA_FIRSTTIME_BACKUP,
|
||||
USER_ACCOUNTS_FIRSTTIME_BACKUP, DISPLAY_FIRSTTIME_BACKUP,
|
||||
DISPLAY_TBOX_FIRSTTIME_BACKUP, DISPLAY_DISPLAY_FIRSTTIME_BACKUP };
|
||||
protected static final String[] CONFIGURATION_MODELS = {
|
||||
DISPLAY,
|
||||
DISPLAY_TBOX,
|
||||
DISPLAY_DISPLAY,
|
||||
USER_ACCOUNTS,
|
||||
ABOX_ASSERTIONS_FIRSTTIME_BACKUP,
|
||||
TBOX_ASSERTIONS_FIRSTTIME_BACKUP,
|
||||
APPLICATION_METADATA_FIRSTTIME_BACKUP,
|
||||
USER_ACCOUNTS_FIRSTTIME_BACKUP,
|
||||
DISPLAY_FIRSTTIME_BACKUP,
|
||||
DISPLAY_TBOX_FIRSTTIME_BACKUP,
|
||||
DISPLAY_DISPLAY_FIRSTTIME_BACKUP,
|
||||
INTERFACE_I18N,
|
||||
INTERFACE_I18N_FIRSTTIME_BACKUP,
|
||||
};
|
||||
|
||||
/**
|
||||
* These decorators are added to a Configuration ModelMaker, regardless of
|
||||
|
|
|
@ -6,6 +6,7 @@ import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY;
|
|||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_DISPLAY;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY_TBOX;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.USER_ACCOUNTS;
|
||||
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.INTERFACE_I18N;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
|
@ -13,17 +14,10 @@ import javax.servlet.ServletContextListener;
|
|||
|
||||
import org.apache.jena.ontology.OntModel;
|
||||
import org.apache.jena.rdf.model.Model;
|
||||
import org.apache.jena.rdf.model.Property;
|
||||
import org.apache.jena.rdf.model.RDFNode;
|
||||
import org.apache.jena.rdf.model.Resource;
|
||||
import org.apache.jena.rdf.model.Statement;
|
||||
import org.apache.jena.rdf.model.StmtIterator;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
|
||||
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus;
|
||||
|
@ -46,6 +40,7 @@ public class ConfigurationModelsSetup implements ServletContextListener {
|
|||
setupModel(ctx, DISPLAY_TBOX, "displayTbox");
|
||||
setupModel(ctx, DISPLAY_DISPLAY, "displayDisplay");
|
||||
setupModel(ctx, USER_ACCOUNTS, "auth");
|
||||
setupModel(ctx, INTERFACE_I18N, "interface-i18n");
|
||||
ss.info(this, "Set up the display models and the user accounts model.");
|
||||
} catch (Exception e) {
|
||||
ss.fatal(this, e.getMessage(), e.getCause());
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import stubs.javax.servlet.ServletContextStub;
|
||||
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
|
||||
import edu.cornell.mannlib.vitro.webapp.i18n.selection.SelectedLocale;
|
||||
|
||||
/* $This file is distributed under the terms of the license in LICENSE$ */
|
||||
|
||||
/**
|
||||
* Test the I18N functionality.
|
||||
*
|
||||
* Start by checking the logic that finds approximate matches for
|
||||
* language-specific property files.
|
||||
*/
|
||||
public class I18nTest extends AbstractTestClass {
|
||||
private static final List<Locale> SELECTABLE_LOCALES = locales("es_MX",
|
||||
"en_US");
|
||||
|
||||
ServletContextStub ctx;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
ctx = new ServletContextStub();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMatchOnLanguageRegion() {
|
||||
assertLocales("fr_CA", SELECTABLE_LOCALES, "fr_CA", "fr", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMatchOnLanguage() {
|
||||
assertLocales("fr", SELECTABLE_LOCALES, "fr", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMatchOnRoot() {
|
||||
assertLocales("", SELECTABLE_LOCALES, "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchOnLanguageRegion() {
|
||||
assertLocales("es_ES", SELECTABLE_LOCALES, "es_ES", "es", "es_MX", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchOnLanguage() {
|
||||
assertLocales("es", SELECTABLE_LOCALES, "es", "es_MX", "");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private void assertLocales(String requested, List<Locale> selectable,
|
||||
String... expected) {
|
||||
SelectedLocale.setSelectableLocales(ctx, selectable);
|
||||
List<Locale> expectedLocales = locales(expected);
|
||||
|
||||
I18n.ThemeBasedControl control = new I18n.ThemeBasedControl(ctx,
|
||||
"bogusThemeDirectory");
|
||||
List<Locale> actualLocales = control.getCandidateLocales(
|
||||
"bogusBaseName", locale(requested));
|
||||
|
||||
assertEquals("Expected locales", expectedLocales, actualLocales);
|
||||
}
|
||||
|
||||
private static List<Locale> locales(String... strings) {
|
||||
List<Locale> locales = new ArrayList<>();
|
||||
for (String s : strings) {
|
||||
locales.add(locale(s));
|
||||
}
|
||||
return locales;
|
||||
}
|
||||
|
||||
private static Locale locale(String s) {
|
||||
String[] parts = s.split("_");
|
||||
String language = (parts.length > 0) ? parts[0] : "";
|
||||
String country = (parts.length > 1) ? parts[1] : "";
|
||||
String variant = (parts.length > 2) ? parts[2] : "";
|
||||
return new Locale(language, country, variant);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
|
||||
import org.apache.jena.graph.NodeFactory;
|
||||
import org.apache.jena.ontology.OntModel;
|
||||
import org.apache.jena.rdf.model.Selector;
|
||||
import org.apache.jena.rdf.model.SimpleSelector;
|
||||
import org.apache.jena.rdf.model.StmtIterator;
|
||||
import org.apache.jena.rdf.model.impl.PropertyImpl;
|
||||
import org.junit.Test;
|
||||
|
||||
import stubs.javax.servlet.ServletContextStub;
|
||||
|
||||
public class TranslationConverterTest {
|
||||
|
||||
private static final String WILMA = "wilma";
|
||||
private static final String HAS_THEME = "http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme";
|
||||
private static final String VITRO = "Vitro";
|
||||
private static final String VIVO = "VIVO";
|
||||
private static final String HAS_APP = "http://vivoweb.org/ontology/core/properties/vocabulary#hasApp";
|
||||
private static final String HAS_KEY = "http://vivoweb.org/ontology/core/properties/vocabulary#hasKey";
|
||||
private static final String ROOT_PATH = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/root";
|
||||
private static final String INIT_N3_FILE = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationConverterTest/modelInitContent.n3";
|
||||
ServletContextStub ctx = new ServletContextStub();
|
||||
private OntModel model;
|
||||
|
||||
@Test
|
||||
public void testConversion() throws FileNotFoundException {
|
||||
VitroResourceBundle.addAppPrefix("customprefix");
|
||||
VitroResourceBundle.addAppPrefix("vivo");
|
||||
TranslationConverter converter = TranslationConverter.getInstance();
|
||||
model = converter.memModel;
|
||||
File n3 = new File(INIT_N3_FILE);
|
||||
assertTrue(model.isEmpty());
|
||||
model.read(new FileReader(n3), null, "n3");
|
||||
assertFalse(model.isEmpty());
|
||||
File appI18n = new File(ROOT_PATH + TranslationConverter.APP_I18N_PATH);
|
||||
File localI18n = new File(ROOT_PATH + TranslationConverter.LOCAL_I18N_PATH);
|
||||
File themes = new File(ROOT_PATH + TranslationConverter.THEMES_PATH);
|
||||
ctx.setRealPath(TranslationConverter.APP_I18N_PATH, appI18n.getAbsolutePath());
|
||||
ctx.setRealPath(TranslationConverter.LOCAL_I18N_PATH, localI18n.getAbsolutePath());
|
||||
ctx.setRealPath(TranslationConverter.THEMES_PATH, themes.getAbsolutePath());
|
||||
converter.ctx = ctx;
|
||||
converter.convertAll();
|
||||
|
||||
assertEquals(2, getCount(HAS_KEY, "test_key_all_en_US"));
|
||||
assertEquals(2, getCount(HAS_KEY, "test_key_all_en_CA"));
|
||||
|
||||
assertEquals(2, getCount(HAS_KEY, "test_key_all"));
|
||||
|
||||
assertEquals(1, getCount(HAS_KEY, "property_to_overwrite"));
|
||||
|
||||
assertTrue(n3TranslationValueIsOverwrittenByProperty(model));
|
||||
|
||||
assertEquals(3, getCount(HAS_THEME, WILMA));
|
||||
assertEquals(6, getCount(HAS_APP, VITRO));
|
||||
assertEquals(3, getCount(HAS_APP, VIVO));
|
||||
// printResultModel();
|
||||
}
|
||||
|
||||
private void printResultModel() {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
model.write(baos, "N3");
|
||||
System.out.println(baos.toString());
|
||||
}
|
||||
|
||||
private int getCount(String hasTheme, String wilma) {
|
||||
Selector selector = new SimpleSelector(null, new PropertyImpl(hasTheme), wilma);
|
||||
StmtIterator it = model.listStatements(selector);
|
||||
int count = 0;
|
||||
while (it.hasNext()) {
|
||||
count++;
|
||||
it.next();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private boolean n3TranslationValueIsOverwrittenByProperty(OntModel model) {
|
||||
return model.getGraph().contains(
|
||||
NodeFactory.createURI("urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663600"),
|
||||
NodeFactory.createURI("http://www.w3.org/2000/01/rdf-schema#label"),
|
||||
NodeFactory.createLiteral("value from properties file","en-US"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package edu.cornell.mannlib.vitro.webapp.i18n;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.jena.query.Dataset;
|
||||
import org.apache.jena.query.DatasetFactory;
|
||||
import org.apache.jena.rdf.model.Model;
|
||||
import org.apache.jena.rdf.model.ModelFactory;
|
||||
import org.junit.Test;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel;
|
||||
|
||||
public class TranslationProviderTest {
|
||||
|
||||
private static final String VITRO = "Vitro";
|
||||
private static final String VIVO = "VIVO";
|
||||
private static final String ROOT = "src/test/resources/edu/cornell/mannlib/vitro/webapp/i18n/TranslationProviderTest/";
|
||||
private static final String TRANSLATIONS_N3_FILE = ROOT + "modelInitContent.n3";
|
||||
private static final String WILMA = "wilma";
|
||||
private static final String NEMO = "nemo";
|
||||
|
||||
private Model i18nModel;
|
||||
private RDFServiceModel rdfService;
|
||||
private TranslationProvider tp;
|
||||
|
||||
public void init(String i18nFile, String themeName, String appName) throws FileNotFoundException {
|
||||
i18nModel = ModelFactory.createDefaultModel();
|
||||
i18nModel.read(new FileReader(new File(i18nFile)), null, "n3");
|
||||
Dataset ds = DatasetFactory.createTxnMem();
|
||||
ds.addNamedModel("http://vitro.mannlib.cornell.edu/default/interface-i18n", i18nModel);
|
||||
rdfService = new RDFServiceModel(ds);
|
||||
tp = TranslationProvider.getInstance();
|
||||
tp.rdfService = rdfService;
|
||||
tp.setTheme(themeName);
|
||||
tp.application = appName;
|
||||
tp.clearCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotExistingKey() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "non_existing_key", array);
|
||||
assertEquals("ERROR: Translation not found 'non_existing_key'", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVitroWilmaEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey Vitro wilma en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVitroWilmaDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array);
|
||||
assertEquals("testkey Vitro wilma de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVIVOWilmaEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey VIVO wilma en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVIVOWilmaDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array);
|
||||
assertEquals("testkey VIVO wilma de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeFallbackVitroNemoEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey Vitro no theme en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeFallbackVitroNemoDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array);
|
||||
assertEquals("testkey Vitro no theme de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeFallbackVIVONemoEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey VIVO no theme en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeFallbackVIVONemoDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey", array);
|
||||
assertEquals("testkey VIVO no theme de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppFallbackVIVONemoEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey_app_fallback", array);
|
||||
assertEquals("testkey_app_fallback Vitro wilma en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppFallbackVIVONemoDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey_app_fallback", array);
|
||||
assertEquals("testkey_app_fallback Vitro wilma de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppAndThemeFallbackVIVONemoEnUS() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey_app_fallback", array);
|
||||
assertEquals("testkey_app_fallback Vitro no theme en-US", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppAndThemeFallbackVIVONemoDeDE() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, NEMO, VIVO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("de-DE"), "testkey_app_fallback", array);
|
||||
assertEquals("testkey_app_fallback Vitro no theme de-DE", translation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCache() throws FileNotFoundException {
|
||||
init(TRANSLATIONS_N3_FILE, WILMA, VITRO);
|
||||
Object array[] = {};
|
||||
String translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey Vitro wilma en-US", translation);
|
||||
tp.application = VIVO;
|
||||
translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey Vitro wilma en-US", translation);
|
||||
tp.clearCache();
|
||||
translation = tp.getTranslation(Collections.singletonList("en-US"), "testkey", array);
|
||||
assertEquals("testkey VIVO wilma en-US", translation);
|
||||
}
|
||||
}
|
|
@ -51,14 +51,11 @@ public class I18nStub extends I18n {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected I18nBundle getBundle(String bundleName, HttpServletRequest req) {
|
||||
return new I18nBundleStub(bundleName);
|
||||
protected I18nBundle getBundle( HttpServletRequest req) {
|
||||
return new I18nBundleStub();
|
||||
}
|
||||
|
||||
private class I18nBundleStub extends I18nBundle {
|
||||
public I18nBundleStub(String bundleName) {
|
||||
super(bundleName, new DummyResourceBundle(), null);
|
||||
}
|
||||
private class I18nBundleStub implements I18nBundle {
|
||||
|
||||
@Override
|
||||
public String text(String key, Object... parameters) {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663600>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "value from n3 file"@en-US ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"VIVO" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"property_to_overwrite" .
|
|
@ -0,0 +1 @@
|
|||
test_key_all = test value all
|
|
@ -0,0 +1 @@
|
|||
test_key_all_en_CA = test value all_en_CA
|
|
@ -0,0 +1 @@
|
|||
test_key = test value vitro_all
|
|
@ -0,0 +1,2 @@
|
|||
test_key = test value vivo_all_en_US
|
||||
property_to_overwrite = value from properties file
|
|
@ -0,0 +1 @@
|
|||
test_key_all_en_US = test value customprefix_all_en_US
|
|
@ -0,0 +1 @@
|
|||
test_key_all = test value all wilma
|
|
@ -0,0 +1 @@
|
|||
test_key_all_en_CA = test value all_en_CA wilma
|
|
@ -0,0 +1 @@
|
|||
test_key_all_en_US = test value vivo_all_en_US wilma
|
|
@ -0,0 +1,77 @@
|
|||
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663600>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey VIVO no theme en-US"@en-US ;
|
||||
rdfs:label "testkey VIVO no theme de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"VIVO" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663601>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey Vitro no theme en-US"@en-US ;
|
||||
rdfs:label "testkey Vitro no theme de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"Vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663602>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey VIVO wilma en-US"@en-US ;
|
||||
rdfs:label "testkey VIVO wilma de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"VIVO" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme>
|
||||
"wilma" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663603>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey Vitro wilma en-US"@en-US ;
|
||||
rdfs:label "testkey Vitro wilma de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"Vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme>
|
||||
"wilma" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663604>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey Vitro vitro en-US"@en-US ;
|
||||
rdfs:label "testkey Vitro vitro de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"Vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme>
|
||||
"vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663605>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey_app_fallback Vitro wilma en-US"@en-US ;
|
||||
rdfs:label "testkey_app_fallback Vitro wilma de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"Vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasTheme>
|
||||
"wilma" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey_app_fallback" .
|
||||
|
||||
<urn:uuid:8c80dbf5-adda-41d5-a6fe-d5efde663606>
|
||||
a owl:NamedIndividual , <http://vivoweb.org/ontology/core/properties/vocabulary#PropertyKey> ;
|
||||
rdfs:label "testkey_app_fallback Vitro no theme en-US"@en-US ;
|
||||
rdfs:label "testkey_app_fallback Vitro no theme de-DE"@de-DE ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasApp>
|
||||
"Vitro" ;
|
||||
<http://vivoweb.org/ontology/core/properties/vocabulary#hasKey>
|
||||
"testkey_app_fallback" .
|
||||
|
||||
|
|
@ -52,6 +52,7 @@ edu.ucsf.vitro.opensocial.OpenSocialSmokeTests
|
|||
|
||||
# For multiple language support
|
||||
edu.cornell.mannlib.vitro.webapp.i18n.selection.LocaleSelectionSetup
|
||||
edu.cornell.mannlib.vitro.webapp.i18n.I18nContextListener
|
||||
|
||||
# The search indexer uses a "public" permission, so the PropertyRestrictionPolicyHelper
|
||||
# and the PermissionRegistry must already be set up.
|
||||
|
|
Loading…
Add table
Reference in a new issue