29
29
import com .google .javascript .jscomp .JsMessage .Part ;
30
30
import com .google .javascript .jscomp .JsMessage .PlaceholderFormatException ;
31
31
import com .google .javascript .jscomp .JsMessage .StringPart ;
32
+ import com .google .javascript .jscomp .JsMessageVisitor .ExtractedIcuTemplateParts ;
33
+ import com .google .javascript .jscomp .JsMessageVisitor .IcuMessageTemplateString ;
32
34
import com .google .javascript .jscomp .JsMessageVisitor .MalformedException ;
33
35
import com .google .javascript .jscomp .NodeTraversal .AbstractPostOrderCallback ;
34
36
import com .google .javascript .rhino .Node ;
@@ -276,6 +278,20 @@ public boolean unescapeHtmlEntities() {
276
278
private Node createMsgPropertiesNode (JsMessage message , MsgOptions msgOptions ) {
277
279
QuotedKeyObjectLitBuilder msgPropsBuilder = new QuotedKeyObjectLitBuilder ();
278
280
msgPropsBuilder .addString ("key" , message .getKey ());
281
+ if (msgOptions .isIcuTemplate () && !message .canonicalPlaceholderNames ().isEmpty ()) {
282
+ // ICU messages created using `declareIcuTemplate` can get stored into the XMB file as
283
+ // multiple parts if necessary to record example or original code text.
284
+ // `icu_placeholder_names` stores these parts of the ICU message, which allows us to
285
+ // correctly calculate the message ID in the protected message.
286
+ final Node namesArrayLit = astFactory .createArraylit ();
287
+ for (String name : message .canonicalPlaceholderNames ()) {
288
+ namesArrayLit .addChildToBack (astFactory .createString (name ));
289
+ }
290
+ // Example:
291
+ // declareIcuTemplate('blah blah {PH1} blah {PH2}', ... );
292
+ // icu_placeholder_names: ['PH1', 'PH2']
293
+ msgPropsBuilder .addNode ("icu_placeholder_names" , namesArrayLit );
294
+ }
279
295
String altId = message .getAlternateId ();
280
296
if (altId != null ) {
281
297
msgPropsBuilder .addString ("alt_id" , altId );
@@ -882,12 +898,41 @@ private ProtectedJsMessage(
882
898
checkState (propertiesNode .isObjectLit (), propertiesNode );
883
899
String msgKey = null ;
884
900
String meaning = null ;
901
+ Set <String > icuPlaceholderNames = new LinkedHashSet <>();
902
+ String messageText = null ;
903
+ Node messageTextNode = null ;
885
904
for (Node strKey = propertiesNode .getFirstChild ();
886
905
strKey != null ;
887
906
strKey = strKey .getNext ()) {
888
907
checkState (strKey .isStringKey (), strKey );
889
908
String key = strKey .getString ();
890
909
Node valueNode = strKey .getOnlyChild ();
910
+ if (key .equals ("icu_placeholder_names" )) {
911
+ checkState (valueNode .isArrayLit (), "icu_placeholder_names must be an array" );
912
+ // If the message is an ICU template and `icu_placeholder_names` is present, then there
913
+ // are placeholders in the message. These placeholders will be replaced at runtime, but
914
+ // it is important that we keep track of these placeholders because it means that the
915
+ // ICU template CANNOT be treated as a single string part, because having placeholders
916
+ // means that the message has multiple parts.
917
+ // When a message is a `declareIcuTemplate` with multiple parts, we generate a `msg id`
918
+ // in the XMB, which is sent to the Translation Console so that the translators can
919
+ // translate this. We generate the deterministic `msg id` using an algorithm that takes
920
+ // into account how many parts the message has.
921
+ // Now during JSCompiler compilation process, we protect the message by wrapping it in a
922
+ // `__jscomp_define_msg__` (for safety because we don't want any of our optimization
923
+ // passes to change the message).
924
+ // Later in this method, we generate an ID (using `idGenerator.generateId()`) and use
925
+ // this to lookup a message in the translated XTB file. As I mentioned earlier, the
926
+ // algorithm for generating an ID needs to know the correct parts of the message, so we
927
+ // fail to generate the same ID we did when we added the message to the XMB.
928
+ // This `icu_placeholder_names` field is necessary to help us figure out the correct
929
+ // parts of the message, in order to generate the correct message ID that matches the ID
930
+ // we generated when we added the message to the XMB (which is the same ID in the XTB).
931
+ for (Node valueNodeChild : valueNode .children ()) {
932
+ icuPlaceholderNames .add (valueNodeChild .getString ());
933
+ }
934
+ continue ;
935
+ }
891
936
checkState (valueNode .isStringLit (), valueNode );
892
937
String value = valueNode .getString ();
893
938
switch (key ) {
@@ -903,17 +948,13 @@ private ProtectedJsMessage(
903
948
jsMessageBuilder .setAlternateId (value );
904
949
break ;
905
950
case "msg_text" :
906
- try {
907
- // NOTE: If the text is for an ICU template, then it will not contain any
908
- // placeholders ("{$placeholderName}"), so it will be treated as a single string
909
- // part.
910
- jsMessageBuilder .appendParts (JsMessageVisitor .parseJsMessageTextIntoParts (value ));
911
- } catch (PlaceholderFormatException unused ) {
912
- // Somehow we stored the protected message text incorrectly, which should never
913
- // happen.
914
- throw new IllegalStateException (
915
- valueNode .getLocation () + ": Placeholder incorrectly formatted: >" + value + "<" );
916
- }
951
+ // This may be an ICU template that also has the `icu_placeholder_names` property, which
952
+ // means we need to append multiple parts of the message to `jsMessageBuilder`. For now,
953
+ // we will save the message text and current node, and we'll parse it once we know if
954
+ // this is an ICU template with multiple parts (after this loop to run through all the
955
+ // properties is finished).
956
+ messageText = value ;
957
+ messageTextNode = valueNode ;
917
958
break ;
918
959
case "isIcuTemplate" :
919
960
isIcuTemplate = true ;
@@ -930,6 +971,39 @@ private ProtectedJsMessage(
930
971
throw new IllegalStateException ("unknown protected message key: " + strKey );
931
972
}
932
973
}
974
+
975
+ try {
976
+ if (!icuPlaceholderNames .isEmpty ()) {
977
+ checkState (
978
+ isIcuTemplate ,
979
+ "Found icu_placeholder_names for a message that is not an ICU template." );
980
+ // This is an ICU template with placeholders ("{$placeholderName}"). We cannot treat this
981
+ // as a single string part, because it has multiple parts. Otherwise, we will generate the
982
+ // wrong message id and we will not be able to find the correct translated message in the
983
+ // XTB file (because when the XMB message was created during message extraction, we
984
+ // treated this ICU template as having multiple parts).
985
+ final IcuMessageTemplateString icuMessageTemplateString =
986
+ new IcuMessageTemplateString (messageText );
987
+ final ExtractedIcuTemplateParts extractedIcuTemplateParts =
988
+ icuMessageTemplateString .extractParts (icuPlaceholderNames );
989
+
990
+ // Append the parts of the ICU template to the jsMessageBuilder.
991
+ jsMessageBuilder .appendParts (extractedIcuTemplateParts .extractedParts );
992
+ } else {
993
+ // This message is a single string part. It may be an ICU template without placeholders,
994
+ // or it may be a normal `goog.getMsg()` message.
995
+ jsMessageBuilder .appendParts (JsMessageVisitor .parseJsMessageTextIntoParts (messageText ));
996
+ }
997
+ } catch (PlaceholderFormatException unused ) {
998
+ // Somehow we stored the protected message text incorrectly, which should never
999
+ // happen 🙏
1000
+ throw new IllegalStateException (
1001
+ messageTextNode .getLocation ()
1002
+ + ": Placeholder incorrectly formatted: >"
1003
+ + messageText
1004
+ + "<" );
1005
+ }
1006
+
933
1007
final String externalMessageId = JsMessageVisitor .getExternalMessageId (msgKey );
934
1008
if (externalMessageId != null ) {
935
1009
// MSG_EXTERNAL_12345 = ...
0 commit comments