Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ dependencies {

implementation(libs.bundles.log4j)
implementation(libs.slf4j.simple)
implementation(libs.flexmark.all)
implementation(libs.apache.commons.logging)

implementation(libs.apache.commons.lang)
implementation(libs.icu4j)
implementation(libs.gson)

implementation(libs.bundles.imageio)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* This software Copyright by the RPTools.net development team, and
* licensed under the Affero GPL Version 3 or, at your option, any later
* version.
*
* MapTool Source Code is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU Affero General Public
* License * along with this source Code. If not, please visit
* <http://www.gnu.org/licenses/> and specifically the Affero license
* text at <http://www.gnu.org/licenses/agpl.html>.
*/
package net.rptools.maptool.language;

import com.google.gson.JsonElement;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public abstract class AbstractMessageBuilder {
Comment thread
bubblobill marked this conversation as resolved.
Outdated
protected final Map<String, Object> messageParams;
protected String msgKey;

protected AbstractMessageBuilder(final String i18nKey) {
this.msgKey = i18nKey;
messageParams = new HashMap<>();
}

/** Persuade likely value types to something meaningful */
protected Function<Object, String> stringify =
value -> {
if (value instanceof JsonElement je) {
return je.toString();
} else if (value instanceof List<?> list) {
return Arrays.deepToString(list.toArray());
} else if (value instanceof Object[] array) {
return Arrays.deepToString(array);
}
return String.valueOf(value);
};

public AbstractMessageBuilder namedValue(final String name, final Object value) {
messageParams.put(name, stringify.apply(value));
return this;
}

public String build() {
return I18N.getMessage(msgKey, messageParams);
}
}
166 changes: 123 additions & 43 deletions common/src/main/java/net/rptools/maptool/language/I18N.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
*/
package net.rptools.maptool.language;

import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import com.ibm.icu.text.MessageFormat;
import com.vladsch.flexmark.util.sequence.Escaping;
import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.*;
import net.rptools.lib.OsDetection;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand All @@ -41,15 +39,18 @@
*
* @author tcroft
*/
@SuppressWarnings("unused")
public class I18N {
private static final ResourceBundle BUNDLE;
private static final Logger log = LogManager.getLogger(I18N.class);
private static final String DESCRIPTION_EXTENSION = ".description";

private static final char MNEMONIC_MARKER = '&';
private static final Pattern MNEMONIC_PREFIX_PATTERN =
Pattern.compile(MNEMONIC_MARKER + "([a-z0-9])", Pattern.CASE_INSENSITIVE);
private static Enumeration<String> keys;

static {
// Put here to make breakpointing easier. :)
// Put here to make break-pointing easier. :)
BUNDLE = ResourceBundle.getBundle("net.rptools.maptool.language.i18n");
I18nTools report = new I18nTools(false);
}
Expand All @@ -72,17 +73,6 @@ public static JMenu createMenu(String key) {
return menu;
}

/**
* Returns the description text for the given key. This text normally appears in the statusbar of
* the main application frame. The input key has the string DESCRIPTION_EXTENSION appended to it.
*
* @param key the key to use for the i18n lookup.
* @return the i81n version of the string.
*/
public static String getDescription(String key) {
return getString(key + DESCRIPTION_EXTENSION);
}

/**
* Returns the character to use as the menu mnemonic for the given key. This method searches the
* properties file for the given key. If the value contains an ampersand ("&amp;") the character
Expand All @@ -91,19 +81,34 @@ public static String getDescription(String key) {
* @param key the component to search for
* @return the character to use as the mnemonic (as an <code>int</code>)
*/
public static int getMnemonic(String key) {
private static int getMnemonic(String key) {
String value = getString(key);
if (value == null || value.length() < 2) return -1;

int index = value.indexOf('&');
if (value == null || value.length() < 2) {
return -1;
}
// replace HTML entities with characters to prevent spurious results - should not happen but
// this is not Utopia
value = convertText.apply(value, false);
int index = value.indexOf(MNEMONIC_MARKER);
if (index != -1 && index + 1 < value.length()) {
return Character.toUpperCase(value.charAt(index + 1));
}
return -1;
}

/**
* Returns the String that results from a lookup within the properties file.
* Returns the description text for the given key. This text normally appears in the status-bar of
* the main application frame. The input key has the string DESCRIPTION_EXTENSION appended to it.
*
* @param key the key to use for the i18n lookup.
* @return the i81n version of the string.
*/
private static String getDescription(String key) {
return getString(key + DESCRIPTION_EXTENSION);
}

/**
* Returns the String matching the key within the properties file.
*
* @param key the component to search for
* @param bundle the resource bundle to get the i18n string from.
Expand All @@ -118,7 +123,7 @@ public static String getString(String key, ResourceBundle bundle) {
}

/**
* Returns the String that results from a lookup within the properties file.
* Returns the String matching the key within the properties file.
*
* @param key the component to search for
* @return the String found or <code>null</code>
Expand All @@ -132,9 +137,9 @@ public static String getString(String key) {
}

/**
* Returns the text associated with the given key after removing any menu mnemonic. So for the key
* <b>action.loadMap</b> that has the value {@code &Load Map} in the properties file, this method
* returns "Load Map".
* Returns the String matching the key within the properties file after removing any menu
* mnemonic. So for the key <b>action.loadMap</b> that has the value {@code &Load Map} in the
* properties file, this method returns "Load Map".
*
* @param key the component to search for
* @return the String found with mnemonics removed, or the input key if not found
Expand All @@ -150,32 +155,97 @@ public static String getText(String key) {
log.debug("Cannot find key '" + key + "' in properties file.");
return key;
}
return value.replaceAll("\\&", "");
// remove mnemonic marker and return value
return convertText.apply(value, true);
}

/**
* Functionally identical to {@link #getText(String key)} except that this one bundles the
* formatting calls into this code. This version of the method is truly only needed when the
* string being retrieved contains parameters. In MapTool, this commonly means the player's name
* or a filename. See the "Parameterized Strings" section of the <b>i18n.properties</b> file for
* example usage. Full documentation for this technique can be found under {@link
* MessageFormat#format}.
* To avoid breaking HTML encoded characters when dealing with &amp;, e.g. <code>
* &amp;lt;div&amp;gt;</code> for <code>&lt;div&gt;</code>, or returning a false positive for a
* mnemonic key, we need to replace entities with their actual characters first.
*/
private static final BiFunction<String, Boolean, String> convertText =
(string, stripAmpersand) -> {
if (string.indexOf(MNEMONIC_MARKER) == -1) {
return string;
} else {
string = Escaping.unescapeString(string);
if (stripAmpersand) {
return MNEMONIC_PREFIX_PATTERN.matcher(string).replaceAll("$1");
}
return Escaping.escapeHtml(string, false);
}
};

/**
* Simple functionality &ndash; similar to {@link #getText(String key)} &ndash; with simple
* indexed argument replacement. Use this version where the target string pattern contains
* placeholders in the form <code>{n}</code> where n is an integer.
*
* <p>See the "Parameterised Strings" section of the <b>i18n.properties</b> file for example
* usage. Full documentation for this technique can be found under {@link
* MessageFormat#format(String, Object...)}.
*
* @param key the <code>propertyKey</code> to use for lookup in the properties file
* @param args parameters needed for formatting purposes
* @param args parameters (in order) needed for formatting purposes
* @return the formatted String
*/
public static String getText(String key, Object... args) {
// If the key doesn't exist in the file, the key becomes the format and
// nothing will be substituted; it's a little extra work, but is not the normal case
// anyway.
String msg = MessageFormat.format(getText(key), args);
return msg;
return java.text.MessageFormat.format(getText(key), args);
}

/**
* Localised message with no argument substitution.
*
* @param key The key to look up for the message.
* @return The localised message text.
*/
public static String getMessage(String key) {
return getMessage(key, new ArrayList<>());
}

/**
* Message composition for use with named arguments. Use when the message pattern string contains
* field names, for example: <code>
* Argument at index {paramIndex} to function {functionName} is invalid.</code>
*
* @param key The key to look up for the message.
* @param namedArguments List of pairs containing the parameter name and the substitution value.
* @return Localised message with parameter placeholders replaced.
*/
public static String getMessage(String key, List<Pair<String, Object>> namedArguments) {
Map<String, Object> namedArgs = new HashMap<>();
for (Pair<String, Object> pair : namedArguments) {
namedArgs.put(pair.getKey(), pair.getValue());
}
return getMessage(key, namedArgs);
}

/**
* Set all of the I18N values on an <code>Action</code> by retrieving said values from the
* properties file.
* Message composition for use with named arguments. Use when the message pattern string contains
* field names, for example: <code>
* Argument at index {paramIndex} to function {functionName} is invalid.</code>
*
* @param key The key to look up for the message.
* @param namedArguments Map&lt;String,Object&gt; containing the parameter name and associated
* value.
* @return Localised message with parameter placeholders replaced.
*/
public static String getMessage(String key, Map<String, Object> namedArguments) {
try {
return MessageFormat.format(getText(key), namedArguments);
} catch (IllegalArgumentException iae) {
log.error(iae.getMessage(), iae);
return "";
}
}

/**
* Set all the I18N values on an <code>Action</code> by retrieving said values from the properties
* file.
*
* <p>Uses the <code>key</code> as the index for the properties file to set the <b>Action.NAME</b>
* field of <b>action</b>.
Expand Down Expand Up @@ -226,7 +296,7 @@ public static List<String> getMatchingKeys(String regex) {
public static List<String> getMatchingKeys(Pattern regex) {
Enumeration<String> keys = BUNDLE.getKeys();

List<String> menuItemKeys = new LinkedList<String>();
List<String> menuItemKeys = new LinkedList<>();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
if (regex.matcher(key).find()) {
Expand All @@ -235,4 +305,14 @@ public static List<String> getMatchingKeys(Pattern regex) {
}
return menuItemKeys;
}

public static class MessageBuilder extends AbstractMessageBuilder {
protected MessageBuilder(String i18nKey) {
super(i18nKey);
}

public static MessageBuilder forKey(String i18nKey) {
return new MessageBuilder(i18nKey);
}
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ apache-commons-logging = { group = "commons-logging", name = "commons-logging",
# For Sentry bug reporting
sentry = { group = "io.sentry", name = "sentry", version.ref = "sentry" }
sentry-log4j = { group = "io.sentry", name = "sentry-log4j2", version.ref = "sentry" }

icu4j = { group = "com.ibm.icu", name = "icu4j", version = "78.3" }
# Networking
# Web RTC
websocket = { group = "org.java-websocket", name = "Java-WebSocket", version = "1.6.0" }
Expand Down
Loading
Loading