Skip to content

Commit c72bb7c

Browse files
authored
Enhances I18NCreator for creating Message interfaces from properties (#10166)
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. Fixes #9049
1 parent 3a7ec34 commit c72bb7c

File tree

4 files changed

+194
-46
lines changed

4 files changed

+194
-46
lines changed

user/src/com/google/gwt/i18n/rebind/AbstractLocalizableInterfaceCreator.java

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,6 @@ public void generate() throws FileNotFoundException, IOException {
174174
}
175175
}
176176

177-
/**
178-
* Create a String method declaration from a Dictionary/value pair.
179-
*
180-
* @param key Dictionary
181-
* @param defaultValue default value
182-
*/
183-
public void genSimpleMethodDecl(String key, String defaultValue) {
184-
genMethodDecl("String", defaultValue, key);
185-
}
186-
187177
/**
188178
* Create method args based upon the default value.
189179
*
@@ -250,11 +240,15 @@ void generateFromPropertiesFile() throws IOException {
250240
+ resourceFile
251241
+ "' cannot be used to generate message classes, as it has no key/value pairs defined.");
252242
}
243+
generateMethods(p, keys);
244+
composer.commit(new PrintWriterTreeLogger());
245+
}
246+
247+
void generateMethods(LocalizedProperties properties, String[] keys) {
253248
for (String key : keys) {
254-
String value = p.getProperty(key);
255-
genSimpleMethodDecl(key, value);
249+
String value = properties.getProperty(key);
250+
genMethodDecl(key, value);
256251
}
257-
composer.commit(new PrintWriterTreeLogger());
258252
}
259253

260254
private void addFormatters() {
@@ -265,14 +259,14 @@ private void addFormatters() {
265259
formatters.add(new RenameDuplicates());
266260
}
267261

268-
private String formatKey(String key) {
262+
protected String formatKey(String key) {
269263
for (ResourceKeyFormatter formatter : formatters) {
270264
key = formatter.format(key);
271265
}
272266
return key;
273267
}
274268

275-
private void genMethodDecl(String type, String defaultValue, String key) {
269+
private void genMethodDecl(String defaultValue, String key) {
276270
composer.beginJavaDocComment();
277271
String escaped = makeJavaString(defaultValue);
278272
composer.println("Translated " + escaped + ".\n");
@@ -281,7 +275,7 @@ private void genMethodDecl(String type, String defaultValue, String key) {
281275
genValueAnnotation(defaultValue);
282276
composer.println("@Key(" + makeJavaString(key) + ")");
283277
String methodName = formatKey(key);
284-
composer.print(type + " " + methodName);
278+
composer.print("String " + methodName);
285279
composer.print("(");
286280
genMethodArgs(defaultValue);
287281
composer.print(");\n");

user/src/com/google/gwt/i18n/rebind/MessagesInterfaceCreator.java

Lines changed: 161 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616
package com.google.gwt.i18n.rebind;
1717

1818
import com.google.gwt.i18n.client.Messages;
19-
import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk;
20-
import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk;
19+
import com.google.gwt.i18n.server.MessageFormatUtils;
20+
import com.google.gwt.i18n.server.MessageFormatUtils.ArgumentChunk;
21+
import com.google.gwt.i18n.server.MessageFormatUtils.TemplateChunk;
22+
23+
import org.apache.tapestry.util.text.LocalizedProperties;
2124

2225
import java.io.File;
2326
import java.io.IOException;
2427
import java.text.ParseException;
25-
import java.util.HashSet;
26-
import java.util.Set;
28+
import java.util.Collections;
29+
import java.util.HashMap;
30+
import java.util.Map;
2731

2832
/**
2933
* Creates a MessagesInterface from a Resource file.
@@ -32,22 +36,21 @@ public class MessagesInterfaceCreator extends
3236
AbstractLocalizableInterfaceCreator {
3337

3438
/**
35-
* Searches for MessageFormat-style args in the template string and returns
36-
* a set of argument indices seen.
39+
* Searches for MessageFormat-style args in the template string and returns a map of of argument
40+
* indices seen.
3741
*
3842
* @param template template to parse
3943
* @return set of argument indices seen
4044
* @throws ParseException if the template is incorrect.
4145
*/
42-
private static Set<Integer> numberOfMessageArgs(String template)
43-
throws ParseException {
44-
Set<Integer> seenArgs = new HashSet<Integer>();
45-
for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
46+
private static Map<Integer, ArgumentChunk> getMessageArgs(String template) throws ParseException {
47+
HashMap<Integer, ArgumentChunk> args = new HashMap<>();
48+
for (TemplateChunk chunk : MessageFormatUtils.MessageStyle.MESSAGE_FORMAT.parse(template)) {
4649
if (chunk instanceof ArgumentChunk) {
47-
seenArgs.add(((ArgumentChunk) chunk).getArgumentNumber());
50+
args.put(((ArgumentChunk) chunk).getArgumentNumber(), (ArgumentChunk) chunk);
4851
}
4952
}
50-
return seenArgs;
53+
return args;
5154
}
5255

5356
/**
@@ -65,28 +68,61 @@ public MessagesInterfaceCreator(String className, String packageName,
6568
Messages.class);
6669
}
6770

71+
@Override
72+
void generateMethods(LocalizedProperties properties, String[] keys) {
73+
for (int i = 0; i < keys.length; i++) {
74+
String key = keys[i];
75+
String value = properties.getProperty(key);
76+
Map<String, String> plurals = new HashMap<>();
77+
while (i + 1 < keys.length && isNextPlural(key, keys[i + 1])) {
78+
i++;
79+
plurals.put(keys[i], properties.getProperty(keys[i]));
80+
}
81+
genMethodDecl(value, key, plurals);
82+
}
83+
}
84+
6885
@Override
6986
protected void genMethodArgs(String defaultValue) {
70-
try {
71-
Set<Integer> seenArgs = numberOfMessageArgs(defaultValue);
72-
int maxArgSeen = -1;
73-
for (int arg : seenArgs) {
74-
if (arg > maxArgSeen) {
75-
maxArgSeen = arg;
76-
}
87+
}
88+
89+
private boolean isNextPlural(String key, String nextKey) {
90+
return nextKey.matches(key + "\\[.*\\]");
91+
}
92+
93+
private void genMethodArgs(Map<Integer, ArgumentChunk> args) {
94+
for (int i = 0; i <= Collections.max(args.keySet()); i++) {
95+
if (i > 0) {
96+
composer.print(", ");
7797
}
78-
for (int i = 0; i <= maxArgSeen; i++) {
79-
if (i > 0) {
80-
composer.print(", ");
81-
}
82-
if (!seenArgs.contains(i)) {
83-
composer.print("@Optional ");
84-
}
85-
composer.print("String arg" + i);
98+
if (!args.containsKey(i)) {
99+
composer.print("@Optional String arg" + i);
100+
continue;
86101
}
87-
} catch (ParseException e) {
88-
throw new RuntimeException(defaultValue
89-
+ " could not be parsed as a MessageFormat string.", e);
102+
String format = (format = args.get(i).getFormat()) != null ? format : "string";
103+
String subFormat = (subFormat = args.get(i).getSubFormat()) != null ? subFormat : "";
104+
if (args.get(i).isList()) {
105+
composer.print("java.util.List<");
106+
}
107+
switch (format) {
108+
case "number":
109+
determineNumberType(subFormat);
110+
break;
111+
case "date":
112+
case "time":
113+
case "localdatetime":
114+
composer.print("java.util.Date");
115+
break;
116+
case "safehtml":
117+
composer.print("com.google.gwt.safehtml.shared.SafeHtml");
118+
break;
119+
default:
120+
composer.print("String");
121+
}
122+
if (args.get(i).isList()) {
123+
composer.print(">");
124+
}
125+
composer.print(" arg" + i);
90126
}
91127
}
92128

@@ -100,4 +136,99 @@ protected String javaDocComment(String path) {
100136
return "Interface to represent the messages contained in resource bundle:\n\t"
101137
+ path + "'.";
102138
}
139+
140+
private void determineNumberType(String subFormat) {
141+
switch (subFormat) {
142+
case "integer":
143+
composer.print("Integer");
144+
break;
145+
case "currency":
146+
case "percent":
147+
default:
148+
if (subFormat.contains(".")) {
149+
composer.print("Double");
150+
} else {
151+
composer.print("Integer");
152+
}
153+
}
154+
}
155+
156+
private String determineReturnType(Map<Integer, ArgumentChunk> args) {
157+
for (ArgumentChunk arg : args.values()) {
158+
if ("safehtml".equals(arg.getFormat())) {
159+
return "com.google.gwt.safehtml.shared.SafeHtml";
160+
}
161+
}
162+
return "String";
163+
}
164+
165+
private void genPluralsAnnotation(Map<String, String> plurals) {
166+
composer.print("@AlternateMessage({");
167+
String[] keys = plurals.keySet().toArray(new String[] {});
168+
if (keys.length > 1) {
169+
composer.println();
170+
composer.indent();
171+
}
172+
for (int i = 0; i < keys.length; i++) {
173+
String key = keys[i];
174+
if (i > 0) {
175+
composer.println(",");
176+
}
177+
composer.print("\"");
178+
composer.print(key.substring(key.indexOf('[') + 1, key.length() - 1));
179+
composer.print("\", \"");
180+
composer.print(makeJavaString(plurals.get(key)));
181+
composer.println("\"");
182+
}
183+
if (keys.length > 1) {
184+
composer.println();
185+
composer.outdent();
186+
}
187+
composer.println("})");
188+
}
189+
190+
private void genMethodDecl(String defaultValue, String key, Map<String, String> plurals) {
191+
try {
192+
Map<Integer, ArgumentChunk> args = getMessageArgs(defaultValue);
193+
genMethodJavaDoc(defaultValue, args);
194+
genValueAnnotation(defaultValue);
195+
if (!plurals.isEmpty()) {
196+
genPluralsAnnotation(plurals);
197+
}
198+
composer.println("@Key(" + makeJavaString(key) + ")");
199+
String methodName = formatKey(key);
200+
String type = determineReturnType(args);
201+
composer.print(type + " " + methodName);
202+
composer.print("(");
203+
if (!plurals.isEmpty()) {
204+
composer.print("@PluralCount ");
205+
}
206+
if (!args.isEmpty()) {
207+
genMethodArgs(args);
208+
}
209+
composer.print(");\n");
210+
} catch (ParseException e) {
211+
throw new RuntimeException(defaultValue + " could not be parsed as a MessageFormat string.",
212+
e);
213+
}
214+
}
215+
216+
private void genMethodJavaDoc(String defaultValue, Map<Integer, ArgumentChunk> args) {
217+
composer.beginJavaDocComment();
218+
String escaped = makeJavaString(defaultValue);
219+
composer.println("Translated " + escaped + ".\n");
220+
if (!args.isEmpty()) {
221+
for (int i = 0; i <= Collections.max(args.keySet()); i++) {
222+
composer.print("@param arg" + i);
223+
if (args.containsKey(i)) {
224+
composer.println(" " + makeJavaString(args.get(i).getAsMessageFormatString()));
225+
} else {
226+
composer.println(" optional");
227+
}
228+
}
229+
}
230+
composer.println("@return translated " + escaped);
231+
composer.endJavaDocComment();
232+
}
233+
103234
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
stringArg={0}
2+
integerArg={0,number}
3+
currencyArg={0,number,currency}
4+
percentArg={0,number,percent}
5+
doubleArg={0,number,###,##0.00}
6+
dateArg={0,date}
7+
timeArg={0,time}
8+
localdatetime={0,localdatetime}
9+
safehtmlArg={0,safehtml}
10+
optionalArgs={1} and {3}
11+
pluralArg={0,number} of items
12+
pluralArg[none]=There are no items
13+
pluralArg[one]=There is one item
14+
pluralArg[two]=There are two items
15+
pluralArg[few]=There are a few items
16+
pluralArg[many]=There are many items
17+
pluralArg[\=4]= There is exactly 4 items
18+

user/test/com/google/gwt/i18n/tools/I18NSyncTest_.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ public void testMessagesQuoting() throws IOException {
8787
I18NSync.createMessagesInterfaceFromClassName(className, CLIENT_SOURCE_DIR);
8888
}
8989

90+
public void testMessagesArgTypes() throws IOException {
91+
String className = CLIENT_SOURCE_PACKAGE + "TestMessagesArgTypes";
92+
I18NSync.createMessagesInterfaceFromClassName(className, CLIENT_SOURCE_DIR);
93+
}
94+
9095
public void testMethodRenaming() throws IOException {
9196
String className = CLIENT_SOURCE_PACKAGE + "TestBadKeys";
9297
I18NSync.createConstantsWithLookupInterfaceFromClassName(className,

0 commit comments

Comments
 (0)