Reland of "DevTools: Added GRD/GRDP files for all localizable strings in the DevTools."

This is a reland of https://chromium-review.googlesource.com/c/chromium/src/+/1613921/, which was reverted (Bug=966547) because
third_party/blink/renderer/devtools/front_end/langpacks/devtools_ui_strings.grd was being picked up by Google's internal localization pipeline.

Bug=941561

Change-Id: I9a97e16e367716189d34944b76605c9321d47092
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1627921
Reviewed-by: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: Nico Weber <thakis@chromium.org>
Reviewed-by: Alexei Filippov <alph@chromium.org>
Commit-Queue: Lorne Mitchell <lomitch@microsoft.com>
Cr-Original-Commit-Position: refs/heads/master@{#664013}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 613ca444d07326b6f087c083af79a636a858a7d9
diff --git a/scripts/localization_utils/check_localized_strings.js b/scripts/localization_utils/check_localized_strings.js
new file mode 100644
index 0000000..be79c23
--- /dev/null
+++ b/scripts/localization_utils/check_localized_strings.js
@@ -0,0 +1,384 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Functions in this script parse DevTools frontend .js and module.json files,
+ * collect localizable strings, check if frontend strings are in .grd/.grdp
+ * files and report error if present.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const {promisify} = require('util');
+const writeFileAsync = promisify(fs.writeFile);
+const localizationUtils = require('./localization_utils');
+const escodegen = localizationUtils.escodegen;
+const esprimaTypes = localizationUtils.esprimaTypes;
+const esprima = localizationUtils.esprima;
+const DEVTOOLS_FRONTEND_PATH = path.resolve(__dirname, '..', '..', 'front_end');
+const extensionStringKeys = ['category', 'destination', 'title', 'title-mac'];
+
+// Format of frontendStrings
+// { IDS_md5-hash => {
+//     string: string,
+//     code: string,
+//     filepath: string,
+//     location: {
+//       start: {
+//         line: number, (1-based)
+//         column: number (0-based)
+//       },
+//       end: {
+//         line: number,
+//         column: number
+//       }
+//     },
+//     arguments: string[]
+//   }
+// }
+const frontendStrings = new Map();
+
+// Format
+// {
+//   IDS_KEY => {
+//     filepath: string,
+//     location: {
+//       start: {
+//         line: number
+//       },
+//       end: {
+//         line: number
+//       }
+//     }
+//   }
+// }
+const IDSkeys = new Map();
+
+const devtoolsFrontendPath = path.resolve(__dirname, '..', '..', 'front_end');
+
+async function parseLocalizableResourceMaps(isDebug) {
+  const devtoolsFiles = [];
+  await localizationUtils.getFilesFromDirectory(devtoolsFrontendPath, devtoolsFiles, ['.js', 'module.json']);
+
+  const promises = [parseLocalizableStrings(devtoolsFiles, isDebug), parseIDSKeys(localizationUtils.GRD_PATH, isDebug)];
+  return Promise.all(promises);
+}
+
+/**
+ * The following functions parse localizable strings (wrapped in
+ * Common.UIString, UI.formatLocalized or ls``) from devtools frontend files.
+ */
+
+async function parseLocalizableStrings(devtoolsFiles, isDebug) {
+  const promises = devtoolsFiles.map(filePath => parseLocalizableStringsFromFile(filePath));
+  await Promise.all(promises);
+  if (isDebug)
+    await writeFileAsync(path.resolve(__dirname, 'localizable_strings.json'), JSON.stringify(frontendStrings));
+  return frontendStrings;
+}
+
+async function parseLocalizableStringsFromFile(filePath) {
+  const fileContent = await localizationUtils.parseFileContent(filePath);
+  if (path.basename(filePath) === 'module.json')
+    return parseLocalizableStringFromModuleJson(fileContent, filePath);
+
+  const ast = esprima.parse(fileContent, {loc: true});
+  for (const node of ast.body)
+    parseLocalizableStringFromNode(node, filePath);
+}
+
+function parseLocalizableStringFromModuleJson(fileContent, filePath) {
+  const fileJSON = JSON.parse(fileContent);
+  if (!fileJSON.extensions)
+    return;
+
+  for (const extension of fileJSON.extensions) {
+    for (const key in extension) {
+      if (extensionStringKeys.includes(key)) {
+        addString(extension[key], extension[key], filePath);
+      } else if (key === 'device') {
+        addString(extension.device.title, extension.device.title, filePath);
+      } else if (key === 'options') {
+        for (const option of extension.options) {
+          addString(option.title, option.title, filePath);
+          if (option.text !== undefined)
+            addString(option.text, option.text, filePath);
+        }
+      }
+    }
+  }
+}
+
+function parseLocalizableStringFromNode(node, filePath) {
+  if (!node)
+    return;
+
+  if (Array.isArray(node)) {
+    for (const child of node)
+      parseLocalizableStringFromNode(child, filePath);
+
+    return;
+  }
+
+  const keys = Object.keys(node);
+  const objKeys = keys.filter(key => key !== 'loc' && typeof node[key] === 'object');
+  if (objKeys.length === 0) {
+    // base case: all values are non-objects -> node is a leaf
+    return;
+  }
+
+  const locCase = localizationUtils.getLocalizationCase(node);
+  switch (locCase) {
+    case 'Common.UIString':
+      handleCommonUIString(node, filePath);
+      break;
+    case 'UI.formatLocalized':
+      if (node.arguments !== undefined && node.arguments[1] !== undefined && node.arguments[1].elements !== undefined)
+        handleCommonUIString(node, filePath, node.arguments[1].elements);
+      break;
+    case 'Tagged Template':
+      handleTemplateLiteral(node.quasi, escodegen.generate(node), filePath);
+      break;
+    case null:
+      break;
+    default:
+      throw new Error(
+          `${filePath}${localizationUtils.getLocationMessage(node.loc)}: unexpected localization case for node: ${
+              escodegen.generate(node)}`);
+  }
+
+  for (const key of objKeys) {
+    // recursively parse all the child nodes
+    parseLocalizableStringFromNode(node[key], filePath);
+  }
+}
+
+function handleCommonUIString(node, filePath, argumentNodes) {
+  if (argumentNodes === undefined)
+    argumentNodes = node.arguments.slice(1);
+  const firstArgType = node.arguments[0].type;
+  switch (firstArgType) {
+    case esprimaTypes.LITERAL:
+      const message = node.arguments[0].value;
+      addString(message, escodegen.generate(node), filePath, node.loc, argumentNodes);
+      break;
+    case esprimaTypes.TEMP_LITERAL:
+      handleTemplateLiteral(node.arguments[0], escodegen.generate(node), filePath, argumentNodes);
+      break;
+    default:
+      break;
+  }
+}
+
+function handleTemplateLiteral(node, code, filePath, argumentNodes) {
+  if (node.expressions.length === 0) {
+    // template literal does not contain any variables, parse the value
+    addString(node.quasis[0].value.cooked, code, filePath, node.loc, argumentNodes);
+    return;
+  }
+
+  argumentNodes = node.expressions;
+  let processedMsg = '';
+  for (let i = 0; i < node.quasis.length; i++) {
+    processedMsg += node.quasis[i].value.cooked;
+    if (i < node.expressions.length) {
+      // add placeholder for variable so that
+      // the ph tag gets generated
+      processedMsg += '%s';
+    }
+  }
+  addString(processedMsg, code, filePath, node.loc, argumentNodes);
+}
+
+function addString(str, code, filePath, location, argumentNodes) {
+  const currentString = {
+    string: str,
+    code: code,
+    filepath: filePath,
+  };
+  if (location)
+    currentString.location = location;
+  if (argumentNodes && argumentNodes.length > 0)
+    currentString.arguments = argumentNodes.map(argNode => escodegen.generate(argNode));
+
+  // In the case of duplicates, to enforce that entries are added to
+  // a consistent GRDP file, we use the file path that sorts lowest as
+  // the winning entry into frontendStrings.
+  const ids = localizationUtils.getIDSKey(str);
+  if (frontendStrings.has(ids) && frontendStrings.get(ids).filepath <= filePath)
+    return;
+  frontendStrings.set(ids, currentString);
+}
+
+/**
+ * The following functions parse <message>s and their IDS keys from
+ * devtools frontend grdp files.
+ */
+
+async function parseIDSKeys(grdFilePath, isDebug) {
+  // NOTE: this function assumes that no <message> tags are present in the parent
+  const grdpFilePaths = await parseGRDFile(grdFilePath);
+  await parseGRDPFiles(grdpFilePaths);
+  if (isDebug)
+    await writeFileAsync(path.resolve(__dirname, 'IDS_Keys.json'), JSON.stringify(IDSkeys));
+  return IDSkeys;
+}
+
+async function parseGRDFile(grdFilePath) {
+  const fileContent = await localizationUtils.parseFileContent(grdFilePath);
+  const grdFileDir = path.dirname(grdFilePath);
+  const partFileRegex = /<part file="(.*?)"/g;
+
+  let match;
+  const grdpFilePaths = new Set();
+  while ((match = partFileRegex.exec(fileContent)) !== null) {
+    if (match.index === partFileRegex.lastIndex)
+      partFileRegex.lastIndex++;
+    // match[0]: full match
+    // match[1]: part file path
+    grdpFilePaths.add(path.resolve(grdFileDir, match[1]));
+  }
+  return grdpFilePaths;
+}
+
+function parseGRDPFiles(grdpFilePaths) {
+  const promises = Array.from(grdpFilePaths, grdpFilePath => parseGRDPFile(grdpFilePath));
+  return Promise.all(promises);
+}
+
+function trimGrdpPlaceholder(placeholder) {
+  const exampleRegex = new RegExp('<ex>.*?<\/ex>', 'gms');
+  // $1s<ex>my example</ex> -> $1s
+  return placeholder.replace(exampleRegex, '').trim();
+}
+
+function convertToFrontendPlaceholders(message) {
+  // <ph name="phname">$1s<ex>my example</ex></ph> and <ph name="phname2">$2.3f</ph>
+  // match[0]: <ph name="phname1">$1s</ph>
+  // match[1]: $1s<ex>my example</ex>
+  let placeholderRegex = new RegExp('<ph[^>]*>(.*?)<\/ph>', 'gms');
+  let match;
+  while ((match = placeholderRegex.exec(message)) !== null) {
+    const placeholder = match[0];
+    const placeholderValue = trimGrdpPlaceholder(match[1]);
+    const newPlaceholderValue = placeholderValue.replace(/\$[1-9]/, '%');
+    message =
+        message.substring(0, match.index) + newPlaceholderValue + message.substring(match.index + placeholder.length);
+    // Modified the message, so search from the beginning of the string again.
+    placeholderRegex.lastIndex = 0;
+  }
+  return message;
+}
+
+function trimGrdpMessage(message) {
+  // '    Message text \n  ' trims to ' Message text '.
+  const fixedLeadingWhitespace = 4;  // GRDP encoding uses 4 leading spaces.
+  const trimmedMessage = message.substring(4);
+  return trimmedMessage.substring(0, trimmedMessage.lastIndexOf('\n'));
+}
+
+async function parseGRDPFile(filePath) {
+  const fileContent = await localizationUtils.parseFileContent(filePath);
+
+  function lineNumberOfIndex(str, index) {
+    const stringToIndex = str.substr(0, index);
+    return stringToIndex.split('\n').length;
+  }
+
+  // Example:
+  //  <message name="IDS_*" desc="*">
+  //      Message text here with optional placeholders <ph name="phname">$1s</ph>
+  //  </message>
+  // match[0]: the entire '<message>...</message>' block.
+  // match[1]: '     Message text here with optional placeholders <ph name="phname">$1s</ph>\n  '
+  const messageRegex = new RegExp('<message[^>]*>\s*\n(.*?)<\/message>', 'gms');
+  let match;
+  while ((match = messageRegex.exec(fileContent)) !== null) {
+    const line = lineNumberOfIndex(fileContent, match.index);
+
+    let message = match[1];
+    message = trimGrdpMessage(message);
+    message = convertToFrontendPlaceholders(message);
+    message = localizationUtils.sanitizeStringIntoFrontendFormat(message);
+
+    const ids = localizationUtils.getIDSKey(message);
+    IDSkeys.set(ids, {filepath: filePath, location: {start: {line}, end: {line}}});
+  }
+}
+
+/**
+ * The following functions compare frontend localizable strings
+ * with grdp <message>s and report error of resources to add or
+ * remove.
+ */
+async function getAndReportResourcesToAdd(frontendStrings, IDSkeys) {
+  const keysToAddToGRD = getDifference(IDSkeys, frontendStrings);
+  if (keysToAddToGRD.size === 0)
+    return;
+
+  let errorStr = 'The following frontend string(s) need to be added to GRD/GRDP file(s).\n';
+  errorStr += 'Please refer to auto-generated message(s) below and modify as needed.\n\n';
+
+  const frontendDirs = await localizationUtils.getChildDirectoriesFromDirectory(DEVTOOLS_FRONTEND_PATH);
+  const fileToGRDPMap = new Map();
+
+  // Example error message:
+  // third_party/blink/renderer/devtools/front_end/network/NetworkDataGridNode.js Line 973: ls`(disk cache)`
+  // Add a new message tag for this string to third_party\blink\renderer\devtools\front_end\network\network_strings.grdp
+  // <message name="IDS_DEVTOOLS_ad86890fb40822a3b12627efaca4ecd7" desc="Fill in the description.">
+  //   (disk cache)
+  // </message>
+  for (const [key, stringObj] of keysToAddToGRD) {
+    let relativeGRDPFilePath = '';
+    if (fileToGRDPMap.has(stringObj.filepath)) {
+      relativeGRDPFilePath = fileToGRDPMap.get(stringObj.filepath);
+    } else {
+      relativeGRDPFilePath = localizationUtils.getRelativeFilePathFromSrc(
+          localizationUtils.getGRDPFilePath(stringObj.filepath, frontendDirs));
+      fileToGRDPMap.set(stringObj.filepath, relativeGRDPFilePath);
+    }
+    errorStr += `${localizationUtils.getRelativeFilePathFromSrc(stringObj.filepath)}${
+        localizationUtils.getLocationMessage(stringObj.location)}: ${stringObj.code}\n`;
+    errorStr += `Add a new message tag for this string to ${
+        localizationUtils.getRelativeFilePathFromSrc(
+            localizationUtils.getGRDPFilePath(stringObj.filepath, frontendDirs))}\n\n`;
+    errorStr += localizationUtils.createGrdpMessage(key, stringObj);
+  }
+  return errorStr;
+}
+
+function getAndReportResourcesToRemove(frontendStrings, IDSkeys) {
+  const keysToRemoveFromGRD = getDifference(frontendStrings, IDSkeys);
+  if (keysToRemoveFromGRD.size === 0)
+    return;
+
+  let errorStr =
+      '\nThe message(s) associated with the following IDS key(s) should be removed from its GRD/GRDP file(s):\n';
+  // Example error message:
+  // third_party/blink/renderer/devtools/front_end/help/help_strings.grdp Line 18: IDS_DEVTOOLS_7d0ee6fed10d3d4e5c9ee496729ab519
+  for (const [key, keyObj] of keysToRemoveFromGRD) {
+    errorStr += `${localizationUtils.getRelativeFilePathFromSrc(keyObj.filepath)}${
+        localizationUtils.getLocationMessage(keyObj.location)}: ${key}\n\n`;
+  }
+  return errorStr;
+}
+
+/**
+ * Output a Map containing entries that are in @comparison but not @reference in sorted order.
+ */
+function getDifference(reference, comparison) {
+  const difference = [];
+  for (const [key, value] of comparison) {
+    if (!reference.has(key))
+      difference.push([key, value]);
+  }
+  return new Map(difference.sort());
+}
+
+module.exports = {
+  parseLocalizableResourceMaps,
+  getAndReportResourcesToAdd,
+  getAndReportResourcesToRemove,
+  getDifference
+};
diff --git a/scripts/localization_utils/localization_utils.js b/scripts/localization_utils/localization_utils.js
new file mode 100644
index 0000000..11119c5
--- /dev/null
+++ b/scripts/localization_utils/localization_utils.js
@@ -0,0 +1,269 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const fs = require('fs');
+const md5 = require('./md5');
+const {promisify} = require('util');
+const path = require('path');
+const readFileAsync = promisify(fs.readFile);
+const readDirAsync = promisify(fs.readdir);
+const statAsync = promisify(fs.stat);
+
+const esprimaTypes = {
+  BI_EXPR: 'BinaryExpression',
+  CALL_EXPR: 'CallExpression',
+  COND_EXPR: 'ConditionalExpression',
+  IDENTIFIER: 'Identifier',
+  LITERAL: 'Literal',
+  MEMBER_EXPR: 'MemberExpression',
+  TAGGED_TEMP_EXPR: 'TaggedTemplateExpression',
+  TEMP_LITERAL: 'TemplateLiteral'
+};
+
+const excludeFiles = ['lighthouse-dt-bundle.js', 'Tests.js'];
+const excludeDirs = ['test_runner', 'Images', 'langpacks', 'node_modules'];
+const cppSpecialCharactersMap = {
+  '"': '\\"',
+  '\\': '\\\\',
+  '\n': '\\n'
+};
+const IDSPrefix = 'IDS_DEVTOOLS_';
+
+const THIRD_PARTY_PATH = path.resolve(__dirname, '..', '..', '..', '..', '..');
+const SRC_PATH = path.resolve(THIRD_PARTY_PATH, '..');
+const GRD_PATH = path.resolve(__dirname, '..', '..', 'front_end', 'langpacks', 'devtools_ui_strings.grd');
+const REPO_NODE_MODULES_PATH = path.resolve(THIRD_PARTY_PATH, 'node', 'node_modules');
+const escodegen = require(path.resolve(REPO_NODE_MODULES_PATH, 'escodegen'));
+const esprima = require(path.resolve(REPO_NODE_MODULES_PATH, 'esprima'));
+
+function getRelativeFilePathFromSrc(filePath) {
+  return path.relative(SRC_PATH, filePath);
+}
+
+function shouldParseDirectory(directoryName) {
+  return !excludeDirs.some(dir => directoryName.includes(dir));
+}
+
+/**
+ * @filepath can be partial path or full path, as long as it contains the file name.
+ */
+function shouldParseFile(filepath) {
+  return !excludeFiles.includes(path.basename(filepath));
+}
+
+async function parseFileContent(filePath) {
+  const fileContent = await readFileAsync(filePath);
+  return fileContent.toString();
+}
+
+function isNodeCallOnObject(node, objectName, propertyName) {
+  return node !== undefined && node.type === esprimaTypes.CALL_EXPR &&
+      verifyCallExpressionCallee(node.callee, objectName, propertyName);
+}
+
+function isNodeCommonUIStringCall(node) {
+  return isNodeCallOnObject(node, 'Common', 'UIString');
+}
+
+function isNodeUIformatLocalized(node) {
+  return isNodeCallOnObject(node, 'UI', 'formatLocalized');
+}
+
+function isNodelsTaggedTemplateExpression(node) {
+  return node !== undefined && node.type === esprimaTypes.TAGGED_TEMP_EXPR && verifyIdentifier(node.tag, 'ls') &&
+      node.quasi !== undefined && node.quasi.type !== undefined && node.quasi.type === esprimaTypes.TEMP_LITERAL;
+}
+
+/**
+ * Verify callee of objectName.propertyName(), e.g. Common.UIString().
+ */
+function verifyCallExpressionCallee(callee, objectName, propertyName) {
+  return callee !== undefined && callee.type === esprimaTypes.MEMBER_EXPR && callee.computed === false &&
+      verifyIdentifier(callee.object, objectName) && verifyIdentifier(callee.property, propertyName);
+}
+
+function verifyIdentifier(node, name) {
+  return node !== undefined && node.type === esprimaTypes.IDENTIFIER && node.name === name;
+}
+
+function getLocalizationCase(node) {
+  if (isNodeCommonUIStringCall(node))
+    return 'Common.UIString';
+  else if (isNodelsTaggedTemplateExpression(node))
+    return 'Tagged Template';
+  else if (isNodeUIformatLocalized(node))
+    return 'UI.formatLocalized';
+  else
+    return null;
+}
+
+function isLocalizationCall(node) {
+  return isNodeCommonUIStringCall(node) || isNodelsTaggedTemplateExpression(node) || isNodeUIformatLocalized(node);
+}
+
+/**
+ * Verify if callee is functionName() or object.functionName().
+ */
+function verifyFunctionCallee(callee, functionName) {
+  return callee !== undefined &&
+      ((callee.type === esprimaTypes.IDENTIFIER && callee.name === functionName) ||
+       (callee.type === esprimaTypes.MEMBER_EXPR && verifyIdentifier(callee.property, functionName)));
+}
+
+function getLocationMessage(location) {
+  if (location !== undefined && location.start !== undefined && location.end !== undefined &&
+      location.start.line !== undefined && location.end.line !== undefined) {
+    const startLine = location.start.line;
+    const endLine = location.end.line;
+    if (startLine === endLine)
+      return ` Line ${startLine}`;
+    else
+      return ` Line ${location.start.line}-${location.end.line}`;
+  }
+  return '';
+}
+
+function sanitizeStringIntoGRDFormat(str) {
+  return str.replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&apos;')
+}
+
+function sanitizeStringIntoFrontendFormat(str) {
+  return str.replace(/&apos;/g, '\'')
+      .replace(/&quot;/g, '"')
+      .replace(/&gt;/g, '>')
+      .replace(/&lt;/g, '<')
+      .replace(/&amp;/g, '&');
+}
+
+function sanitizeString(str, specialCharactersMap) {
+  let sanitizedStr = '';
+  for (let i = 0; i < str.length; i++) {
+    let currChar = str.charAt(i);
+    if (specialCharactersMap[currChar] !== undefined)
+      currChar = specialCharactersMap[currChar];
+
+    sanitizedStr += currChar;
+  }
+  return sanitizedStr;
+}
+
+function sanitizeStringIntoCppFormat(str) {
+  return sanitizeString(str, cppSpecialCharactersMap);
+}
+
+async function getFilesFromItem(itemPath, filePaths, acceptedFileEndings) {
+  const stat = await statAsync(itemPath);
+  if (stat.isDirectory() && shouldParseDirectory(itemPath))
+    return await getFilesFromDirectory(itemPath, filePaths, acceptedFileEndings);
+
+  const hasAcceptedEnding =
+      acceptedFileEndings.some(acceptedEnding => itemPath.toLowerCase().endsWith(acceptedEnding.toLowerCase()));
+  if (hasAcceptedEnding && shouldParseFile(itemPath))
+    filePaths.push(itemPath);
+}
+
+async function getFilesFromDirectory(directoryPath, filePaths, acceptedFileEndings) {
+  const itemNames = await readDirAsync(directoryPath);
+  const promises = [];
+  for (const itemName of itemNames) {
+    const itemPath = path.resolve(directoryPath, itemName);
+    promises.push(getFilesFromItem(itemPath, filePaths, acceptedFileEndings));
+  }
+  return Promise.all(promises);
+}
+
+async function getChildDirectoriesFromDirectory(directoryPath) {
+  const dirPaths = [];
+  const itemNames = await readDirAsync(directoryPath);
+  for (const itemName of itemNames) {
+    const itemPath = path.resolve(directoryPath, itemName);
+    const stat = await statAsync(itemPath);
+    if (stat.isDirectory() && shouldParseDirectory(itemName))
+      dirPaths.push(itemPath);
+  }
+  return dirPaths;
+}
+
+/**
+ * Get the parent grdp file path for the input frontend file path.
+ * NOTE: Naming convention of a grdp file is the name of the child directory under
+ * devtools/front_end plus _strings.grdp
+ */
+function getGRDPFilePath(frontendFilepath, frontendDirs) {
+  const frontendDirsLowerCase = frontendDirs.map(dir => dir.toLowerCase());
+  const dirpath = path.dirname(frontendFilepath);
+  if (frontendDirsLowerCase.includes(dirpath.toLowerCase()))
+    return path.resolve(dirpath, `${path.basename(dirpath)}_strings.grdp`);
+}
+
+function modifyStringIntoGRDFormat(str, args) {
+  let sanitizedStr = sanitizeStringIntoGRDFormat(str);
+
+  const phRegex = /%d|%f|%s|%.[0-9]f/gm;
+  if (!str.match(phRegex))
+    return sanitizedStr;
+
+  let phNames;
+  if (args !== undefined)
+    phNames = args.map(arg => arg.replace(/[^a-zA-Z]/gm, '_').toUpperCase());
+  else
+    phNames = ['PH1', 'PH2', 'PH3', 'PH4', 'PH5', 'PH6', 'PH7', 'PH8', 'PH9'];
+
+  // It replaces all placeholders with <ph> tags.
+  let match;
+  let count = 1;
+  while ((match = phRegex.exec(sanitizedStr)) !== null) {
+    // This is necessary to avoid infinite loops with zero-width matches
+    if (match.index === phRegex.lastIndex)
+      phRegex.lastIndex++;
+
+    // match[0]: the placeholder (e.g. %d, %s, %.2f, etc.)
+    const ph = match[0];
+    // e.g. $1s, $1d, $1.2f
+    const newPh = `$${count}` + ph.substr(1);
+
+    const i = sanitizedStr.indexOf(ph);
+    sanitizedStr = `${sanitizedStr.substring(0, i)}<ph name="${phNames[count - 1]}">${newPh}</ph>${
+        sanitizedStr.substring(i + ph.length)}`;
+    count++;
+  }
+  return sanitizedStr;
+}
+
+function createGrdpMessage(ids, stringObj) {
+  let message = `  <message name="${ids}" desc="">\n`;
+  message += `    ${modifyStringIntoGRDFormat(stringObj.string, stringObj.arguments)}\n`;
+  message += '  </message>\n';
+  return message;
+}
+
+function getIDSKey(str) {
+  return `${IDSPrefix}${md5(str)}`
+}
+
+module.exports = {
+  createGrdpMessage,
+  escodegen,
+  esprima,
+  esprimaTypes,
+  getChildDirectoriesFromDirectory,
+  getFilesFromDirectory,
+  getGRDPFilePath,
+  getIDSKey,
+  getLocalizationCase,
+  getLocationMessage,
+  getRelativeFilePathFromSrc,
+  GRD_PATH,
+  IDSPrefix,
+  isLocalizationCall,
+  modifyStringIntoGRDFormat,
+  parseFileContent,
+  sanitizeStringIntoCppFormat,
+  sanitizeStringIntoFrontendFormat,
+  verifyFunctionCallee
+};
\ No newline at end of file
diff --git a/scripts/localization_utils/md5.js b/scripts/localization_utils/md5.js
new file mode 100644
index 0000000..3f59f37
--- /dev/null
+++ b/scripts/localization_utils/md5.js
@@ -0,0 +1,157 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+function md5(s) {
+  return binl2hex(core_md5(str2binl(s), s.length * 8));
+}
+
+function core_md5(x, len) {
+  /* append padding */
+  x[len >> 5] |= 0x80 << ((len) % 32);
+  x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+  var a = 1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d = 271733878;
+
+  for (var i = 0; i < x.length; i += 16) {
+    var olda = a;
+    var oldb = b;
+    var oldc = c;
+    var oldd = d;
+
+    a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
+    d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
+    c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
+    b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
+    a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
+    d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
+    c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
+    b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
+    a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
+    d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
+    c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
+    b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
+    a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
+    d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
+    c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
+    b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
+
+    a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
+    d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
+    c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
+    b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
+    a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
+    d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
+    c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
+    b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
+    a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
+    d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
+    c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
+    b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
+    a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
+    d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
+    c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
+    b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
+
+    a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
+    d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
+    c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
+    b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
+    a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
+    d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
+    c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
+    b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
+    a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
+    d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
+    c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
+    b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
+    a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
+    d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
+    c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
+    b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
+
+    a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
+    d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
+    c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
+    b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
+    a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
+    d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
+    c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
+    b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
+    a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
+    d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
+    c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
+    b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
+    a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
+    d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
+    c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
+    b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+  }
+  return Array(a, b, c, d);
+}
+
+function md5_cmn(q, a, b, x, s, t) {
+  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
+}
+
+function md5_ff(a, b, c, d, x, s, t) {
+  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+}
+
+function md5_gg(a, b, c, d, x, s, t) {
+  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+}
+
+function md5_hh(a, b, c, d, x, s, t) {
+  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+}
+
+function md5_ii(a, b, c, d, x, s, t) {
+  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+}
+
+function safe_add(x, y) {
+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xFFFF);
+}
+
+function bit_rol(num, cnt) {
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+function str2binl(str) {
+  var bin = Array();
+  var mask = (1 << 8) - 1;
+  for (var i = 0; i < str.length * 8; i += 8)
+    bin[i >> 5] |= (str.charCodeAt(i / 8) & mask) << (i % 32);
+  return bin;
+}
+
+function binl2hex(binarray) {
+  var hex_tab = '0123456789abcdef';
+  var str = '';
+  for (var i = 0; i < binarray.length * 4; i++) {
+    str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) +
+        hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF);
+  }
+  return str;
+}
+
+module.exports = md5;
\ No newline at end of file