diff --git a/src/main/java/org/jabref/JabRefMain.java b/src/main/java/org/jabref/JabRefMain.java
index e57f22c69b6..be386b1dc8c 100644
--- a/src/main/java/org/jabref/JabRefMain.java
+++ b/src/main/java/org/jabref/JabRefMain.java
@@ -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
diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java
index 62465bdc147..13d1a83366b 100644
--- a/src/main/java/org/jabref/gui/JabRefFrame.java
+++ b/src/main/java/org/jabref/gui/JabRefFrame.java
@@ -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"),
diff --git a/src/main/java/org/jabref/logic/l10n/Localization.java b/src/main/java/org/jabref/logic/l10n/Localization.java
index 8fafc13b3ae..8e5ef7982d8 100644
--- a/src/main/java/org/jabref/logic/l10n/Localization.java
+++ b/src/main/java/org/jabref/logic/l10n/Localization.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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 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);
@@ -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 createLookupMap(ResourceBundle baseBundle) {
+ final ArrayList 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 lookup;
+
+ LocalizationBundle(HashMap 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 getKeys() {
+ return Collections.enumeration(lookup.keySet());
}
- return translate(menuTitles, "menu item", key, params);
- }
+ @Override
+ protected Set handleKeySet() {
+ return lookup.keySet();
+ }
+
+ @Override
+ public boolean containsKey(String key) {
+ return (key != null) && lookup.containsKey(key);
+ }
+ }
}
diff --git a/src/main/java/org/jabref/logic/l10n/LocalizationBundle.java b/src/main/java/org/jabref/logic/l10n/LocalizationBundle.java
deleted file mode 100644
index 35250cf6e06..00000000000
--- a/src/main/java/org/jabref/logic/l10n/LocalizationBundle.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.jabref.logic.l10n;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Objects;
-import java.util.ResourceBundle;
-import java.util.stream.Collectors;
-
-/**
- * A bundle containing localized strings.
- * It wraps an ordinary resource bundle and performs escaping/unescaping of keys and values similar to
- * {@link Localization}. Needed to support JavaFX inline binding.
- */
-public class LocalizationBundle extends ResourceBundle {
-
- private final ResourceBundle baseBundle;
-
- public LocalizationBundle(ResourceBundle baseBundle) {
- this.baseBundle = Objects.requireNonNull(baseBundle);
- }
-
- @Override
- protected Object handleGetObject(String key) {
- return Localization.translate(baseBundle, "message", key);
- }
-
- @Override
- public Enumeration getKeys() {
- ArrayList baseKeys = Collections.list(baseBundle.getKeys());
- List unescapedKeys = baseKeys.stream().map(key -> new LocalizationKey(key).getTranslationValue())
- .collect(Collectors.toList());
- return Collections.enumeration(unescapedKeys);
- }
-}
diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java
index b225120cb9c..6bbd42f0710 100644
--- a/src/main/java/org/jabref/preferences/JabRefPreferences.java
+++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java
@@ -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"));