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

This change includes...
* A main GRD file for the DevTools (front_end/langpacks/devtools_ui_strings.grd)
* GRDP files organized by subfolder
* A node script that keeps the GRD/GRDP files in sync with the keys in the DevTools frontend
* A git cl presubmit --upload check that runs the node script

Note: Subsequent changes will add a build step to generate a .pak file that contains these DevTools strings. You can read about the overall approach here (https://bugs.chromium.org/p/chromium/issues/detail?id=941561).


Details of this design:
======================
We followed a similar pattern used by WebUI where strings are encoded in GRIT GRD/P files, which are used by a localization service to perform translations. They are also consumed in the build step to generate a .pak file, which is loaded by the browser's resource system.

Chromium documentation:
* https://www.chromium.org/developers/tools-we-use-in-chromium/grit/grit-users-guide
* https://www.chromium.org/developers/design-documents/ui-localization

Frontend strings:
-----------------
These are the localizable strings that are displayed to the user.

GRDP <message> strings:
-----------------------
Each frontend string has a corresponding <message> entry in a GRDP file. These entries are what the localization service will use to perform localizations. It's also the input to the GRIT compiler, which generates a .pak file, which is loaded by the browser's resource_bundle system.

GRDP <messsage> placeholders:
----------------------------
Frontend strings use placeholders, which are used to substitute in values at runtime.
For example,
'This string has %s two placeholder %.2f.'

Since the order of the string may change in a different language, we need to encode the order of the placeholders. As such, in the GRDP file you'll find %s replaced with $[1-9].

For example,
'This string has <ph name="ph1">$1s</ph> two placeholder <ph name="ph2">$2.2f</ph>.'

Also, note that the precision and type of the placeholder is maintained (i.e. .2f).

Detecting changes:
-----------------
The check_localizable_resources.js script performs the following check and generates an error if there are any changes that need to be made to a GRDP file.

1. Parses the frontend strings and hashes them.
2. Reads the messages from the GRDP files and hashes them.
3. Uses a difference between these two sets to report which strings need to be added and/or removed from GRDP files.

Optionally, the user can specify --autofix and it will automatically update the appropriate GRDP files.

Presubmit check:
---------------
Running git cl presubmit --upload will run the check_localizable_resources.js script with the --autofix argument.

If there are any changes, they reported to the user like this.

** Presubmit ERRORS **
Error: Found changes to localizable DevTools strings.
DevTools localizable resources checker has updated the appropriate grdp file(s).
Manually write a description for any new <message> entries.
Use git status to see what has changed.

BUG=941561

Change-Id: I5ac1656a037a6aaffeb4f64b103c4daec28be39a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1613921
Reviewed-by: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: Alexei Filippov <alph@chromium.org>
Commit-Queue: Lorne Mitchell <lomitch@microsoft.com>
Cr-Original-Commit-Position: refs/heads/master@{#662371}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 2f05fdbf2ab3458b4b3a3f2a86f908a3a7d04f79
diff --git a/scripts/check_localizability.js b/scripts/check_localizability.js
index 963eb49..30102a7 100644
--- a/scripts/check_localizability.js
+++ b/scripts/check_localizability.js
@@ -11,21 +11,11 @@
 // In this case, add it to the excluded errors at the top of the script.
 
 const path = require('path');
+const localizationUtils = require('./localization_utils/localization_utils');
+const esprimaTypes = localizationUtils.esprimaTypes;
+const escodegen = localizationUtils.escodegen;
+const esprima = localizationUtils.esprima;
 
-// Use modules in third_party/node/node_modules
-const THIRD_PARTY_PATH = path.resolve(__dirname, '..', '..', '..', '..');
-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'));
-
-const fs = require('fs');
-const {promisify} = require('util');
-const readDirAsync = promisify(fs.readdir);
-const readFileAsync = promisify(fs.readFile);
-const statAsync = promisify(fs.stat);
-
-const excludeFiles = ['lighthouse-dt-bundle.js', 'Tests.js'];
-const excludeDirs = ['_test_runner', 'Images', 'node_modules'];
 // Exclude known errors
 const excludeErrors = [
   'Common.UIString(view.title())', 'Common.UIString(setting.title() || \'\')', 'Common.UIString(option.text)',
@@ -34,16 +24,6 @@
   'Common.UIString(extension.title())', 'Common.UIString(this._currentValueLabel, value)'
 ];
 
-const esprimaTypes = {
-  BI_EXPR: 'BinaryExpression',
-  CALL_EXPR: 'CallExpression',
-  COND_EXPR: 'ConditionalExpression',
-  IDENTIFIER: 'Identifier',
-  MEMBER_EXPR: 'MemberExpression',
-  TAGGED_TEMP_EXPR: 'TaggedTemplateExpression',
-  TEMP_LITERAL: 'TemplateLiteral'
-};
-
 const usage = `Usage: node ${path.basename(process.argv[0])} [-a | <.js file path>*]
 
 -a: If present, check all devtools frontend .js files
@@ -62,7 +42,7 @@
     let filePaths = [];
     if (process.argv[2] === '-a') {
       const frontendPath = path.resolve(__dirname, '..', 'front_end');
-      await getFilesFromDirectory(frontendPath, filePaths);
+      await localizationUtils.getFilesFromDirectory(frontendPath, filePaths, ['.js']);
     } else {
       filePaths = process.argv.slice(2);
     }
@@ -86,73 +66,15 @@
 
 main();
 
-function verifyIdentifier(node, name) {
-  return node !== undefined && node.type === esprimaTypes.IDENTIFIER && node.name === name;
-}
-
-/**
- * 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 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;
-}
-
 function includesConditionalExpression(listOfElements) {
   return listOfElements.filter(ele => ele !== undefined && ele.type === esprimaTypes.COND_EXPR).length > 0;
 }
 
-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);
-}
-
 function addError(error, errors) {
   if (!errors.includes(error))
     errors.push(error);
 }
 
-function getLocation(node) {
-  if (node !== undefined && node.loc !== undefined && node.loc.start !== undefined && node.loc.end !== undefined &&
-      node.loc.start.line !== undefined && node.loc.end.line !== undefined) {
-    const startLine = node.loc.start.line;
-    const endLine = node.loc.end.line;
-    if (startLine === endLine)
-      return ` Line ${startLine}`;
-    else
-      return ` Line ${node.loc.start.line}-${node.loc.end.line}`;
-  }
-  return '';
-}
-
 function buildConcatenatedNodesList(node, nodes) {
   if (!node)
     return;
@@ -173,7 +95,7 @@
  */
 function checkConcatenation(parentNode, node, filePath, errors) {
   function isWord(node) {
-    return (node.type === 'Literal' && !!node.value.match(/[a-z]/i));
+    return (node.type === esprimaTypes.LITERAL && !!node.value.match(/[a-z]/i));
   }
   function isConcatenation(node) {
     return (node !== undefined && node.type === esprimaTypes.BI_EXPR && node.operator === '+');
@@ -183,16 +105,18 @@
     return;
 
   if (isConcatenation(node)) {
-    let concatenatedNodes = [];
+    const concatenatedNodes = [];
     buildConcatenatedNodesList(node, concatenatedNodes);
-    const hasLocalizationCall = !!concatenatedNodes.find(currentNode => isLocalizationCall(currentNode));
+    const hasLocalizationCall =
+        !!concatenatedNodes.find(currentNode => localizationUtils.isLocalizationCall(currentNode));
     if (hasLocalizationCall) {
       const hasAlphabeticLiteral = !!concatenatedNodes.find(currentNode => isWord(currentNode));
       if (hasAlphabeticLiteral) {
         const code = escodegen.generate(node);
         addError(
             `${filePath}${
-                getLocation(node)}: string concatenation should be changed to variable substitution with ls: ${code}`,
+                localizationUtils.getLocationMessage(
+                    node.loc)}: string concatenation should be changed to variable substitution with ls: ${code}`,
             errors);
       }
     }
@@ -200,26 +124,18 @@
 }
 
 /**
- * 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)));
-}
-
-/**
  * Check if an argument of a function is localized.
  */
 function checkFunctionArgument(functionName, argumentIndex, node, filePath, errors) {
-  if (node !== undefined && node.type === esprimaTypes.CALL_EXPR && verifyFunctionCallee(node.callee, functionName) &&
-      node.arguments !== undefined && node.arguments.length > argumentIndex) {
+  if (node !== undefined && node.type === esprimaTypes.CALL_EXPR &&
+      localizationUtils.verifyFunctionCallee(node.callee, functionName) && node.arguments !== undefined &&
+      node.arguments.length > argumentIndex) {
     const arg = node.arguments[argumentIndex];
     // No need to localize empty strings.
-    if (arg.type == 'Literal' && arg.value === '')
+    if (arg.type === esprimaTypes.LITERAL && arg.value === '')
       return;
 
-    if (!isLocalizationCall(arg)) {
+    if (!localizationUtils.isLocalizationCall(arg)) {
       let order = '';
       switch (argumentIndex) {
         case 0:
@@ -235,8 +151,8 @@
           order = `${argumentIndex + 1}th`;
       }
       addError(
-          `${filePath}${getLocation(node)}: ${order} argument to ${functionName}() should be localized: ${
-              escodegen.generate(node)}`,
+          `${filePath}${localizationUtils.getLocationMessage(node.loc)}: ${order} argument to ${
+              functionName}() should be localized: ${escodegen.generate(node)}`,
           errors);
     }
   }
@@ -266,19 +182,22 @@
     return;
   }
 
-  const locCase = getLocalizationCase(node);
+  const locCase = localizationUtils.getLocalizationCase(node);
   const code = escodegen.generate(node);
   switch (locCase) {
     case 'Common.UIString':
     case 'UI.formatLocalized':
       const firstArgType = node.arguments[0].type;
-      if (firstArgType !== 'Literal' && firstArgType !== 'TemplateLiteral' && firstArgType !== 'Identifier' &&
-          !excludeErrors.includes(code)) {
-        addError(`${filePath}${getLocation(node)}: first argument to call should be a string: ${code}`, errors);
+      if (firstArgType !== esprimaTypes.LITERAL && firstArgType !== esprimaTypes.TEMP_LITERAL &&
+          firstArgType !== esprimaTypes.IDENTIFIER && !excludeErrors.includes(code)) {
+        addError(
+            `${filePath}${localizationUtils.getLocationMessage(node.loc)}: first argument to call should be a string: ${
+                code}`,
+            errors);
       }
       if (includesConditionalExpression(node.arguments.slice(1))) {
         addError(
-            `${filePath}${getLocation(node)}: conditional(s) found in ${
+            `${filePath}${localizationUtils.getLocationMessage(node.loc)}: conditional(s) found in ${
                 code}. Please extract conditional(s) out of the localization call.`,
             errors);
       }
@@ -286,7 +205,7 @@
     case 'Tagged Template':
       if (includesConditionalExpression(node.quasi.expressions)) {
         addError(
-            `${filePath}${getLocation(node)}: conditional(s) found in ${
+            `${filePath}${localizationUtils.getLocationMessage(node.loc)}: conditional(s) found in ${
                 code}. Please extract conditional(s) out of the localization call.`,
             errors);
       }
@@ -303,42 +222,11 @@
   }
 }
 
-function getRelativeFilePathFromSrc(fullFilePath) {
-  return path.relative(path.resolve(THIRD_PARTY_PATH, '..'), fullFilePath);
-}
-
 async function auditFileForLocalizability(filePath, errors) {
-  const fileContent = await readFileAsync(filePath);
-  const ast = esprima.parse(fileContent.toString(), {loc: true});
+  const fileContent = await localizationUtils.parseFileContent(filePath);
+  const ast = esprima.parse(fileContent, {loc: true});
 
-  const relativeFilePath = getRelativeFilePathFromSrc(filePath);
+  const relativeFilePath = localizationUtils.getRelativeFilePathFromSrc(filePath);
   for (const node of ast.body)
     analyzeNode(undefined, node, relativeFilePath, errors);
 }
-
-function shouldParseDirectory(directoryName) {
-  return !excludeDirs.reduce((result, dir) => result || directoryName.indexOf(dir) !== -1, false);
-}
-
-function shouldParseFile(filePath) {
-  return (path.extname(filePath) === '.js' && !excludeFiles.includes(path.basename(filePath)));
-}
-
-async function getFilesFromItem(itemPath, filePaths) {
-  const stat = await statAsync(itemPath);
-  if (stat.isDirectory() && shouldParseDirectory(itemPath))
-    return await getFilesFromDirectory(itemPath, filePaths);
-
-  if (shouldParseFile(itemPath))
-    filePaths.push(itemPath);
-}
-
-async function getFilesFromDirectory(directoryPath, filePaths) {
-  const itemNames = await readDirAsync(directoryPath);
-  const promises = [];
-  for (const itemName of itemNames) {
-    const itemPath = path.resolve(directoryPath, itemName);
-    promises.push(getFilesFromItem(itemPath, filePaths));
-  }
-  await Promise.all(promises);
-}
diff --git a/scripts/check_localizable_resources.js b/scripts/check_localizable_resources.js
new file mode 100644
index 0000000..946cbcf
--- /dev/null
+++ b/scripts/check_localizable_resources.js
@@ -0,0 +1,240 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Copyright (C) Microsoft Corporation. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * This script is part of the presubmit check that parses DevTools frontend .js and
+ * module.json files, collects localizable strings, checks if frontend strings are
+ * in .grd/.grdp files and reports error if present.
+ *
+ * If argument '--autofix' is present, add the new resources to and remove unused
+ * messages from GRDP files.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const {promisify} = require('util');
+const writeFileAsync = promisify(fs.writeFile);
+const appendFileAsync = promisify(fs.appendFile);
+const checkLocalizedStrings = require('./localization_utils/check_localized_strings');
+const localizationUtils = require('./localization_utils/localization_utils');
+
+const grdpFileStart = '<?xml version="1.0" encoding="utf-8"?>\n<grit-part>\n';
+const grdpFileEnd = '</grit-part>';
+
+async function main() {
+  let frontendStrings;
+  let IDSkeys;
+
+  try {
+    [frontendStrings, IDSkeys] = await checkLocalizedStrings.parseLocalizableResourceMaps(false);
+
+    if (process.argv.includes('--autofix')) {
+      await autofix(frontendStrings, IDSkeys);
+    } else {
+      await getErrors(frontendStrings, IDSkeys);
+    }
+  } catch (e) {
+    console.log(`Error: ${e.message}`);
+    process.exit(1);
+  }
+}
+
+main();
+
+async function getErrors(frontendStrings, IDSkeys) {
+  const toAddError = await checkLocalizedStrings.getAndReportResourcesToAdd(frontendStrings, IDSkeys);
+  const toRemoveError = checkLocalizedStrings.getAndReportResourcesToRemove(frontendStrings, IDSkeys);
+  let error = `${toAddError ? toAddError : ''}${toRemoveError ? toRemoveError : ''}`;
+
+  if (error === '') {
+    console.log('DevTools localizable resources checker passed.');
+    return;
+  }
+
+  error += '\nThe errors are potentially fixable with the `--autofix` option.';
+
+  throw new Error(error);
+}
+
+async function autofix(frontendStrings, IDSkeys) {
+  const resourceAdded = await addResourcesToGRDP(frontendStrings, IDSkeys);
+  const resourceRemoved = await removeResourcesFromGRDP(frontendStrings, IDSkeys);
+
+  if (!resourceAdded && !resourceRemoved) {
+    console.log('DevTools localizable resources checker passed.');
+    return;
+  }
+
+  let message =
+      'Found changes to localizable DevTools strings.\nDevTools localizable resources checker has updated the appropriate grdp file(s).';
+  if (resourceAdded)
+    message += '\nManually write a description for any new <message> entries.';
+  message += '\nUse git status to see what has changed.';
+  throw new Error(message);
+}
+
+// Return true if any resources are added
+async function addResourcesToGRDP(frontendStrings, IDSkeys) {
+  const keysToAddToGRD = checkLocalizedStrings.getDifference(IDSkeys, frontendStrings);
+  if (keysToAddToGRD.size === 0)
+    return false;
+
+  const frontendPath = path.resolve(__dirname, '..', 'front_end');
+  const frontendDirs = await localizationUtils.getChildDirectoriesFromDirectory(frontendPath);
+  // Map file path to its parent grdp file path
+  const filePathToGrdpFilePath = new Map();
+  // Map grdp file path to strings to be added to that file so that we only need to
+  // modify every grdp file once
+  const grdpFilePathToStrings = new Map();
+
+  // Get the grdp files that need to be modified
+  for (const [key, stringObj] of keysToAddToGRD) {
+    let grdpFilePath = '';
+    if (filePathToGrdpFilePath.has(stringObj.filepath)) {
+      grdpFilePath = filePathToGrdpFilePath.get(stringObj.filepath);
+    } else {
+      grdpFilePath = localizationUtils.getGRDPFilePath(stringObj.filepath, frontendDirs);
+      filePathToGrdpFilePath.set(stringObj.filepath, grdpFilePath);
+    }
+
+    if (!grdpFilePathToStrings.has(grdpFilePath))
+      grdpFilePathToStrings.set(grdpFilePath, []);
+
+    // Add the IDS key to stringObj so we have access to it later
+    stringObj.ids = key;
+    grdpFilePathToStrings.get(grdpFilePath).push(stringObj);
+  }
+
+  const promises = [];
+
+  const grdpFilePathsToAdd = [];
+  for (let [grdpFilePath, stringsToAdd] of grdpFilePathToStrings) {
+    // The grdp file doesn't exist, so create one.
+    if (!fs.existsSync(grdpFilePath)) {
+      let grdpMessagesToAdd = '';
+      for (const stringObj of stringsToAdd)
+        grdpMessagesToAdd += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
+
+      // Create a new grdp file and reference it in the parent grd file
+      promises.push(appendFileAsync(grdpFilePath, `${grdpFileStart}${grdpMessagesToAdd}${grdpFileEnd}`));
+      grdpFilePathsToAdd.push(
+          path.relative(path.dirname(localizationUtils.GRD_PATH), grdpFilePath).split(path.sep).join('/'));
+      continue;
+    }
+
+    const grdpFileContent = await localizationUtils.parseFileContent(grdpFilePath);
+    const grdpFileLines = grdpFileContent.split('\n');
+
+    let newGrdpFileContent = '';
+    const IDSRegex = new RegExp(`"(${localizationUtils.IDSPrefix}.*?)"`);
+    for (let i = 0; i < grdpFileLines.length; i++) {
+      const grdpLine = grdpFileLines[i];
+      const match = grdpLine.match(IDSRegex);
+      // match[0]: full match
+      // match[1]: message IDS key
+      if (match) {
+        const ids = match[1];
+        const stringsToAddRemaining = [];
+        for (const stringObj of stringsToAdd) {
+          // Insert the new <message> in sorted order.
+          if (ids > stringObj.ids)
+            newGrdpFileContent += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
+          else
+            stringsToAddRemaining.push(stringObj);
+        }
+        stringsToAdd = stringsToAddRemaining;
+      } else if (grdpLine.includes(grdpFileEnd)) {
+        // Just hit the end tag, so insert any remaining <message>s.
+        for (const stringObj of stringsToAdd)
+          newGrdpFileContent += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
+      }
+      newGrdpFileContent += grdpLine;
+      if (i < grdpFileLines.length - 1)
+        newGrdpFileContent += '\n';
+    }
+
+    promises.push(writeFileAsync(grdpFilePath, newGrdpFileContent));
+  }
+  promises.push(addChildGRDPFilePathsToGRD(grdpFilePathsToAdd.sort()));
+  await Promise.all(promises);
+  return true;
+}
+
+async function addChildGRDPFilePathsToGRD(relativeGrdpFilePaths) {
+  function createPartFileEntry(relativeGrdpFilePath) {
+    return `      <part file="${relativeGrdpFilePath}" />\n`;
+  }
+
+  const grdFileContent = await localizationUtils.parseFileContent(localizationUtils.GRD_PATH);
+  const grdLines = grdFileContent.split('\n');
+
+  let newGrdFileContent = '';
+  for (let i = 0; i < grdLines.length; i++) {
+    const grdLine = grdLines[i];
+    // match[0]: full match
+    // match[1]: relative grdp file path
+    const match = grdLine.match(/<part file="(.*?)"/);
+    if (match) {
+      const relativeGrdpFilePathsRemaining = [];
+      for (const relativeGrdpFilePath of relativeGrdpFilePaths) {
+        if (relativeGrdpFilePath < match[1])
+          newGrdFileContent += createPartFileEntry(relativeGrdpFilePath);
+        else
+          relativeGrdpFilePathsRemaining.push(relativeGrdpFilePath);
+      }
+      relativeGrdpFilePaths = relativeGrdpFilePathsRemaining;
+    } else if (grdLine.includes('</messages>')) {
+      for (const relativeGrdpFilePath of relativeGrdpFilePaths)
+        newGrdFileContent += createPartFileEntry(relativeGrdpFilePath);
+    }
+    newGrdFileContent += grdLine;
+    if (i < grdLines.length - 1)
+      newGrdFileContent += '\n';
+  }
+  return writeFileAsync(localizationUtils.GRD_PATH, newGrdFileContent);
+}
+
+// Return true if any resources are removed
+async function removeResourcesFromGRDP(frontendStrings, IDSkeys) {
+  const keysToRemoveFromGRD = checkLocalizedStrings.getDifference(frontendStrings, IDSkeys);
+  if (keysToRemoveFromGRD.size === 0)
+    return false;
+
+  // Map grdp file path to message IDS keys to remove
+  const grdpFilePathToKeys = new Map();
+  for (const [key, messageObj] of keysToRemoveFromGRD) {
+    if (!grdpFilePathToKeys.has(messageObj.filepath))
+      grdpFilePathToKeys.set(messageObj.filepath, []);
+
+    grdpFilePathToKeys.get(messageObj.filepath).push(key);
+  }
+
+  const promises = [];
+  for (const [grdpFilePath, listOfKeys] of grdpFilePathToKeys) {
+    let newGrdpFileContent = '';
+    const grdpFileContent = await localizationUtils.parseFileContent(grdpFilePath);
+    const grdpFileLines = grdpFileContent.split('\n');
+
+    for (let i = 0; i < grdpFileLines.length; i++) {
+      if (!lineContainsIDS(grdpFileLines[i], listOfKeys)) {
+        newGrdpFileContent += grdpFileLines[i];
+        if (i < grdpFileLines.length - 1)
+          newGrdpFileContent += '\n';
+        continue;
+      }
+
+      while (!grdpFileLines[i].includes('</message>'))
+        i++;
+    }
+
+    promises.push(writeFileAsync(grdpFilePath, newGrdpFileContent));
+  }
+  await Promise.all(promises);
+  return true;
+}
+
+function lineContainsIDS(line, listOfIDS) {
+  return listOfIDS.some(ids => line.includes(ids));
+}
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