From f0f5c5d74452f5ad9166ed3a6a50bb4aa54d8786 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 26 Mar 2026 15:13:47 +0800 Subject: [PATCH 1/6] Implemented ICU4J Added ParserExceptionBuilder class for easy construction using named parameters. --- common/build.gradle | 4 +- .../net/rptools/maptool/language/I18N.java | 140 ++++++++++++------ gradle/libs.versions.toml | 2 +- .../exceptions/ParserExceptionBuilder.java | 90 +++++++++++ .../rptools/maptool/language/i18n.properties | 22 ++- 5 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java diff --git a/common/build.gradle b/common/build.gradle index 2ad0f14070..36c38d4a8b 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -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) diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index 2e2a429ac9..11b30bfcd1 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -14,16 +14,15 @@ */ 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.annotation.Nullable; +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; @@ -45,11 +44,13 @@ 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 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); } @@ -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 ("&") the character @@ -91,11 +81,15 @@ public static String getDescription(String key) { * @param key the component to search for * @return the character to use as the mnemonic (as an int) */ - 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)); } @@ -103,7 +97,18 @@ public static int getMnemonic(String key) { } /** - * 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. @@ -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 null @@ -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 - * action.loadMap 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 action.loadMap 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 @@ -150,32 +155,81 @@ 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 i18n.properties 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 &, e.g. + * &lt;div&gt; for <div>, or returning a false positive for a + * mnemonic key, we need to replace entities with their actual characters first. + */ + private static final BiFunction 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); + } + + /** + * 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()); + } + try { + return MessageFormat.format(getText(key), namedArgs); + } catch (IllegalArgumentException iae) { + log.error(iae.getMessage(), iae); + return ""; + } } /** - * Set all of the I18N values on an Action by retrieving said values from the - * properties file. + * 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<>()); + } + + + /** + * 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 +280,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()) { 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/ParserExceptionBuilder.java b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java new file mode 100644 index 0000000000..f541aa5062 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java @@ -0,0 +1,90 @@ +package net.rptools.maptool.client.functions.exceptions; + +import com.google.gson.JsonElement; +import net.rptools.maptool.language.I18N; +import net.rptools.parser.ParserException; +import org.apache.commons.lang3.tuple.Pair; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ParserExceptionBuilder { + private ParserException parserException; + private Throwable throwable = null; + private List> messageParams; + private static ParserExceptionBuilder instance; + private String msgKey = null; + private ParserExceptionBuilder() {} + private void checkInitialised(){ + if(instance == null){ + start(); + } + } + public ParserException build() { + 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 ParserExceptionBuilder start(){ + return start(null); + } + public static ParserExceptionBuilder start(String i18nKey){ + instance = new ParserExceptionBuilder(); + instance.messageParams = new ArrayList<>(); + instance.throwable = null; + instance.msgKey = i18nKey; + return instance; + } + public ParserExceptionBuilder forThrowable(final Throwable cause) { + checkInitialised(); + throwable = cause; + return this; + } + public ParserExceptionBuilder functionName(final String functionName){ + checkInitialised(); + messageParams.add(Pair.of("functionName", functionName)); + return this; + } + public ParserExceptionBuilder i18nKey(final String i18nKey){ + checkInitialised(); + instance.msgKey = i18nKey; + return this; + } + public ParserExceptionBuilder parameterIndex(final int parameterIndex){ + checkInitialised(); + messageParams.add(Pair.of("parameterIndex", parameterIndex)); + return this; + } + public ParserExceptionBuilder options(String options){ + checkInitialised(); + messageParams.add(Pair.of("options", options)); + return this; + } + public ParserExceptionBuilder results(String results){ + checkInitialised(); + messageParams.add(Pair.of("results", results)); + return this; + } + public ParserExceptionBuilder parameterValue(Object parameterValue){ + checkInitialised(); + if(parameterValue instanceof JsonElement je){ + parameterValue = je.toString(); + } else if(parameterValue instanceof List list){ + parameterValue = Arrays.deepToString(list.toArray()); + } else if(parameterValue instanceof Object[] array){ + parameterValue = Arrays.deepToString(array); + } + messageParams.add(Pair.of("parameterValue", parameterValue)); + return this; + } + + + +} diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index eee02f0da2..f8c084aecb 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 From 31078c18e9bc3176f1d5185398b51a03f5912249 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 26 Mar 2026 15:29:30 +0800 Subject: [PATCH 2/6] Streamlining and formatting --- .../net/rptools/maptool/language/I18N.java | 11 +- .../exceptions/ParserExceptionBuilder.java | 167 ++++++++++-------- 2 files changed, 99 insertions(+), 79 deletions(-) diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index 11b30bfcd1..db388e50da 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -19,7 +19,6 @@ import java.util.*; import java.util.function.BiFunction; import java.util.regex.Pattern; -import javax.annotation.Nullable; import javax.swing.*; import net.rptools.lib.OsDetection; import org.apache.commons.lang3.tuple.Pair; @@ -198,13 +197,15 @@ public static String getText(String key, Object... args) { } /** - * 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. + * 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) { + public static String getMessage(String key, List> namedArguments) { Map namedArgs = new HashMap<>(); for (Pair pair : namedArguments) { namedArgs.put(pair.getKey(), pair.getValue()); @@ -219,6 +220,7 @@ public static String getMessage( /** * Localised message with no argument substitution. + * * @param key The key to look up for the message. * @return The localised message text. */ @@ -226,7 +228,6 @@ public static String getMessage(String key) { return getMessage(key, new ArrayList<>()); } - /** * Set all the I18N values on an Action by retrieving said values from the properties * file. diff --git a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java index f541aa5062..4cc271a0da 100644 --- a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java +++ b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java @@ -1,90 +1,109 @@ +/* + * 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 com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import net.rptools.maptool.language.I18N; import net.rptools.parser.ParserException; import org.apache.commons.lang3.tuple.Pair; +public class ParserExceptionBuilder { + private Throwable throwable = null; + private List> messageParams; + private static ParserExceptionBuilder instance; + private String msgKey = null; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; + private ParserExceptionBuilder() {} -public class ParserExceptionBuilder { - private ParserException parserException; - private Throwable throwable = null; - private List> messageParams; - private static ParserExceptionBuilder instance; - private String msgKey = null; - private ParserExceptionBuilder() {} - private void checkInitialised(){ - if(instance == null){ - start(); - } - } - public ParserException build() { - 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")); - } + private void checkInitialised() { + if (instance == null) { + start(); } + } - public static ParserExceptionBuilder start(){ - return start(null); - } - public static ParserExceptionBuilder start(String i18nKey){ - instance = new ParserExceptionBuilder(); - instance.messageParams = new ArrayList<>(); - instance.throwable = null; - instance.msgKey = i18nKey; - return instance; - } - public ParserExceptionBuilder forThrowable(final Throwable cause) { - checkInitialised(); - throwable = cause; - return this; - } - public ParserExceptionBuilder functionName(final String functionName){ - checkInitialised(); - messageParams.add(Pair.of("functionName", functionName)); - return this; - } - public ParserExceptionBuilder i18nKey(final String i18nKey){ - checkInitialised(); - instance.msgKey = i18nKey; - return this; - } - public ParserExceptionBuilder parameterIndex(final int parameterIndex){ - checkInitialised(); - messageParams.add(Pair.of("parameterIndex", parameterIndex)); - return this; - } - public ParserExceptionBuilder options(String options){ - checkInitialised(); - messageParams.add(Pair.of("options", options)); - return this; - } - public ParserExceptionBuilder results(String results){ - checkInitialised(); - messageParams.add(Pair.of("results", results)); - return this; - } - public ParserExceptionBuilder parameterValue(Object parameterValue){ - checkInitialised(); - if(parameterValue instanceof JsonElement je){ - parameterValue = je.toString(); - } else if(parameterValue instanceof List list){ - parameterValue = Arrays.deepToString(list.toArray()); - } else if(parameterValue instanceof Object[] array){ - parameterValue = Arrays.deepToString(array); - } - messageParams.add(Pair.of("parameterValue", parameterValue)); - return this; + public ParserException build() { + 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 ParserExceptionBuilder start() { + return start(null); + } + + public static ParserExceptionBuilder start(String i18nKey) { + instance = new ParserExceptionBuilder(); + instance.messageParams = new ArrayList<>(); + instance.throwable = null; + instance.msgKey = i18nKey; + return instance; + } + public ParserExceptionBuilder forThrowable(final Throwable cause) { + checkInitialised(); + throwable = cause; + return this; + } + public ParserExceptionBuilder i18nKey(final String i18nKey) { + checkInitialised(); + instance.msgKey = i18nKey; + return this; + } + public ParserExceptionBuilder functionName(final String functionName) { + checkInitialised(); + return namedValue("functionName", functionName); + } + + public ParserExceptionBuilder parameterIndex(final int parameterIndex) { + checkInitialised(); + return namedValue("parameterIndex", parameterIndex); + } + + public ParserExceptionBuilder options(String options) { + checkInitialised(); + return namedValue("options", options); + } + + public ParserExceptionBuilder results(String results) { + checkInitialised(); + return namedValue("results", results); + } + + public ParserExceptionBuilder parameterValue(Object parameterValue) { + return namedValue("parameterValue", parameterValue); + } + + public ParserExceptionBuilder namedValue(final String name, Object value) { + checkInitialised(); + if (value instanceof JsonElement je) { + value = je.toString(); + } else if (value instanceof List list) { + value = Arrays.deepToString(list.toArray()); + } else if (value instanceof Object[] array) { + value = Arrays.deepToString(array); + } + messageParams.add(Pair.of(name, value)); + return this; + } } From eb783a85093a040c1bd54cc8e1ec2c9eaa3312a5 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 26 Mar 2026 16:30:54 +0800 Subject: [PATCH 3/6] Added getMessage(key, Map) --- .../net/rptools/maptool/language/I18N.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index db388e50da..519138886c 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -196,6 +196,17 @@ public static String getText(String key, Object... args) { 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: @@ -210,22 +221,24 @@ public static String getMessage(String key, List> namedArgu for (Pair pair : namedArguments) { namedArgs.put(pair.getKey(), pair.getValue()); } - try { - return MessageFormat.format(getText(key), namedArgs); - } catch (IllegalArgumentException iae) { - log.error(iae.getMessage(), iae); - return ""; - } + return getMessage(key, namedArgs); } - /** - * Localised message with no argument substitution. + * 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. - * @return The localised message text. + * @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) { - return getMessage(key, new ArrayList<>()); + public static String getMessage(String key, Map namedArguments) { + try { + return MessageFormat.format(getText(key), namedArguments); + } catch (IllegalArgumentException iae) { + log.error(iae.getMessage(), iae); + return ""; + } } /** From f909657eff9fb0c158ba5e05aa585f5bdcddad5f Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 26 Mar 2026 16:31:20 +0800 Subject: [PATCH 4/6] formatting --- common/src/main/java/net/rptools/maptool/language/I18N.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index 519138886c..00aff266e2 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -196,7 +196,6 @@ public static String getText(String key, Object... args) { return java.text.MessageFormat.format(getText(key), args); } - /** * Localised message with no argument substitution. * @@ -223,13 +222,15 @@ public static String getMessage(String key, List> namedArgu } return getMessage(key, namedArgs); } + /** * 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. + * @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) { From 4ba4aafee5d80cd2b9012b6014f7278863d9a398 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Fri, 27 Mar 2026 15:38:57 +0800 Subject: [PATCH 5/6] Message builder --- .../language/AbstractMessageBuilder.java | 54 +++++++++++++ .../net/rptools/maptool/language/I18N.java | 11 +++ .../exceptions/ParserExceptionBuilder.java | 77 +++++-------------- 3 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java diff --git a/common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java b/common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java new file mode 100644 index 0000000000..54cea12292 --- /dev/null +++ b/common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java @@ -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 + * and specifically the Affero license + * text at . + */ +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 { + protected final Map messageParams; + protected String msgKey; + + protected AbstractMessageBuilder(final String i18nKey) { + this.msgKey = i18nKey; + messageParams = new HashMap<>(); + } + + /** Persuade likely value types to something meaningful */ + protected Function 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); + } +} diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index 00aff266e2..68a69eced9 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -39,6 +39,7 @@ * * @author tcroft */ +@SuppressWarnings("unused") public class I18N { private static final ResourceBundle BUNDLE; private static final Logger log = LogManager.getLogger(I18N.class); @@ -304,4 +305,14 @@ public static List 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); + } + } } diff --git a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java index 4cc271a0da..b34777c475 100644 --- a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java +++ b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java @@ -14,29 +14,24 @@ */ package net.rptools.maptool.client.functions.exceptions; -import com.google.gson.JsonElement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import net.rptools.maptool.language.AbstractMessageBuilder; import net.rptools.maptool.language.I18N; import net.rptools.parser.ParserException; -import org.apache.commons.lang3.tuple.Pair; -public class ParserExceptionBuilder { - private Throwable throwable = null; - private List> messageParams; - private static ParserExceptionBuilder instance; - private String msgKey = null; +public class ParserExceptionBuilder extends AbstractMessageBuilder { + private Throwable throwable; - private ParserExceptionBuilder() {} + protected ParserExceptionBuilder(final String i18nKey) { + super(i18nKey); + throwable = null; + } - private void checkInitialised() { - if (instance == null) { - start(); - } + protected ParserExceptionBuilder(final Throwable cause) { + super(null); + throwable = cause; } - public ParserException build() { + public ParserException exception() { if (throwable != null) { return new ParserException(throwable); } else if (msgKey != null) { @@ -46,64 +41,32 @@ public ParserException build() { } } - public static ParserExceptionBuilder start() { - return start(null); - } - - public static ParserExceptionBuilder start(String i18nKey) { - instance = new ParserExceptionBuilder(); - instance.messageParams = new ArrayList<>(); - instance.throwable = null; - instance.msgKey = i18nKey; - return instance; + public static ParserExceptionBuilder forKey(String i18nKey) { + return new ParserExceptionBuilder(i18nKey); } public ParserExceptionBuilder forThrowable(final Throwable cause) { - checkInitialised(); throwable = cause; return this; } - public ParserExceptionBuilder i18nKey(final String i18nKey) { - checkInitialised(); - instance.msgKey = i18nKey; - return this; - } - public ParserExceptionBuilder functionName(final String functionName) { - checkInitialised(); - return namedValue("functionName", functionName); + return (ParserExceptionBuilder) namedValue("functionName", functionName); } public ParserExceptionBuilder parameterIndex(final int parameterIndex) { - checkInitialised(); - return namedValue("parameterIndex", parameterIndex); + return (ParserExceptionBuilder) namedValue("parameterIndex", parameterIndex); } - public ParserExceptionBuilder options(String options) { - checkInitialised(); - return namedValue("options", options); + public ParserExceptionBuilder parameterValue(Object parameterValue) { + return (ParserExceptionBuilder) namedValue("parameterValue", parameterValue); } public ParserExceptionBuilder results(String results) { - checkInitialised(); - return namedValue("results", results); - } - - public ParserExceptionBuilder parameterValue(Object parameterValue) { - return namedValue("parameterValue", parameterValue); + return (ParserExceptionBuilder) namedValue("results", results); } - public ParserExceptionBuilder namedValue(final String name, Object value) { - checkInitialised(); - if (value instanceof JsonElement je) { - value = je.toString(); - } else if (value instanceof List list) { - value = Arrays.deepToString(list.toArray()); - } else if (value instanceof Object[] array) { - value = Arrays.deepToString(array); - } - messageParams.add(Pair.of(name, value)); - return this; + public ParserExceptionBuilder options(String options) { + return (ParserExceptionBuilder) namedValue("options", options); } } From c268cfabbb14204d25c2ca531c2545bd3ac74fe1 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Fri, 3 Apr 2026 11:29:45 +0800 Subject: [PATCH 6/6] Changed class names from builders to factories --- ...ilder.java => AbstractMessageFactory.java} | 6 ++-- .../net/rptools/maptool/language/I18N.java | 8 ++--- ...ilder.java => ParserExceptionFactory.java} | 34 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) rename common/src/main/java/net/rptools/maptool/language/{AbstractMessageBuilder.java => AbstractMessageFactory.java} (90%) rename src/main/java/net/rptools/maptool/client/functions/exceptions/{ParserExceptionBuilder.java => ParserExceptionFactory.java} (56%) diff --git a/common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java b/common/src/main/java/net/rptools/maptool/language/AbstractMessageFactory.java similarity index 90% rename from common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java rename to common/src/main/java/net/rptools/maptool/language/AbstractMessageFactory.java index 54cea12292..e305a2dee3 100644 --- a/common/src/main/java/net/rptools/maptool/language/AbstractMessageBuilder.java +++ b/common/src/main/java/net/rptools/maptool/language/AbstractMessageFactory.java @@ -21,11 +21,11 @@ import java.util.Map; import java.util.function.Function; -public abstract class AbstractMessageBuilder { +public abstract class AbstractMessageFactory { protected final Map messageParams; protected String msgKey; - protected AbstractMessageBuilder(final String i18nKey) { + protected AbstractMessageFactory(final String i18nKey) { this.msgKey = i18nKey; messageParams = new HashMap<>(); } @@ -43,7 +43,7 @@ protected AbstractMessageBuilder(final String i18nKey) { return String.valueOf(value); }; - public AbstractMessageBuilder namedValue(final String name, final Object value) { + public AbstractMessageFactory namedValue(final String name, final Object value) { messageParams.put(name, stringify.apply(value)); return this; } diff --git a/common/src/main/java/net/rptools/maptool/language/I18N.java b/common/src/main/java/net/rptools/maptool/language/I18N.java index 68a69eced9..e82e3c2da7 100644 --- a/common/src/main/java/net/rptools/maptool/language/I18N.java +++ b/common/src/main/java/net/rptools/maptool/language/I18N.java @@ -306,13 +306,13 @@ public static List getMatchingKeys(Pattern regex) { return menuItemKeys; } - public static class MessageBuilder extends AbstractMessageBuilder { - protected MessageBuilder(String i18nKey) { + public static class MessageFactory extends AbstractMessageFactory { + protected MessageFactory(String i18nKey) { super(i18nKey); } - public static MessageBuilder forKey(String i18nKey) { - return new MessageBuilder(i18nKey); + public static MessageFactory forKey(String i18nKey) { + return new MessageFactory(i18nKey); } } } diff --git a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java similarity index 56% rename from src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java rename to src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java index b34777c475..a12d66fc91 100644 --- a/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionBuilder.java +++ b/src/main/java/net/rptools/maptool/client/functions/exceptions/ParserExceptionFactory.java @@ -14,19 +14,19 @@ */ package net.rptools.maptool.client.functions.exceptions; -import net.rptools.maptool.language.AbstractMessageBuilder; +import net.rptools.maptool.language.AbstractMessageFactory; import net.rptools.maptool.language.I18N; import net.rptools.parser.ParserException; -public class ParserExceptionBuilder extends AbstractMessageBuilder { +public class ParserExceptionFactory extends AbstractMessageFactory { private Throwable throwable; - protected ParserExceptionBuilder(final String i18nKey) { + protected ParserExceptionFactory(final String i18nKey) { super(i18nKey); throwable = null; } - protected ParserExceptionBuilder(final Throwable cause) { + protected ParserExceptionFactory(final Throwable cause) { super(null); throwable = cause; } @@ -41,32 +41,32 @@ public ParserException exception() { } } - public static ParserExceptionBuilder forKey(String i18nKey) { - return new ParserExceptionBuilder(i18nKey); + public static ParserExceptionFactory forKey(String i18nKey) { + return new ParserExceptionFactory(i18nKey); } - public ParserExceptionBuilder forThrowable(final Throwable cause) { + public ParserExceptionFactory forThrowable(final Throwable cause) { throwable = cause; return this; } - public ParserExceptionBuilder functionName(final String functionName) { - return (ParserExceptionBuilder) namedValue("functionName", functionName); + public ParserExceptionFactory functionName(final String functionName) { + return (ParserExceptionFactory) namedValue("functionName", functionName); } - public ParserExceptionBuilder parameterIndex(final int parameterIndex) { - return (ParserExceptionBuilder) namedValue("parameterIndex", parameterIndex); + public ParserExceptionFactory parameterIndex(final int parameterIndex) { + return (ParserExceptionFactory) namedValue("parameterIndex", parameterIndex); } - public ParserExceptionBuilder parameterValue(Object parameterValue) { - return (ParserExceptionBuilder) namedValue("parameterValue", parameterValue); + public ParserExceptionFactory parameterValue(Object parameterValue) { + return (ParserExceptionFactory) namedValue("parameterValue", parameterValue); } - public ParserExceptionBuilder results(String results) { - return (ParserExceptionBuilder) namedValue("results", results); + public ParserExceptionFactory results(String results) { + return (ParserExceptionFactory) namedValue("results", results); } - public ParserExceptionBuilder options(String options) { - return (ParserExceptionBuilder) namedValue("options", options); + public ParserExceptionFactory options(String options) { + return (ParserExceptionFactory) namedValue("options", options); } }