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, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
+function sanitizeStringIntoFrontendFormat(str) {
+ return str.replace(/'/g, '\'')
+ .replace(/"/g, '"')
+ .replace(/>/g, '>')
+ .replace(/</g, '<')
+ .replace(/&/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