1414 */
1515package net .rptools .maptool .language ;
1616
17- import java .text .MessageFormat ;
18- import java .util .Enumeration ;
19- import java .util .LinkedList ;
20- import java .util .List ;
21- import java .util .MissingResourceException ;
22- import java .util .ResourceBundle ;
17+ import com .ibm .icu .text .MessageFormat ;
18+ import com .vladsch .flexmark .util .sequence .Escaping ;
19+ import java .util .*;
2320import java .util .regex .Pattern ;
24- import javax .swing .Action ;
25- import javax .swing .JMenu ;
21+ import javax .swing .*;
2622import net .rptools .lib .OsDetection ;
23+ import org .apache .commons .lang3 .tuple .Pair ;
2724import org .apache .logging .log4j .LogManager ;
2825import org .apache .logging .log4j .Logger ;
2926
3532 *
3633 * <p>As MapTool uses a base name for the string and extensions for alternate pieces (such as <code>
3734 * action.loadMap</code> as the base and <code>action.loadMap.accel</code> as the menu accelerator
38- * key) there are different methods used to return the different components.
35+ * key) different methods are used to return the different components.
3936 *
4037 * <p>The ResourceBundle name is <b>net.rptools.maptool.language.i18n</b>.
4138 *
4239 * @author tcroft
4340 */
41+ @ SuppressWarnings ("unused" )
4442public class I18N {
4543 private static final ResourceBundle BUNDLE ;
4644 private static final Logger log = LogManager .getLogger (I18N .class );
4745 private static final String DESCRIPTION_EXTENSION = ".description" ;
48-
46+ private static final char MNEMONIC_MARKER = '&' ;
47+ private static final Pattern MNEMONIC_PREFIX_PATTERN =
48+ Pattern .compile (MNEMONIC_MARKER + "([a-z0-9])" , Pattern .CASE_INSENSITIVE );
4949 private static Enumeration <String > keys ;
5050
5151 static {
52- // Put here to make breakpointing easier. :)
52+ // Put here to make break-pointing easier. :)
5353 BUNDLE = ResourceBundle .getBundle ("net.rptools.maptool.language.i18n" );
5454 I18nTools report = new I18nTools (false );
5555 }
@@ -72,38 +72,42 @@ public static JMenu createMenu(String key) {
7272 return menu ;
7373 }
7474
75- /**
76- * Returns the description text for the given key. This text normally appears in the statusbar of
77- * the main application frame. The input key has the string DESCRIPTION_EXTENSION appended to it.
78- *
79- * @param key the key to use for the i18n lookup.
80- * @return the i81n version of the string.
81- */
82- public static String getDescription (String key ) {
83- return getString (key + DESCRIPTION_EXTENSION );
84- }
85-
8675 /**
8776 * Returns the character to use as the menu mnemonic for the given key. This method searches the
88- * properties file for the given key. If the value contains an ampersand ("&") the character
89- * following the ampersand is converted to uppercase and returned.
77+ * properties file for the given key. Where the value contains an ampersand ("&") the
78+ * following character is converted to uppercase and returned.
9079 *
9180 * @param key the component to search for
9281 * @return the character to use as the mnemonic (as an <code>int</code>)
9382 */
94- public static int getMnemonic (String key ) {
83+ private static int getMnemonic (String key ) {
9584 String value = getString (key );
96- if (value == null || value .length () < 2 ) return -1 ;
97-
98- int index = value .indexOf ('&' );
85+ if (value == null || value .length () < 2 ) {
86+ return -1 ;
87+ }
88+ // replace HTML entities with characters to prevent spurious results - should not happen but
89+ // this is not Utopia
90+ value = replaceHtmlEntities (value , false );
91+ int index = value .indexOf (MNEMONIC_MARKER );
9992 if (index != -1 && index + 1 < value .length ()) {
10093 return Character .toUpperCase (value .charAt (index + 1 ));
10194 }
10295 return -1 ;
10396 }
10497
10598 /**
106- * Returns the String that results from a lookup within the properties file.
99+ * Returns the description text for the given key. This text normally appears in the status-bar of
100+ * the main application frame. The input key has the string DESCRIPTION_EXTENSION appended to it.
101+ *
102+ * @param key the key to use for the i18n lookup.
103+ * @return the i81n version of the string.
104+ */
105+ private static String getDescription (String key ) {
106+ return getString (key + DESCRIPTION_EXTENSION );
107+ }
108+
109+ /**
110+ * Returns the String matching the key within the properties file.
107111 *
108112 * @param key the component to search for
109113 * @param bundle the resource bundle to get the i18n string from.
@@ -118,7 +122,7 @@ public static String getString(String key, ResourceBundle bundle) {
118122 }
119123
120124 /**
121- * Returns the String that results from a lookup within the properties file.
125+ * Returns the String matching the key within the properties file.
122126 *
123127 * @param key the component to search for
124128 * @return the String found or <code>null</code>
@@ -132,9 +136,9 @@ public static String getString(String key) {
132136 }
133137
134138 /**
135- * Returns the text associated with the given key after removing any menu mnemonic. So for the key
136- * <b>action.loadMap</b> that has the value {@code &Load Map} in the properties file, this method
137- * returns "Load Map".
139+ * Returns the String matching the key within the properties file after removing any menu
140+ * mnemonic. So for the key <b>action.loadMap</b> that has the value {@code &Load Map} in the
141+ * properties file, this method returns "Load Map".
138142 *
139143 * @param key the component to search for
140144 * @return the String found with mnemonics removed, or the input key if not found
@@ -147,50 +151,114 @@ public static String getText(String key) {
147151
148152 String value = getString (key );
149153 if (value == null ) {
150- log .debug ("Cannot find key '" + key + " ' in properties file." );
154+ log .debug ("Cannot find key '{} ' in properties file." , key );
151155 return key ;
152156 }
153- return value .replaceAll ("\\ &" , "" );
157+ // remove mnemonic marker and return value
158+ return replaceHtmlEntities (value , true );
154159 }
155160
156161 /**
157- * Functionally identical to {@link #getText(String key)} except that this one bundles the
158- * formatting calls into this code. This version of the method is truly only needed when the
159- * string being retrieved contains parameters. In MapTool, this commonly means the player's name
160- * or a filename. See the "Parameterized Strings" section of the <b>i18n.properties</b> file for
161- * example usage. Full documentation for this technique can be found under {@link
162- * MessageFormat#format}.
162+ * To avoid breaking HTML encoded characters when dealing with &, e.g. <code>
163+ * &lt;div&gt;</code> for <code><div></code>, or returning a false positive for a
164+ * mnemonic key, we need to replace entities with their actual characters first.
165+ */
166+ private static String replaceHtmlEntities (String string , boolean stripAmpersand ) {
167+ if (string .indexOf (MNEMONIC_MARKER ) == -1 ) {
168+ return string ;
169+ } else {
170+ string = Escaping .unescapeString (string );
171+ if (stripAmpersand ) {
172+ return MNEMONIC_PREFIX_PATTERN .matcher (string ).replaceAll ("$1" );
173+ }
174+ return Escaping .escapeHtml (string , false );
175+ }
176+ }
177+
178+ /**
179+ * Simple functionality to {@link #getText(String key)} using indexed argument replacement. Use
180+ * this version where the target string pattern contains placeholders in the form <code>{n}</code>
181+ * where n is an integer.
182+ *
183+ * <p>See the "Parameterised Strings" section of the <b>i18n.properties</b> file for example
184+ * usage. Full documentation for this technique can be found under {@link
185+ * MessageFormat#format(String, Object...)}.
163186 *
164187 * @param key the <code>propertyKey</code> to use for lookup in the properties file
165- * @param args parameters needed for formatting purposes
188+ * @param args parameters (in order) needed for formatting purposes
166189 * @return the formatted String
167190 */
168191 public static String getText (String key , Object ... args ) {
169192 // If the key doesn't exist in the file, the key becomes the format and
170193 // nothing will be substituted; it's a little extra work, but is not the normal case
171194 // anyway.
172- String msg = MessageFormat .format (getText (key ), args );
173- return msg ;
195+ return java .text .MessageFormat .format (getText (key ), args );
196+ }
197+
198+ /**
199+ * Localised message with no argument substitution.
200+ *
201+ * @param key The key to look up for the message.
202+ * @return The localised message text.
203+ */
204+ public static String getMessage (String key ) {
205+ return getMessage (key , new ArrayList <>());
206+ }
207+
208+ /**
209+ * Message composition for use with named arguments. Use when the message pattern string contains
210+ * field names, for example: <code>
211+ * Argument at index {paramIndex} to function {functionName} is invalid.</code>
212+ *
213+ * @param key The key to look up for the message.
214+ * @param namedArguments List of pairs containing the parameter name and the substitution value.
215+ * @return Localised message with parameter placeholders replaced.
216+ */
217+ public static String getMessage (String key , List <Pair <String , Object >> namedArguments ) {
218+ Map <String , Object > namedArgs = new HashMap <>();
219+ for (Pair <String , Object > pair : namedArguments ) {
220+ namedArgs .put (pair .getKey (), pair .getValue ());
221+ }
222+ return getMessage (key , namedArgs );
174223 }
175224
176225 /**
177- * Set all of the I18N values on an <code>Action</code> by retrieving said values from the
178- * properties file.
226+ * Message composition for use with named arguments. Use when the message pattern string contains
227+ * field names, for example: <code>
228+ * Argument at index {paramIndex} to function {functionName} is invalid.</code>
229+ *
230+ * @param key The key to look up for the message.
231+ * @param namedArguments Map<String, Object> containing the parameter name and associated
232+ * value.
233+ * @return Localised message with parameter placeholders replaced.
234+ */
235+ public static String getMessage (String key , Map <String , Object > namedArguments ) {
236+ try {
237+ return MessageFormat .format (getText (key ), namedArguments );
238+ } catch (IllegalArgumentException iae ) {
239+ log .error (iae .getMessage (), iae );
240+ return "" ;
241+ }
242+ }
243+
244+ /**
245+ * Set all the I18N values on an <code>Action</code> by retrieving said values from the properties
246+ * file.
179247 *
180248 * <p>Uses the <code>key</code> as the index for the properties file to set the <b>Action.NAME</b>
181249 * field of <b>action</b>.
182250 *
183251 * <p>The string used for the <b>NAME</b> is searched for an ampersand ("&") to determine the
184- * mnemonic used by any menu item (no mnemonic is set if there is no ampersand). If there is one,
185- * the <b>Action.MNEMONIC_KEY</b> property is set.
252+ * mnemonic used by any menu item (no mnemonic is set without an ampersand). Where it exists the
253+ * <b>Action.MNEMONIC_KEY</b> property is set.
186254 *
187255 * <p>The <code>key</code> string has "<code>.accel</code>" appended to it and the properties file
188- * is searched again, this time to obtain a string representing the shortcut key. If there is one,
189- * the <b>Action.ACCELERATOR_KEY</b> property is set.
256+ * is searched again, this time getting a string representing the shortcut key. Where found, the
257+ * <b>Action.ACCELERATOR_KEY</b> property is set.
190258 *
191259 * <p>The <code>key</code> string has "<code>.description</code>" appended to it and the
192- * properties file is searched again, this time to obtain a string representing the status bar
193- * help message. If there is one , the <b>Action.SHORT_DESCRIPTION</b> property is set.
260+ * properties file is searched again, this time to get a string representing the status bar help
261+ * message. If found , the <b>Action.SHORT_DESCRIPTION</b> property is set.
194262 *
195263 * @param key String to use as an index into the <b>i18n.properties</b> file
196264 * @param action Action used to store the retrieved settings
@@ -226,7 +294,7 @@ public static List<String> getMatchingKeys(String regex) {
226294 public static List <String > getMatchingKeys (Pattern regex ) {
227295 Enumeration <String > keys = BUNDLE .getKeys ();
228296
229- List <String > menuItemKeys = new LinkedList <String >();
297+ List <String > menuItemKeys = new LinkedList <>();
230298 while (keys .hasMoreElements ()) {
231299 String key = keys .nextElement ();
232300 if (regex .matcher (key ).find ()) {
@@ -235,4 +303,14 @@ public static List<String> getMatchingKeys(Pattern regex) {
235303 }
236304 return menuItemKeys ;
237305 }
306+
307+ public static class MessageFactory extends AbstractMessageFactory {
308+ protected MessageFactory (String i18nKey ) {
309+ super (i18nKey );
310+ }
311+
312+ public static MessageFactory forKey (String i18nKey ) {
313+ return new MessageFactory (i18nKey );
314+ }
315+ }
238316}
0 commit comments