Skip to content

Commit

Permalink
Fix location bundle with fast access (#3327)
Browse files Browse the repository at this point in the history
* Made the LocalizationBundle a private class of Localization and ensured, that all accesses to it are only lookups in hashmaps

Made all access of Location-messages a fast hash table lookup.
Fixed one message in JabRefFrame that used an escaped key instead of the unescaped version.

* Fix NPE in MainTable (#3318)

* Fix NPE in MainTable

* Fix build

* Use of HashMap instead of Hashtable
General code cleanup

* Forgot to replace one null test

* Reordering of import statements and fields
Replacement of last null test with Objects.requireNonNull
  • Loading branch information
Patrick Scheibe authored and lenhard committed Oct 20, 2017
1 parent ffab994 commit 1df1612
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 79 deletions.
4 changes: 3 additions & 1 deletion src/main/java/org/jabref/JabRefMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ private static void start(String[] args) {

Globals.prefs = preferences;
Globals.startBackgroundTasks();
Localization.setLanguage(preferences.get(JabRefPreferences.LANGUAGE));

// Note that the language was already set during the initialization of the preferences and it is safe to
// call the next function.
Globals.prefs.setLanguageDependentDefaultValues();

// Perform Migrations
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/jabref/gui/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ public class JabRefFrame extends JFrame implements OutputPrinter {
Globals.getKeyPrefs().getKey(KeyBinding.OPEN_CONSOLE),
IconTheme.JabRefIcon.CONSOLE.getIcon());
private final AbstractAction pullChangesFromSharedDatabase = new GeneralAction(Actions.PULL_CHANGES_FROM_SHARED_DATABASE,
Localization.menuTitle("Pull_changes_from_shared_database"),
Localization.lang("Pull_changes_from_shared_database"),
Localization.menuTitle("Pull changes from shared database"),
Localization.lang("Pull changes from shared database"),
Globals.getKeyPrefs().getKey(KeyBinding.PULL_CHANGES_FROM_SHARED_DATABASE),
IconTheme.JabRefIcon.PULL.getIcon());
private final AbstractAction mark = new GeneralAction(Actions.MARK_ENTRIES, Localization.menuTitle("Mark entries"),
Expand Down
191 changes: 151 additions & 40 deletions src/main/java/org/jabref/logic/l10n/Localization.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,103 @@
package org.jabref.logic.l10n;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* Provides handling for messages and menu entries in the preferred language of the user.
* <p>
* Notes: All messages and menu-entries in JabRef are stored in escaped form like "This_is_a_message". This message
* serves as key inside the {@link l10n} properties files that hold the translation for many languages. When a message
* is accessed, it needs to be unescaped and possible parameters that can appear in a message need to be filled with
* values.
* <p>
* This implementation loads the appropriate language by importing all keys/values from the correct bundle and stores
* them in unescaped form inside a {@link LocalizationBundle} which provides fast access because it caches the key-value
* pairs.
* <p>
* The access to this is given by the functions {@link Localization#lang(String, String...)} and {@link
* Localization#menuTitle(String, String...)} that developers should use whenever they use strings for the e.g. GUI that
* need to be translatable.
*/
public class Localization {
public static final String RESOURCE_PREFIX = "l10n/JabRef";
public static final String MENU_RESOURCE_PREFIX = "l10n/Menu";
public static final String BIBTEX = "BibTeX";
static final String RESOURCE_PREFIX = "l10n/JabRef";
static final String MENU_RESOURCE_PREFIX = "l10n/Menu";

private static final Log LOGGER = LogFactory.getLog(Localization.class);

private static ResourceBundle messages;
private static ResourceBundle menuTitles;
private static Locale locale;
private static LocalizationBundle localizedMessages;
private static LocalizationBundle localizedMenuTitles;

private Localization() {
}

public static LocalizationBundle getMessages() {
return new LocalizationBundle(messages);
/**
* Public access to all messages that are not menu-entries
*
* @param key The key of the message in unescaped form like "All fields"
* @param params Replacement strings for parameters %0, %1, etc.
* @return The message with replaced parameters
*/
public static String lang(String key, String... params) {
if (localizedMessages == null) {
// I'm logging this because it should never happen
LOGGER.error("Messages are not initialized.");
setLanguage("en");
}
return lookup(localizedMessages, "message", key, params);
}

/**
* Public access to menu entry messages
*
* @param key The key of the message in unescaped form like "Save all"
* @param params Replacement strings for parameters %0, %1, etc.
* @return The message with replaced parameters
*/
public static String menuTitle(String key, String... params) {
if (localizedMenuTitles == null) {
// I'm logging this because it should never happen
LOGGER.error("Menu entries are not initialized");
setLanguage("en");
}
return lookup(localizedMenuTitles, "menu item", key, params);
}

/**
* Sets the language and loads the appropriate translations. Note, that this function should be called before any
* other function of this class.
*
* @param language Language identifier like "en", "de", etc.
*/
public static void setLanguage(String language) {
Optional<Locale> knownLanguage = Languages.convertToSupportedLocale(language);
final Locale defaultLocale = Locale.getDefault();
if (!knownLanguage.isPresent()) {
LOGGER.warn("Language " + language + " is not supported by JabRef (Default:" + Locale.getDefault() + ")");
LOGGER.warn("Language " + language + " is not supported by JabRef (Default:" + defaultLocale + ")");
setLanguage("en");
return;
}

Locale locale = knownLanguage.get();
// avoid reinitialization of the language bundles
final Locale langLocale = knownLanguage.get();
if ((locale != null) && locale.equals(langLocale) && locale.equals(defaultLocale)) {
return;
}
locale = langLocale;
Locale.setDefault(locale);
javax.swing.JComponent.setDefaultLocale(locale);

Expand All @@ -48,54 +110,103 @@ public static void setLanguage(String language) {
}
}

/**
* Public access to the messages bundle for classes like AbstractView.
*
* @return The internally cashed bundle.
*/
public static LocalizationBundle getMessages() {
// avoid situations where this function is called before any language was set
if (locale == null) {
setLanguage("en");
}
return localizedMessages;
}

/**
* Creates and caches the language bundles used in JabRef for a particular language. This function first loads
* correct version of the "escaped" bundles that are given in {@link l10n}. After that, it stores the unescaped
* version in a cached {@link LocalizationBundle} for fast access.
*
* @param locale Localization to use.
*/
private static void createResourceBundles(Locale locale) {
messages = ResourceBundle.getBundle(RESOURCE_PREFIX, locale, new EncodingControl(StandardCharsets.UTF_8));
menuTitles = ResourceBundle.getBundle(MENU_RESOURCE_PREFIX, locale, new EncodingControl(StandardCharsets.UTF_8));
ResourceBundle messages = ResourceBundle.getBundle(RESOURCE_PREFIX, locale, new EncodingControl(StandardCharsets.UTF_8));
ResourceBundle menuTitles = ResourceBundle.getBundle(MENU_RESOURCE_PREFIX, locale, new EncodingControl(StandardCharsets.UTF_8));
Objects.requireNonNull(messages, "Could not load " + RESOURCE_PREFIX + " resource.");
Objects.requireNonNull(menuTitles, "Could not load " + MENU_RESOURCE_PREFIX + " resource.");
localizedMessages = new LocalizationBundle(createLookupMap(messages));
localizedMenuTitles = new LocalizationBundle(createLookupMap(menuTitles));
}

/**
* In the translation, %0, ..., %9 is replaced by the respective params given
* Helper function to create a HashMap from the key/value pairs of a bundle.
*
* @param resBundle the ResourceBundle to use
* @param idForErrorMessage output when translation is not found
* @param key the key to lookup in resBundle
* @param params a list of Strings to replace %0, %1, ...
* @return
* @param baseBundle JabRef language bundle with keys and values for translations.
* @return Lookup map for the baseBundle.
*/
protected static String translate(ResourceBundle resBundle, String idForErrorMessage, String key, String... params) {
Objects.requireNonNull(resBundle);
private static HashMap<String, String> createLookupMap(ResourceBundle baseBundle) {
final ArrayList<String> baseKeys = Collections.list(baseBundle.getKeys());
return new HashMap<>(baseKeys.stream().collect(
Collectors.toMap(
key -> new LocalizationKey(key).getTranslationValue(),
key -> new LocalizationKey(baseBundle.getString(key)).getTranslationValue())
));
}

String translation = null;
try {
String propertiesKey = new LocalizationKey(key).getPropertiesKeyUnescaped();
translation = resBundle.getString(propertiesKey);
} catch (MissingResourceException ex) {
/**
* This looks up a key in the bundle and replaces parameters %0, ..., %9 with the respective params given. Note that
* the keys are the "unescaped" strings from the bundle property files.
*
* @param bundle The {@link LocalizationBundle} which means either {@link Localization#localizedMenuTitles}
* or {@link Localization#localizedMessages}.
* @param idForErrorMessage Identifier-string when the translation is not found.
* @param key The lookup key.
* @param params The parameters that should be inserted into the message
* @return The final message with replaced parameters.
*/
private static String lookup(LocalizationBundle bundle, String idForErrorMessage, String key, String... params) {
Objects.requireNonNull(key);

String translation = bundle.containsKey(key) ? bundle.getString(key) : "";
if (translation.isEmpty()) {
LOGGER.warn("Warning: could not get " + idForErrorMessage + " translation for \"" + key + "\" for locale "
+ Locale.getDefault());
}
if ((translation == null) || translation.isEmpty()) {
LOGGER.warn("Warning: no " + idForErrorMessage + " translation for \"" + key + "\" for locale "
+ Locale.getDefault());

translation = key;
}

return new LocalizationKeyParams(translation, params).replacePlaceholders();
}

public static String lang(String key, String... params) {
if (messages == null) {
setLanguage("en");
/**
* A bundle for caching localized strings. Needed to support JavaFX inline binding.
*/
private static class LocalizationBundle extends ResourceBundle {

private final HashMap<String, String> lookup;

LocalizationBundle(HashMap<String, String> lookupMap) {
lookup = lookupMap;
}
return translate(messages, "message", key, params);
}

public static String menuTitle(String key, String... params) {
if (menuTitles == null) {
setLanguage("en");
public final Object handleGetObject(String key) {
Objects.requireNonNull(key);
return lookup.get(key);
}

@Override
public Enumeration<String> getKeys() {
return Collections.enumeration(lookup.keySet());
}
return translate(menuTitles, "menu item", key, params);
}

@Override
protected Set<String> handleKeySet() {
return lookup.keySet();
}

@Override
public boolean containsKey(String key) {
return (key != null) && lookup.containsKey(key);
}
}
}

36 changes: 0 additions & 36 deletions src/main/java/org/jabref/logic/l10n/LocalizationBundle.java

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/java/org/jabref/preferences/JabRefPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@ private JabRefPreferences() {
// load user preferences
prefs = Preferences.userNodeForPackage(PREFS_BASE_CLASS);

// Since some of the preference settings themselves use localized strings, we cannot set the language after
// the initialization of the preferences in main
// Otherwise that language framework will be instantiated and more importantly, statically initialized preferences
// like the SearchDisplayMode will never be translated.
Localization.setLanguage(prefs.get(LANGUAGE, "en"));

SearchPreferences.putDefaults(defaults);

defaults.put(TEXMAKER_PATH, JabRefDesktop.getNativeDesktop().detectProgramPath("texmaker", "Texmaker"));
Expand Down

0 comments on commit 1df1612

Please sign in to comment.