Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rename to AbstractMessageFactory as its more of a factory than a builder :)

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