From 642e90ae8d885c8131b257ac115571361ec28725 Mon Sep 17 00:00:00 2001 From: Travis Schmidt Date: Wed, 22 Oct 2025 11:24:43 +0200 Subject: [PATCH 1/3] Enhances I18NCreator for creating Message interfaces from properties Currently I18NCreator creates method signatures that only use String arguments regardless of any MessageFormat that may be present for an argument. This patch attempts to create more accurate interface signatures based on the MessageFormat and also includes some GWT specific formats such as {0,list},{0,localdatetime,..} and Plurals. This patch also proposes to add the convention {0,safehtml} to mark that an argument to be of type SafeHtml. If an Argument is found to be of type SafeHtml the return type of the method will be set to SafeHtml. --- .../AbstractLocalizableInterfaceCreator.java | 26 +-- .../i18n/rebind/MessagesInterfaceCreator.java | 190 +++++++++++++++--- .../gen/TestMessagesArgTypes.properties | 18 ++ .../google/gwt/i18n/tools/I18NSyncTest_.java | 5 + 4 files changed, 193 insertions(+), 46 deletions(-) create mode 100644 user/test/com/google/gwt/i18n/client/gen/TestMessagesArgTypes.properties diff --git a/user/src/com/google/gwt/i18n/rebind/AbstractLocalizableInterfaceCreator.java b/user/src/com/google/gwt/i18n/rebind/AbstractLocalizableInterfaceCreator.java index 0a4ba4bcb49..0e1af0b4923 100644 --- a/user/src/com/google/gwt/i18n/rebind/AbstractLocalizableInterfaceCreator.java +++ b/user/src/com/google/gwt/i18n/rebind/AbstractLocalizableInterfaceCreator.java @@ -174,16 +174,6 @@ public void generate() throws FileNotFoundException, IOException { } } - /** - * Create a String method declaration from a Dictionary/value pair. - * - * @param key Dictionary - * @param defaultValue default value - */ - public void genSimpleMethodDecl(String key, String defaultValue) { - genMethodDecl("String", defaultValue, key); - } - /** * Create method args based upon the default value. * @@ -250,11 +240,15 @@ void generateFromPropertiesFile() throws IOException { + resourceFile + "' cannot be used to generate message classes, as it has no key/value pairs defined."); } + generateMethods(p, keys); + composer.commit(new PrintWriterTreeLogger()); + } + + void generateMethods(LocalizedProperties properties, String[] keys) { for (String key : keys) { - String value = p.getProperty(key); - genSimpleMethodDecl(key, value); + String value = properties.getProperty(key); + genMethodDecl(key, value); } - composer.commit(new PrintWriterTreeLogger()); } private void addFormatters() { @@ -265,14 +259,14 @@ private void addFormatters() { formatters.add(new RenameDuplicates()); } - private String formatKey(String key) { + protected String formatKey(String key) { for (ResourceKeyFormatter formatter : formatters) { key = formatter.format(key); } return key; } - private void genMethodDecl(String type, String defaultValue, String key) { + private void genMethodDecl(String defaultValue, String key) { composer.beginJavaDocComment(); String escaped = makeJavaString(defaultValue); composer.println("Translated " + escaped + ".\n"); @@ -281,7 +275,7 @@ private void genMethodDecl(String type, String defaultValue, String key) { genValueAnnotation(defaultValue); composer.println("@Key(" + makeJavaString(key) + ")"); String methodName = formatKey(key); - composer.print(type + " " + methodName); + composer.print("String " + methodName); composer.print("("); genMethodArgs(defaultValue); composer.print(");\n"); diff --git a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java index 9c842092cdb..7a0d604f7df 100644 --- a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java +++ b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java @@ -16,14 +16,18 @@ package com.google.gwt.i18n.rebind; import com.google.gwt.i18n.client.Messages; -import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk; -import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk; +import com.google.gwt.i18n.server.MessageFormatUtils; +import com.google.gwt.i18n.server.MessageFormatUtils.ArgumentChunk; +import com.google.gwt.i18n.server.MessageFormatUtils.TemplateChunk; + +import org.apache.tapestry.util.text.LocalizedProperties; import java.io.File; import java.io.IOException; import java.text.ParseException; -import java.util.HashSet; -import java.util.Set; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Creates a MessagesInterface from a Resource file. @@ -32,22 +36,21 @@ public class MessagesInterfaceCreator extends AbstractLocalizableInterfaceCreator { /** - * Searches for MessageFormat-style args in the template string and returns - * a set of argument indices seen. + * Searches for MessageFormat-style args in the template string and returns a map of of argument + * indices seen. * * @param template template to parse * @return set of argument indices seen * @throws ParseException if the template is incorrect. */ - private static Set numberOfMessageArgs(String template) - throws ParseException { - Set seenArgs = new HashSet(); - for (TemplateChunk chunk : MessageFormatParser.parse(template)) { + private static Map getMessageArgs(String template) throws ParseException { + HashMap args = new HashMap<>(); + for (TemplateChunk chunk : MessageFormatUtils.MessageStyle.MESSAGE_FORMAT.parse(template)) { if (chunk instanceof ArgumentChunk) { - seenArgs.add(((ArgumentChunk) chunk).getArgumentNumber()); + args.put(((ArgumentChunk) chunk).getArgumentNumber(), (ArgumentChunk) chunk); } } - return seenArgs; + return args; } /** @@ -65,28 +68,61 @@ public MessagesInterfaceCreator(String className, String packageName, Messages.class); } + @Override + void generateMethods(LocalizedProperties properties, String[] keys) { + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + String value = properties.getProperty(key); + Map plurals = new HashMap<>(); + while (i + 1 < keys.length && isNextPlural(key, keys[i + 1])) { + i++; + plurals.put(keys[i], properties.getProperty(keys[i])); + } + genMethodDecl(value, key, plurals); + } + } + @Override protected void genMethodArgs(String defaultValue) { - try { - Set seenArgs = numberOfMessageArgs(defaultValue); - int maxArgSeen = -1; - for (int arg : seenArgs) { - if (arg > maxArgSeen) { - maxArgSeen = arg; - } + } + + private boolean isNextPlural(String key, String nextKey) { + return nextKey.matches(key + "\\[.*\\]"); + } + + private void genMethodArgs(Map args) { + for (int i = 0; i <= Collections.max(args.keySet()); i++) { + if (i > 0) { + composer.print(", "); } - for (int i = 0; i <= maxArgSeen; i++) { - if (i > 0) { - composer.print(", "); - } - if (!seenArgs.contains(i)) { - composer.print("@Optional "); - } - composer.print("String arg" + i); + if (!args.containsKey(i)) { + composer.print("@Optional String arg" + i); + continue; } - } catch (ParseException e) { - throw new RuntimeException(defaultValue - + " could not be parsed as a MessageFormat string.", e); + String format = (format = args.get(i).getFormat()) != null ? format : "string"; + String subFormat = (subFormat = args.get(i).getSubFormat()) != null ? subFormat : ""; + if (args.get(i).isList()) { + composer.print("java.util.List<"); + } + switch (format) { + case "number": + determineNumberType(subFormat); + break; + case "date": + case "time": + case "localdatetime": + composer.print("java.util.Date"); + break; + case "safehtml": + composer.print("com.google.gwt.safehtml.shared.SafeHtml"); + break; + default: + composer.print("String"); + } + if (args.get(i).isList()) { + composer.print(">"); + } + composer.print(" arg" + i); } } @@ -100,4 +136,98 @@ protected String javaDocComment(String path) { return "Interface to represent the messages contained in resource bundle:\n\t" + path + "'."; } + + private void determineNumberType(String subFormat) { + switch (subFormat) { + case "integer": + composer.print("Integer"); + break; + case "currency": + case "percent": + default: + if(subFormat.contains(".")) { + composer.print("Double"); + } else { + composer.print("Integer"); + } + } + } + + private String determineReturnType(Map args) { + for (ArgumentChunk arg : args.values()) { + if ("safehtml".equals(arg.getFormat())) { + return "com.google.gwt.safehtml.shared.SafeHtml"; + } + } + return "String"; + } + + private void genPluralsAnnotation(Map plurals) { + composer.print("@AlternateMessage({"); + String[] keys = plurals.keySet().toArray(new String[] {}); + if (keys.length > 1) { + composer.println(""); + composer.indent(); + } + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + if (i > 0) { + composer.println(","); + } + composer.print(makeJavaString("\"" + + key.substring(key.indexOf('[') + 1, key.length() - 1) + + "\", ")); + composer.print(makeJavaString("\"" + plurals.get(key) + "\"")); + } + if (keys.length > 1) { + composer.println(); + composer.outdent(); + } + composer.println("})"); + } + + private void genMethodDecl(String defaultValue, String key, Map plurals) { + try { + Map args = getMessageArgs(defaultValue); + genMethodJavaDoc(defaultValue, args); + genValueAnnotation(defaultValue); + if (!plurals.isEmpty()) { + genPluralsAnnotation(plurals); + } + composer.println("@Key(" + makeJavaString(key) + ")"); + String methodName = formatKey(key); + String type = determineReturnType(args); + composer.print(type + " " + methodName); + composer.print("("); + if (!plurals.isEmpty()) { + composer.print("@PluralCount "); + } + if (!args.isEmpty()) { + genMethodArgs(args); + } + composer.print(");\n"); + } catch (ParseException e) { + throw new RuntimeException(defaultValue + " could not be parsed as a MessageFormat string.", + e); + } + } + + private void genMethodJavaDoc(String defaultValue, Map args) { + composer.beginJavaDocComment(); + String escaped = makeJavaString(defaultValue); + composer.println("Translated " + escaped + ".\n"); + if (!args.isEmpty()) { + for (int i = 0; i <= Collections.max(args.keySet()); i++) { + composer.print("@param arg" + i); + if (args.containsKey(i)) { + composer.println(" " + makeJavaString(args.get(i).getAsMessageFormatString())); + } else { + composer.println(" optional"); + } + } + } + composer.println("@return translated " + escaped); + composer.endJavaDocComment(); + } + } diff --git a/user/test/com/google/gwt/i18n/client/gen/TestMessagesArgTypes.properties b/user/test/com/google/gwt/i18n/client/gen/TestMessagesArgTypes.properties new file mode 100644 index 00000000000..b0c945e192a --- /dev/null +++ b/user/test/com/google/gwt/i18n/client/gen/TestMessagesArgTypes.properties @@ -0,0 +1,18 @@ +stringArg={0} +integerArg={0,number} +currencyArg={0,number,currency} +percentArg={0,number,percent} +doubleArg={0,number,###,##0.00} +dateArg={0,date} +timeArg={0,time} +localdatetime={0,localdatetime} +safehtmlArg={0,safehtml} +optionalArgs={1} and {3} +pluralArg={0,number} of items +pluralArg[none]=There are no items +pluralArg[one]=There is one item +pluralArg[two]=There are two items +pluralArg[few]=There are a few items +pluralArg[many]=There are many items +pluralArg[\=4]= There is exactly 4 items + diff --git a/user/test/com/google/gwt/i18n/tools/I18NSyncTest_.java b/user/test/com/google/gwt/i18n/tools/I18NSyncTest_.java index 389359d33f8..96a097b46c3 100644 --- a/user/test/com/google/gwt/i18n/tools/I18NSyncTest_.java +++ b/user/test/com/google/gwt/i18n/tools/I18NSyncTest_.java @@ -87,6 +87,11 @@ public void testMessagesQuoting() throws IOException { I18NSync.createMessagesInterfaceFromClassName(className, CLIENT_SOURCE_DIR); } + public void testMessagesArgTypes() throws IOException { + String className = CLIENT_SOURCE_PACKAGE + "TestMessagesArgTypes"; + I18NSync.createMessagesInterfaceFromClassName(className, CLIENT_SOURCE_DIR); + } + public void testMethodRenaming() throws IOException { String className = CLIENT_SOURCE_PACKAGE + "TestBadKeys"; I18NSync.createConstantsWithLookupInterfaceFromClassName(className, From 15f7fa7daaaa5266b4a54fa74fb54b364857a94e Mon Sep 17 00:00:00 2001 From: Travis Schmidt Date: Thu, 23 Oct 2025 08:33:35 +0200 Subject: [PATCH 2/3] Enhances I18NCreator for creating Message interfaces from properties Currently I18NCreator creates method signatures that only use String arguments regardless of any MessageFormat that may be present for an argument. This patch attempts to create more accurate interface signatures based on the MessageFormat and also includes some GWT specific formats such as {0,list},{0,localdatetime,..} and Plurals. This patch also proposes to add the convention {0,safehtml} to mark that an argument to be of type SafeHtml. If an Argument is found to be of type SafeHtml the return type of the method will be set to SafeHtml. --- .../gwt/i18n/rebind/MessagesInterfaceCreator.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java index 7a0d604f7df..96d05ba8410 100644 --- a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java +++ b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java @@ -166,7 +166,7 @@ private void genPluralsAnnotation(Map plurals) { composer.print("@AlternateMessage({"); String[] keys = plurals.keySet().toArray(new String[] {}); if (keys.length > 1) { - composer.println(""); + composer.println(); composer.indent(); } for (int i = 0; i < keys.length; i++) { @@ -174,10 +174,11 @@ private void genPluralsAnnotation(Map plurals) { if (i > 0) { composer.println(","); } - composer.print(makeJavaString("\"" + - key.substring(key.indexOf('[') + 1, key.length() - 1) + - "\", ")); - composer.print(makeJavaString("\"" + plurals.get(key) + "\"")); + composer.print("\""); + composer.print(key.substring(key.indexOf('[') + 1, key.length() - 1)); + composer.print("\", \""); + composer.print(makeJavaString(plurals.get(key))); + composer.println("\""); } if (keys.length > 1) { composer.println(); From aa91b6f506759fcd9491bfed38b3626a31042a14 Mon Sep 17 00:00:00 2001 From: Travis Schmidt Date: Thu, 23 Oct 2025 17:30:56 +0200 Subject: [PATCH 3/3] Enhances I18NCreator for creating Message interfaces from properties Currently I18NCreator creates method signatures that only use String arguments regardless of any MessageFormat that may be present for an argument. This patch attempts to create more accurate interface signatures based on the MessageFormat and also includes some GWT specific formats such as {0,list},{0,localdatetime,..} and Plurals. This patch also proposes to add the convention {0,safehtml} to mark that an argument to be of type SafeHtml. If an Argument is found to be of type SafeHtml the return type of the method will be set to SafeHtml. --- .../com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java index 96d05ba8410..3732b3f156a 100644 --- a/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java +++ b/user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java @@ -145,7 +145,7 @@ private void determineNumberType(String subFormat) { case "currency": case "percent": default: - if(subFormat.contains(".")) { + if (subFormat.contains(".")) { composer.print("Double"); } else { composer.print("Integer");