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"));