Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix location bundle with fast access #3327

Merged
merged 5 commits into from
Oct 20, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 1 addition & 6 deletions src/main/java/org/jabref/gui/maintable/MainTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -537,12 +537,7 @@ public boolean isFileColumn(int modelIndex) {
}

private boolean matches(int row, Matcher<BibEntry> m) {
Optional<BibEntry> bibEntry = getBibEntry(row);

if (bibEntry.isPresent()) {
return m.matches(bibEntry.get());
}
return m.matches(null);
return getBibEntry(row).map(m::matches).orElse(false);
}

private boolean isComplete(int row) {
Expand Down
197 changes: 151 additions & 46 deletions src/main/java/org/jabref/logic/l10n/Localization.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,94 @@
package org.jabref.logic.l10n;

import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;

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

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
* 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";
static final String RESOURCE_PREFIX = "l10n/JabRef";
static final String MENU_RESOURCE_PREFIX = "l10n/Menu";
public static final String BIBTEX = "BibTeX";

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,53 +101,105 @@ 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));
// Just for the case something really stupid happened.
if ((messages == null) || (menuTitles == null)) {
LOGGER.error("Could not load language translation files in " + Localization.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark regarding the class files as by @Siedlerchr above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

throw new NullPointerException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think throwing an IllegalStateException would be better here. Just for the more descriptive exception type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used

        Objects.requireNonNull(messages, "Could not load " + RESOURCE_PREFIX + " resource.");
        Objects.requireNonNull(menuTitles, "Could not load " + MENU_RESOURCE_PREFIX + " resource.");

which throws NPE, but gives a description.

}
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());
}

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

@Override
public boolean containsKey(String key) {
return (key != null) && lookup.containsKey(key);
}
return translate(menuTitles, "menu item", key, params);
}

}
Expand Down
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