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 – similar to {@link #getText(String key)} – with simple
+ * indexed argument replacement. Use this version where the target string pattern contains
+ * placeholders in the form {n} where n is an integer.
+ *
+ * See the "Parameterised Strings" section of the i18n.properties file for example
+ * usage. Full documentation for this technique can be found under {@link
+ * MessageFormat#format(String, Object...)}.
*
* @param key the propertyKey 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:
+ * Argument at index {paramIndex} to function {functionName} is invalid.
+ *
+ * @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> namedArguments) {
+ Map namedArgs = new HashMap<>();
+ for (Pair pair : namedArguments) {
+ namedArgs.put(pair.getKey(), pair.getValue());
+ }
+ return getMessage(key, namedArgs);
}
/**
- * Set all of the I18N values on an Action 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:
+ * Argument at index {paramIndex} to function {functionName} is invalid.
+ *
+ * @param key The key to look up for the message.
+ * @param namedArguments Map<String,Object> containing the parameter name and associated
+ * value.
+ * @return Localised message with parameter placeholders replaced.
+ */
+ public static String getMessage(String key, Map namedArguments) {
+ try {
+ return MessageFormat.format(getText(key), namedArguments);
+ } catch (IllegalArgumentException iae) {
+ log.error(iae.getMessage(), iae);
+ return "";
+ }
+ }
+
+ /**
+ * Set all the I18N values on an Action by retrieving said values from the properties
+ * file.
*
* Uses the key as the index for the properties file to set the Action.NAME
* field of action.
@@ -226,7 +296,7 @@ public static List getMatchingKeys(String regex) {
public static List getMatchingKeys(Pattern regex) {
Enumeration keys = BUNDLE.getKeys();
- List menuItemKeys = new LinkedList();
+ List menuItemKeys = new LinkedList<>();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
if (regex.matcher(key).find()) {
@@ -235,4 +305,14 @@ public static List getMatchingKeys(Pattern regex) {
}
return menuItemKeys;
}
+
+ public static class MessageFactory extends AbstractMessageFactory {
+ protected MessageFactory(String i18nKey) {
+ super(i18nKey);
+ }
+
+ public static MessageFactory forKey(String i18nKey) {
+ return new MessageFactory(i18nKey);
+ }
+ }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 353737ab63..bbb06c3776 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }
diff --git a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java
new file mode 100644
index 0000000000..a12d66fc91
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java
@@ -0,0 +1,72 @@
+/*
+ * 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
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.functions.exceptions;
+
+import net.rptools.maptool.language.AbstractMessageFactory;
+import net.rptools.maptool.language.I18N;
+import net.rptools.parser.ParserException;
+
+public class ParserExceptionFactory extends AbstractMessageFactory {
+ private Throwable throwable;
+
+ protected ParserExceptionFactory(final String i18nKey) {
+ super(i18nKey);
+ throwable = null;
+ }
+
+ protected ParserExceptionFactory(final Throwable cause) {
+ super(null);
+ throwable = cause;
+ }
+
+ public ParserException exception() {
+ if (throwable != null) {
+ return new ParserException(throwable);
+ } else if (msgKey != null) {
+ return new ParserException(I18N.getMessage(msgKey, messageParams));
+ } else {
+ return new ParserException(I18N.getMessage("macro.function.general.unknownError"));
+ }
+ }
+
+ public static ParserExceptionFactory forKey(String i18nKey) {
+ return new ParserExceptionFactory(i18nKey);
+ }
+
+ public ParserExceptionFactory forThrowable(final Throwable cause) {
+ throwable = cause;
+ return this;
+ }
+
+ public ParserExceptionFactory functionName(final String functionName) {
+ return (ParserExceptionFactory) namedValue("functionName", functionName);
+ }
+
+ public ParserExceptionFactory parameterIndex(final int parameterIndex) {
+ return (ParserExceptionFactory) namedValue("parameterIndex", parameterIndex);
+ }
+
+ public ParserExceptionFactory parameterValue(Object parameterValue) {
+ return (ParserExceptionFactory) namedValue("parameterValue", parameterValue);
+ }
+
+ public ParserExceptionFactory results(String results) {
+ return (ParserExceptionFactory) namedValue("results", results);
+ }
+
+ public ParserExceptionFactory options(String options) {
+ return (ParserExceptionFactory) namedValue("options", options);
+ }
+}
diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties
index 3f0d4167d9..ac6cb5f31d 100644
--- a/src/main/resources/net/rptools/maptool/language/i18n.properties
+++ b/src/main/resources/net/rptools/maptool/language/i18n.properties
@@ -1511,7 +1511,7 @@ action.gatherDebugInfo.description = Collects information about your
action.gridLineWight = Grid &Line Weight
# These are url menu actions. Each item consists of three keys. The first key is
# action.url.## which contains the displayed string. The second key is
-# action.url.##.description which contains the url to load when the action is
+# action.url.##.description which contains the url to load when the action is
# executed. The last key is action.url.##.icon which contains the image embedded
# inside the MapTool JAR file which appears on the menu item.
action.helpurl.discord = Discord
@@ -2066,7 +2066,7 @@ macro.function.addAllToInitiativeFunction.mustBeGM = Only the GM has the permiss
# MacroArgsFunctions
macro.function.args.incorrectParam = Function "{0}" must be called with exactly 1 numeric parameter.
macro.function.args.outOfRange = Argument index {1} out of range (max of {2}) in function "{0}".
-# assert Function
+# assert Function
# {0} is the error message specified when calling assert() for message.
# Note that I'm leaving off the double quotes on this one, too. I think it
# will look better that way.
@@ -2114,21 +2114,31 @@ macro.function.general.noImpersonated = Error executing "{0}": the
# enough that no thousands separator will be needed either. So leaving
# off the ",number" means they'll be treated as strings and simply
# output as-is. Which is fine. :)
+macro.function.general.unknownError = Unknown error
macro.function.general.reservedJS = {0} is a reserved function in the js. prefix.
macro.function.general.missingKey = Argument {1, number} in function {0} is missing the required key "{2}". Required keys are; "{3}"
+
+macro.function.general.onlyGM = Only the GM can call the "{0}" function.
macro.function.general.noPermJS = You do not have permission to use the "{0}" namespace.
macro.function.general.noPerm = You do not have permission to call the "{0}" function.
macro.function.general.noPermOther = You do not have permission to access another token in the "{0}" function.
+
macro.function.general.notEnoughParam = Function "{0}" requires at least {1} parameters; {2} were provided.
-macro.function.general.onlyGM = Only the GM can call the "{0}" function.
macro.function.general.tooManyParam = Function "{0}" requires no more than {1} parameters; {2} were provided.
+macro.function.general.wrongNumParam = Function "{0}" requires exactly {1} parameters; {2} were provided.
+macro.function.general.invalidParam = Argument at index {parameterIndex} to function {functionName} is invalid.
+macro.function.general.invalidParam.forContext = Argument at index {parameterIndex} to function {functionName} is invalid for {context}.
+macro.function.general.invalidParam.validValues = Argument at index {parameterIndex} to function {functionName} is invalid. Valid values are: {options}.
+macro.function.general.invalidParam.emptyList = Function "{0}": string list at argument {1} cannot be empty
+
macro.function.general.unknownFunction = Unknown function name "{0}".
macro.function.general.unknownToken = Error executing "{0}": the token name or id "{1}" is unknown.
macro.function.general.unknownPropertyType = Error executing "{0}": the property type "{1}" is unknown.
macro.function.general.unknownProperty = Error executing "{0}": the property "{1}" is unknown for type "{2}".
macro.function.general.unknownTokenOnMap = Error executing "{0}": the token name or id "{1}" is unknown on map "{2}".
-macro.function.general.wrongNumParam = Function "{0}" requires exactly {1} parameters; {2} were provided.
-macro.function.general.listCannotBeEmpty = {0}: string list at argument {1} cannot be empty
+
+macro.function.general.noUniqueResult = Function {functionName} could not find a unique match.
+
# Token Distance functions
# I.e. ONE_TWO_ONE or ONE_ONE_ONE
macro.function.getDistance.invalidMetric = Invalid metric type "{0}".
@@ -2577,7 +2587,7 @@ msg.title.importProperties = Import Properties
msg.title.loadAssetTree = Load Asset Tree
msg.title.loadCampaign = Load Campaign
msg.autosave.wait = Waiting up to {0} seconds for autosave to finish...
-# I'm trying to add some consistency to the property names. So...
+# I'm trying to add some consistency to the property names. So...
# "msg.title.*" are strings used as the titles of dialogs and frames
# created by the application.
msg.title.loadMap = Load Map