All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.ReplaceMessages Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2004 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.AstFactory.type;
import static com.google.javascript.jscomp.JsMessageVisitor.MESSAGE_TREE_MALFORMED;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.JsMessage.Part;
import com.google.javascript.jscomp.JsMessage.PlaceholderFormatException;
import com.google.javascript.jscomp.JsMessage.StringPart;
import com.google.javascript.jscomp.JsMessageVisitor.MalformedException;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Node.SideEffectFlags;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 * ReplaceMessages replaces user-visible messages with alternatives. It uses Google specific
 * JsMessageVisitor implementation.
 */
@GwtIncompatible("JsMessage")
public final class ReplaceMessages {
  public static final DiagnosticType BUNDLE_DOES_NOT_HAVE_THE_MESSAGE =
      DiagnosticType.error(
          "JSC_BUNDLE_DOES_NOT_HAVE_THE_MESSAGE",
          "Message with id = {0} could not be found in replacement bundle");

  public static final DiagnosticType INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS =
      DiagnosticType.error(
          "JSC_INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS",
          "Alternate message ID={0} placeholders ({1}) differs from {2} placeholders ({3}).");

  private final AbstractCompiler compiler;
  private final MessageBundle bundle;
  private final boolean strictReplacement;
  private final AstFactory astFactory;

  ReplaceMessages(AbstractCompiler compiler, MessageBundle bundle, boolean strictReplacement) {
    this.compiler = compiler;
    this.astFactory = compiler.createAstFactory();
    this.bundle = bundle;
    this.strictReplacement = strictReplacement;
  }

  /**
   * When the returned pass is executed, the original `goog.getMsg()` etc. calls will be replaced
   * with a form that will survive unchanged through optimizations unless eliminated as unused.
   *
   * 

After all optimizations are complete, the pass returned by `getReplacementCompletionPass()`. */ public CompilerPass getMsgProtectionPass() { return new MsgProtectionPass(); } interface MsgProtectionData { JsMessage getMessage(); Node getMessageNode(); Node getTemplateTextNode(); @Nullable Node getPlaceholderValuesNode(); MsgOptions getMessageOptions(); } class MsgProtectionPass extends JsMessageVisitor { public MsgProtectionPass() { super(ReplaceMessages.this.compiler, bundle.idGenerator()); } @Override public void process(Node externs, Node root) { // Add externs declarations for the function names we use in our replacements. NodeUtil.createSynthesizedExternsSymbol(compiler, ReplaceMessagesConstants.DEFINE_MSG_CALLEE); NodeUtil.createSynthesizedExternsSymbol( compiler, ReplaceMessagesConstants.FALLBACK_MSG_CALLEE); // JsMessageVisitor.process() does the traversal that calls the processX() methods below. super.process(externs, root); } @Override protected void processIcuTemplateDefinition(IcuTemplateDefinition definition) { performMessageProtection( new MsgProtectionData() { @Override public JsMessage getMessage() { return definition.getMessage(); } @Override public Node getMessageNode() { return definition.getMessageNode(); } @Override public Node getTemplateTextNode() { return definition.getTemplateTextNode(); } @Override public @Nullable Node getPlaceholderValuesNode() { // There are no compile-time placeholder replacements for ICU templates return null; } @Override public MsgOptions getMessageOptions() { return ICU_MSG_OPTIONS; } }); } @Override protected void processJsMessageDefinition(JsMessageDefinition definition) { // This is the currently preferred form. // `MSG_A = goog.getMsg('hello, {$name}', {name: getName()}, {html: true})` performMessageProtection( new MsgProtectionData() { @Override public JsMessage getMessage() { return definition.getMessage(); } @Override public Node getMessageNode() { return definition.getMessageNode(); } @Override public Node getTemplateTextNode() { return definition.getTemplateTextNode(); } @Override public @Nullable Node getPlaceholderValuesNode() { return definition.getPlaceholderValuesNode(); } @Override public MsgOptions getMessageOptions() { return getMsgOptionsFromDefinition(definition); } }); } private void performMessageProtection(MsgProtectionData msgProtectionData) { final JsMessage message = msgProtectionData.getMessage(); final Node callNode = msgProtectionData.getMessageNode(); final Node originalMessageString = msgProtectionData.getTemplateTextNode(); final Node placeholdersNode = msgProtectionData.getPlaceholderValuesNode(); final MsgOptions msgOptions = msgProtectionData.getMessageOptions(); checkState(callNode.isCall(), callNode); // `goog.getMsg('message string', {}, {})` final Node googGetMsg = callNode.getFirstChild(); // Construct // `__jscomp_define_msg__({}, {})` final String protectionFunctionName = ReplaceMessagesConstants.DEFINE_MSG_CALLEE; final Node newCallee = createProtectionFunctionCallee(protectionFunctionName).srcref(googGetMsg); final Node msgPropertiesNode = createMsgPropertiesNode(message, msgOptions).srcrefTree(originalMessageString); Node newCallNode = astFactory.createCall(newCallee, type(callNode), msgPropertiesNode).srcref(callNode); // If the result of this call (the message) is unused, there is no reason for optimizations // to preserve it. newCallNode.setSideEffectFlags(SideEffectFlags.NO_SIDE_EFFECTS); if (placeholdersNode != null) { checkState(placeholdersNode.isObjectLit(), placeholdersNode); // put quotes around the keys so they won't get renamed. for (Node strKey = placeholdersNode.getFirstChild(); strKey != null; strKey = strKey.getNext()) { checkState(strKey.isStringKey(), strKey); strKey.setQuotedStringKey(); } newCallNode.addChildToBack(placeholdersNode.detach()); } callNode.replaceWith(newCallNode); compiler.reportChangeToEnclosingScope(newCallNode); } private Node createProtectionFunctionCallee(String protectionFunctionName) { final Node callee = astFactory.createNameWithUnknownType(protectionFunctionName); // The name is declared constant in the externs definition we created, so all references // to it must also be marked as constant for consistency's sake. callee.putBooleanProp(Node.IS_CONSTANT_NAME, true); return callee; } @Override void processMessageFallback(Node callNode, JsMessage message1, JsMessage message2) { final Node originalCallee = checkNotNull(callNode.getFirstChild(), callNode); final Node fallbackCallee = createProtectionFunctionCallee(ReplaceMessagesConstants.FALLBACK_MSG_CALLEE) .srcref(originalCallee); final Node originalFirstArg = checkNotNull(originalCallee.getNext(), callNode); final Node firstMsgKey = astFactory.createString(message1.getKey()).srcref(originalFirstArg); final Node originalSecondArg = checkNotNull(originalFirstArg.getNext(), callNode); final Node secondMsgKey = astFactory.createString(message2.getKey()).srcref(originalSecondArg); // `__jscomp_msg_fallback__('MSG_ONE', MSG_ONE, 'MSG_TWO', MSG_TWO)` final Node newCallNode = astFactory .createCall( fallbackCallee, type(callNode), firstMsgKey, originalFirstArg.detach(), secondMsgKey, originalSecondArg.detach()) .srcref(callNode); // If the result of this call (the message) is unused, there is no reason for optimizations // to preserve it. newCallNode.setSideEffectFlags(SideEffectFlags.NO_SIDE_EFFECTS); callNode.replaceWith(newCallNode); compiler.reportChangeToEnclosingScope(newCallNode); } } private MsgOptions getMsgOptionsFromDefinition(JsMessageDefinition definition) { return new MsgOptions() { @Override public boolean isIcuTemplate() { return false; } @Override public boolean escapeLessThan() { return definition.shouldEscapeLessThan(); } @Override public boolean unescapeHtmlEntities() { return definition.shouldUnescapeHtmlEntities(); } }; } private Node createMsgPropertiesNode(JsMessage message, MsgOptions msgOptions) { QuotedKeyObjectLitBuilder msgPropsBuilder = new QuotedKeyObjectLitBuilder(); msgPropsBuilder.addString("key", message.getKey()); String altId = message.getAlternateId(); if (altId != null) { msgPropsBuilder.addString("alt_id", altId); } final String meaning = message.getMeaning(); if (meaning != null) { msgPropsBuilder.addString("meaning", meaning); } if (msgOptions.isIcuTemplate()) { msgPropsBuilder.addString("msg_text", message.asIcuMessageString()); // Just being present is what records this option as true msgPropsBuilder.addString("isIcuTemplate", ""); } else { msgPropsBuilder.addString("msg_text", message.asJsMessageString()); } if (msgOptions.escapeLessThan()) { // Just being present is what records this option as true msgPropsBuilder.addString("escapeLessThan", ""); } if (msgOptions.unescapeHtmlEntities()) { // Just being present is what records this option as true msgPropsBuilder.addString("unescapeHtmlEntities", ""); } return msgPropsBuilder.build(); } private final class QuotedKeyObjectLitBuilder { // LinkedHashMap to keep the keys in the order we set them so our output is deterministic. private final LinkedHashMap keyToValueNodeMap = new LinkedHashMap<>(); private QuotedKeyObjectLitBuilder addString(String key, String value) { return addNode(key, astFactory.createString(value)); } @CanIgnoreReturnValue private QuotedKeyObjectLitBuilder addNode(String key, Node node) { checkState(!keyToValueNodeMap.containsKey(key), "repeated key: %s", key); keyToValueNodeMap.put(key, node); return this; } private Node build() { final Node result = astFactory.createObjectLit(); for (Entry entry : keyToValueNodeMap.entrySet()) { result.addChildToBack(astFactory.createQuotedStringKey(entry.getKey(), entry.getValue())); } return result; } } /** * When the returned pass is executed, the protected messages created by `getMsgProtectionPass()` * will be replaced by the final message form read from the appropriate message bundle. */ public CompilerPass getReplacementCompletionPass() { return new ReplacementCompletionPass(); } class ReplacementCompletionPass implements CompilerPass { // Keep track of which messages actually got translated, so we know what do do when we // see a message fallback call. final Set translatedMsgKeys = new HashSet<>(); @Override public void process(Node externs, Node root) { // for each `__jscomp_define_msg__` call in post-order traversal // replace it with the appropriate expression NodeTraversal.traverse( compiler, root, new AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { ProtectedJsMessage protectedJsMessage = ProtectedJsMessage.fromAstNode(n, bundle.idGenerator()); if (protectedJsMessage != null) { visitMsgDefinition(protectedJsMessage); } else { ProtectedMsgFallback protectedMsgFallback = ProtectedMsgFallback.fromAstNode(n); if (protectedMsgFallback != null) { visitMsgFallback(protectedMsgFallback); } } } }); } void visitMsgDefinition(ProtectedJsMessage protectedJsMessage) { try { final JsMessage originalMsg = protectedJsMessage.jsMessage; final Node nodeToReplace = protectedJsMessage.definitionNode; final JsMessage translatedMsg = lookupMessage(protectedJsMessage.definitionNode, bundle, originalMsg); final JsMessage msgToUse; if (translatedMsg != null) { msgToUse = translatedMsg; // Remember that this one got translated in case it is used in a fallback. translatedMsgKeys.add(originalMsg.getKey()); } else { if (strictReplacement) { compiler.report( JSError.make(nodeToReplace, BUNDLE_DOES_NOT_HAVE_THE_MESSAGE, originalMsg.getId())); } msgToUse = originalMsg; } final MsgOptions msgOptions = protectedJsMessage.getMsgOptions(); final Map placeholderMap = extractPlaceholderValuesMapOrThrow(protectedJsMessage.substitutionsNode); final Node finalMsgConstructionExpression = constructStringExprNode(msgToUse, placeholderMap, msgOptions, nodeToReplace); finalMsgConstructionExpression.srcrefTreeIfMissing(nodeToReplace); nodeToReplace.replaceWith(finalMsgConstructionExpression); compiler.reportChangeToEnclosingScope(finalMsgConstructionExpression); } catch (MalformedException e) { compiler.report(JSError.make(e.getNode(), MESSAGE_TREE_MALFORMED, e.getMessage())); } } private void visitMsgFallback(ProtectedMsgFallback protectedMsgFallback) { final Node valueNodeToUse; if (translatedMsgKeys.contains(protectedMsgFallback.firstMsgKey)) { // Obviously use the first message, if it is translated. valueNodeToUse = protectedMsgFallback.firstMsgValue; } else if (translatedMsgKeys.contains(protectedMsgFallback.secondMsgKey)) { // Fallback to the second message if it has a translation. valueNodeToUse = protectedMsgFallback.secondMsgValue; } else { // If neither is translated, then use the first message as it is defined in the source code. valueNodeToUse = protectedMsgFallback.firstMsgValue; } valueNodeToUse.detach(); protectedMsgFallback.callNode.replaceWith(valueNodeToUse); compiler.reportChangeToEnclosingScope(valueNodeToUse); } } static ImmutableMap extractPlaceholderValuesMapOrThrow(Node valuesObjLit) { try { return JsMessageVisitor.extractObjectLiteralMap(valuesObjLit).extractAsValueMap(); } catch (MalformedException e) { throw new IllegalStateException(e); } } private static class ProtectedMsgFallback { final Node callNode; final String firstMsgKey; final Node firstMsgValue; final String secondMsgKey; final Node secondMsgValue; ProtectedMsgFallback( Node callNode, String firstMsgKey, Node firstMsgValue, String secondMsgKey, Node secondMsgValue) { this.callNode = callNode; this.firstMsgKey = firstMsgKey; this.firstMsgValue = firstMsgValue; this.secondMsgKey = secondMsgKey; this.secondMsgValue = secondMsgValue; } static @Nullable ProtectedMsgFallback fromAstNode(Node n) { if (!n.isCall()) { return null; } final Node callee = n.getFirstChild(); if (!callee.matchesName(ReplaceMessagesConstants.FALLBACK_MSG_CALLEE)) { return null; } checkState(n.hasXChildren(5), "bad message fallback call: %s", n); final Node firstMsgKeyNode = callee.getNext(); final String firstMsgKey = firstMsgKeyNode.getString(); final Node firstMsgValue = firstMsgKeyNode.getNext(); final Node secondMsgKeyNode = firstMsgValue.getNext(); final String secondMsgKey = secondMsgKeyNode.getString(); final Node secondMsgValue = secondMsgKeyNode.getNext(); return new ProtectedMsgFallback(n, firstMsgKey, firstMsgValue, secondMsgKey, secondMsgValue); } } private @Nullable JsMessage lookupMessage( Node callNode, MessageBundle bundle, JsMessage message) { JsMessage translatedMessage = bundle.getMessage(message.getId()); if (translatedMessage != null) { return translatedMessage; } String alternateId = message.getAlternateId(); if (alternateId == null) { return null; } JsMessage alternateMessage = bundle.getMessage(alternateId); if (alternateMessage != null) { // Validate that the alternate message is compatible with this message. Ideally we'd also // check meaning and description, but they're not populated by `MessageBundle.getMessage`. final ImmutableSet jsCodePlaceholderNames = message.jsPlaceholderNames(); final ImmutableSet alternateMsgPlaceholderNames = alternateMessage.jsPlaceholderNames(); if (!Objects.equals(jsCodePlaceholderNames, alternateMsgPlaceholderNames)) { // The JS code definition has no placeholders, but the message we got from the translation // bundle does have placeholders. // // This can happen for ICU - formatted messages. They can contain placeholders in the // message string in the form "{PH_NAME}", which this compiler ignores, because they will // be replaced at runtime by passing the whole string to a message formatter method. // However, sometimes the translation bundle will represent the "{PH_NAME}" substring as // an actual placeholder reference, because it was generated by some tool other than // closure-compiler. In that case, we need to join all the parts of the message back // together as a single string with the "{PH_NAME}" references back in place. // // Messages with this form always start with a particular formula, which we can test for // here to make sure this isn't actually a case of something being broken in the translation // pipeline. if (jsCodePlaceholderNames.isEmpty() && isStartOfIcuMessage(message.asIcuMessageString())) { return alternateMessage; } else { compiler.report( JSError.make( callNode, INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS, alternateId, String.valueOf(alternateMsgPlaceholderNames), message.getKey(), String.valueOf(jsCodePlaceholderNames))); return null; } } } return alternateMessage; } /** * When the returned pass is executed, the original `goog.getMsg()` etc. calls will all be * replaced with the final message form read from the message bundle. * *

This is the original way of running this pass as a single operation. */ public CompilerPass getFullReplacementPass() { return new FullReplacementPass(); } interface FullReplacementMsgData { JsMessage getMessage(); Node getMessageNode(); MsgOptions getMessageOptions(); ImmutableMap getPlaceholderValueMap(); } class FullReplacementPass extends JsMessageVisitor { public FullReplacementPass() { super(ReplaceMessages.this.compiler, bundle.idGenerator()); } @Override void processMessageFallback(Node callNode, JsMessage message1, JsMessage message2) { boolean isFirstMessageTranslated = (lookupMessage(callNode, bundle, message1) != null); boolean isSecondMessageTranslated = (lookupMessage(callNode, bundle, message2) != null); Node replacementNode = (isSecondMessageTranslated && !isFirstMessageTranslated) ? callNode.getChildAtIndex(2) : callNode.getSecondChild(); callNode.replaceWith(replacementNode.detach()); Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(replacementNode); if (changeScope != null) { compiler.reportChangeToChangeScope(changeScope); } } @Override protected void processIcuTemplateDefinition(IcuTemplateDefinition definition) { processFullReplacement( new FullReplacementMsgData() { @Override public JsMessage getMessage() { return definition.getMessage(); } @Override public Node getMessageNode() { return definition.getMessageNode(); } @Override public MsgOptions getMessageOptions() { return ICU_MSG_OPTIONS; } @Override public ImmutableMap getPlaceholderValueMap() { // There are no compile-time placeholder replacements for an ICU template message. return ImmutableMap.of(); } }); } @Override protected void processJsMessageDefinition(JsMessageDefinition definition) { processFullReplacement( new FullReplacementMsgData() { @Override public JsMessage getMessage() { return definition.getMessage(); } @Override public Node getMessageNode() { return definition.getMessageNode(); } @Override public MsgOptions getMessageOptions() { return getMsgOptionsFromDefinition(definition); } @Override public ImmutableMap getPlaceholderValueMap() { return definition.getPlaceholderValueMap(); } }); } private void processFullReplacement(FullReplacementMsgData fullReplacementMsgData) { final JsMessage message = fullReplacementMsgData.getMessage(); final Node msgNode = fullReplacementMsgData.getMessageNode(); final MsgOptions options = fullReplacementMsgData.getMessageOptions(); final ImmutableMap placeholderValueMap = fullReplacementMsgData.getPlaceholderValueMap(); // Get the replacement. JsMessage replacement = lookupMessage(msgNode, bundle, message); if (replacement == null) { if (strictReplacement) { compiler.report(JSError.make(msgNode, BUNDLE_DOES_NOT_HAVE_THE_MESSAGE, message.getId())); // Fallback to the default message return; } else { // In case if it is not a strict replacement we could leave original // message. replacement = message; } } // Replace the message. Node newValue; try { // Build the replacement tree. newValue = constructStringExprNode(replacement, placeholderValueMap, options, msgNode); } catch (MalformedException e) { compiler.report(JSError.make(e.getNode(), MESSAGE_TREE_MALFORMED, e.getMessage())); newValue = msgNode; } if (newValue != msgNode) { newValue.srcrefTreeIfMissing(msgNode); msgNode.replaceWith(newValue); compiler.reportChangeToEnclosingScope(newValue); } } } /** * Creates a parse tree corresponding to the remaining message parts in an iteration. The result * consists of one or more STRING nodes, placeholder replacement value nodes (which can be * arbitrary expressions), and ADD nodes. * * @param placeholderMap map from placeholder names to value Nodes * @return the root of the constructed parse tree */ private Node constructStringExprNode( JsMessage msgToUse, Map placeholderMap, MsgOptions options, Node nodeToReplace) throws MalformedException { if (placeholderMap.isEmpty()) { // The compiler does not expect to do any placeholder substitution, because the message // definition in the JS code doesn't have any placeholders. if (options.isIcuTemplate()) { // This message was declared as an ICU template. Its placeholders, if any, will not be // replaced during compilation, but rather at runtime by a special localization method. // We just need to put the string back together again, if it was broken up to include // placeholders. return createNodeForMsgString(options, msgToUse.asIcuMessageString()); } else if (msgToUse.jsPlaceholderNames().isEmpty()) { // The translated message read from the bundle also has no placeholders. // It doesn't really matter which asXMessageString() call we make, since they return // the same thing when there are no placeholders. return createNodeForMsgString(options, msgToUse.asJsMessageString()); } else { // The JS code definition has no placeholders, but the message we got from the translation // bundle does have placeholders. // // This can happen for ICU - formatted messages. They can contain placeholders in the // message string in the form "{PH_NAME}", which this compiler ignores, because they will // be replaced at runtime by passing the whole string to a message formatter method. // However, sometimes the translation bundle will represent the "{PH_NAME}" substring as // an actual placeholder reference, because it was generated by some tool other than // closure-compiler. In that case, we need to join all the parts of the message back // together as a single string with the "{PH_NAME}" references back in place. // // Messages with this form always start with a particular formula, which we can test for // here to make sure this isn't actually a case of something being broken in the translation // pipeline. final String icuMsgString = msgToUse.asIcuMessageString(); if (isStartOfIcuMessage(icuMsgString)) { return createNodeForMsgString(options, icuMsgString); } else { throw new MalformedException( "The translated message has placeholders, but the definition in the JS code does" + " not.", nodeToReplace); } } } // In some edge cases a message might have normal string parts that are consecutive. // Join them together before processing them further. // TODO(bradfordcsmith): There's a test case with an "&" escape broken across 2 string // parts. It fails if we stop doing this merging in advance, but I suspect there's no real // life case where this merging is really necessary. I think it's a left-over from when the // bundle-reading code would automatically convert ICU message placeholders into string parts // without actually merging them with the neighboring string parts. List msgParts = mergeStringParts(msgToUse.getParts()); if (msgParts.isEmpty()) { return astFactory.createString(""); } else { Node resultNode = null; for (Part msgPart : msgParts) { final Node partNode; if (msgPart.isPlaceholder()) { final String jsPlaceholderName = msgPart.getJsPlaceholderName(); final Node valueNode = placeholderMap.get(jsPlaceholderName); if (valueNode == null) { throw new MalformedException( "Unrecognized message placeholder referenced: " + jsPlaceholderName, nodeToReplace); } partNode = valueNode.cloneTree(); } else { // The part is just a string literal. partNode = createNodeForMsgString(options, msgPart.getString()); } resultNode = (resultNode == null) ? partNode : astFactory.createAdd(resultNode, partNode); } return resultNode; } } private Node createNodeForMsgString(MsgOptions options, String s) { if (options.escapeLessThan()) { // Note that "&" is not replaced because the translation can contain HTML entities. s = s.replace("<", "<"); } if (options.unescapeHtmlEntities()) { // Unescape entities that need to be escaped when embedding HTML or XML in data/attributes // of an HTML/XML document. See https://www.w3.org/TR/xml/#sec-predefined-ent. // Note that "&" must be the last to avoid "creating" new entities. // To print an html entity in the resulting message, double-escape: `&amp;`. s = s.replace("<", "<") .replace(">", ">") .replace("'", "'") .replace(""", "\"") .replace("&", "&"); } return astFactory.createString(s); } /** Options for escaping characters in translated messages. */ interface MsgOptions { /** True if the message is defined using the ICU template declaration method. */ boolean isIcuTemplate(); /** Replace `'<'` with `'<'` in the message. */ boolean escapeLessThan(); /** * Replace these escaped entities with their literal characters in the message (Overrides * escapeLessThan) * *

     * '<' -> '<'
     * '>' -> '>'
     * ''' -> "'"
     * '"' -> '"'
     * '&' -> '&'
     * 
*/ boolean unescapeHtmlEntities(); } private static final MsgOptions ICU_MSG_OPTIONS = new MsgOptions() { @Override public boolean isIcuTemplate() { return true; } @Override public boolean escapeLessThan() { // The compiler doesn't do escaping for ICU messages. return false; } @Override public boolean unescapeHtmlEntities() { // The compiler doesn't do unescaping for ICU messages. return false; } }; /** Merges consecutive string parts in the list of message parts. */ private static List mergeStringParts(List parts) { List result = new ArrayList<>(); for (Part part : parts) { if (part.isPlaceholder()) { result.add(part); } else { Part lastPart = result.isEmpty() ? null : Iterables.getLast(result); if (lastPart == null || lastPart.isPlaceholder()) { result.add(part); } else { result.set(result.size() - 1, StringPart.create(lastPart.getString() + part.getString())); } } } return result; } /** * Holds information about the protected form of a translatable message that appears in the AST. * *

The original translatable messages are replaced with this protected form by the logic in * `ReplaceMessages` to protect the message information through the optimization passes. * *


   *   // original
   *   var MSG_GREETING = goog.getMsg('Hello, {$name}!', {name: person.getName()}, {html: false});
   *   // protected form
   *   var MSG_GREETING = __jscomp_define_msg__(
   *       {
   *         'key': 'MSG_GREETING',
   *         'msg_text': 'Hello, {$name}!',
   *         'escapeLessThan': ''
   *       },
   *       {'name': person.getName()});
   * 
*/ public static class ProtectedJsMessage { private final JsMessage jsMessage; // The expression Node that defines the message and should be replaced with the localized // message. private final Node definitionNode; // e.g. `{ name: x.getName(), age: x.getAgeString() }` private final @Nullable Node substitutionsNode; // The message was defined in JS code using the ICU template method. private final boolean isIcuTemplate; // Replace `'<'` with `'<'` in the message. private final boolean escapeLessThan; // Replace these escaped entities with their literal characters in the message // (Overrides escapeLessThan) // '<' -> '<' // '>' -> '>' // ''' -> "'" // '"' -> '"' // '&' -> '&' private final boolean unescapeHtmlEntities; private ProtectedJsMessage( JsMessage jsMessage, Node definitionNode, @Nullable Node substitutionsNode, boolean isIcuTemplate, boolean escapeLessThan, boolean unescapeHtmlEntities) { this.jsMessage = jsMessage; this.definitionNode = definitionNode; this.substitutionsNode = substitutionsNode; this.isIcuTemplate = isIcuTemplate; this.escapeLessThan = escapeLessThan; this.unescapeHtmlEntities = unescapeHtmlEntities; } public static @Nullable ProtectedJsMessage fromAstNode( Node node, JsMessage.IdGenerator idGenerator) { if (!node.isCall()) { return null; } final Node calleeNode = checkNotNull(node.getFirstChild(), node); if (!calleeNode.matchesName(ReplaceMessagesConstants.DEFINE_MSG_CALLEE)) { return null; } final Node propertiesNode = checkNotNull(calleeNode.getNext(), calleeNode); final Node substitutionsNode = propertiesNode.getNext(); boolean escapeLessThanOption = false; boolean unescapeHtmlEntitiesOption = false; boolean isIcuTemplate = false; JsMessage.Builder jsMessageBuilder = new JsMessage.Builder(); checkState(propertiesNode.isObjectLit(), propertiesNode); String msgKey = null; String meaning = null; for (Node strKey = propertiesNode.getFirstChild(); strKey != null; strKey = strKey.getNext()) { checkState(strKey.isStringKey(), strKey); String key = strKey.getString(); Node valueNode = strKey.getOnlyChild(); checkState(valueNode.isStringLit(), valueNode); String value = valueNode.getString(); switch (key) { case "key": jsMessageBuilder.setKey(value); msgKey = value; break; case "meaning": jsMessageBuilder.setMeaning(value); meaning = value; break; case "alt_id": jsMessageBuilder.setAlternateId(value); break; case "msg_text": try { // NOTE: If the text is for an ICU template, then it will not contain any // placeholders ("{$placeholderName}"), so it will be treated as a single string // part. jsMessageBuilder.appendParts(JsMessageVisitor.parseJsMessageTextIntoParts(value)); } catch (PlaceholderFormatException unused) { // Somehow we stored the protected message text incorrectly, which should never // happen. throw new IllegalStateException( valueNode.getLocation() + ": Placeholder incorrectly formatted: >" + value + "<"); } break; case "isIcuTemplate": isIcuTemplate = true; break; case "escapeLessThan": // Just being present enables this option escapeLessThanOption = true; break; case "unescapeHtmlEntities": // just being present enables this option unescapeHtmlEntitiesOption = true; break; default: throw new IllegalStateException("unknown protected message key: " + strKey); } } final String externalMessageId = JsMessageVisitor.getExternalMessageId(msgKey); if (externalMessageId != null) { // MSG_EXTERNAL_12345 = ... jsMessageBuilder.setIsExternalMsg(true).setId(externalMessageId); } else { // NOTE: If the message was anonymous (assigned to a variable or property named // MSG_UNNAMED_XXX), the key we have here will be the one generated from the message // text, and we won't end up setting the isAnonymous flag. Nothing seems to use that // flag... // TODO(bradfordcsmith): Maybe remove the isAnonymous flag for jsMessage objects? final String meaningForIdGeneration = meaning != null ? meaning : JsMessageVisitor.removeScopedAliasesPrefix(msgKey); if (idGenerator != null) { jsMessageBuilder.setId( idGenerator.generateId(meaningForIdGeneration, jsMessageBuilder.getParts())); } else { jsMessageBuilder.setId(meaningForIdGeneration); } } return new ProtectedJsMessage( jsMessageBuilder.build(), node, substitutionsNode, isIcuTemplate, escapeLessThanOption, unescapeHtmlEntitiesOption); } MsgOptions getMsgOptions() { return new MsgOptions() { @Override public boolean isIcuTemplate() { return isIcuTemplate; } @Override public boolean escapeLessThan() { return escapeLessThan; } @Override public boolean unescapeHtmlEntities() { return unescapeHtmlEntities; } }; } } /** * Detects an ICU-formatted plural or select message. Any placeholders occurring inside these * messages must be rewritten in ICU format. */ static boolean isStartOfIcuMessage(String part) { // ICU messages start with a '{' followed by an identifier, followed by a ',' and then 'plural' // or 'select' followed by another comma. // the 'startsWith' check is redundant but should allow us to skip using the matcher if (!part.startsWith("{")) { return false; } int commaIndex = part.indexOf(',', 1); // if commaIndex == 1 that means the identifier is empty, which isn't allowed. if (commaIndex <= 1) { return false; } int nextBracketIndex = part.indexOf('{', 1); return (nextBracketIndex == -1 || nextBracketIndex > commaIndex) && (part.startsWith("plural,", commaIndex + 1) || part.startsWith("select,", commaIndex + 1)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy