|  | // 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 {promisify} = require('util'); | 
|  | const writeFileAsync = promisify(fs.writeFile); | 
|  | const appendFileAsync = promisify(fs.appendFile); | 
|  | const checkLocalizedStrings = require('./utils/check_localized_strings'); | 
|  | const localizationUtils = require('./utils/localization_utils'); | 
|  |  | 
|  | const grdpFileStart = '<?xml version="1.0" encoding="utf-8"?>\n<grit-part>\n'; | 
|  | const grdpFileEnd = '</grit-part>'; | 
|  |  | 
|  | async function main() { | 
|  | try { | 
|  | const shouldAutoFix = process.argv.includes('--autofix'); | 
|  | const error = await checkLocalizedStrings.validateGrdAndGrdpFiles(shouldAutoFix); | 
|  | if (error !== '' && !shouldAutoFix) | 
|  | throw new Error(error); | 
|  |  | 
|  | await checkLocalizedStrings.parseLocalizableResourceMaps(); | 
|  | if (shouldAutoFix) | 
|  | await autofix(error); | 
|  | else | 
|  | getErrors(); | 
|  | } catch (e) { | 
|  | console.log(e.stack); | 
|  | process.exit(1); | 
|  | } | 
|  | } | 
|  |  | 
|  | main(); | 
|  |  | 
|  | function getErrors(existingError) { | 
|  | const toAddError = checkLocalizedStrings.getAndReportResourcesToAdd(); | 
|  | const toModifyError = checkLocalizedStrings.getAndReportIDSKeysToModify(); | 
|  | const toRemoveError = checkLocalizedStrings.getAndReportResourcesToRemove(); | 
|  | let error = | 
|  | `${existingError ? `${existingError}\n` : ''}${toAddError || ''}${toModifyError || ''}${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(existingError) { | 
|  | const keysToAddToGRD = checkLocalizedStrings.getMessagesToAdd(); | 
|  | const keysToRemoveFromGRD = checkLocalizedStrings.getMessagesToRemove(); | 
|  | const resourceAdded = await addResourcesToGRDP(keysToAddToGRD, keysToRemoveFromGRD); | 
|  | const resourceModified = await modifyResourcesInGRDP(); | 
|  | const resourceRemoved = await removeResourcesFromGRDP(keysToRemoveFromGRD); | 
|  | const shouldAddExampleTag = checkShouldAddExampleTag(keysToAddToGRD); | 
|  |  | 
|  | if (!resourceAdded && !resourceRemoved && !resourceModified && existingError === '') { | 
|  | console.log('DevTools localizable resources checker passed.'); | 
|  | return; | 
|  | } | 
|  |  | 
|  | let message = | 
|  | 'Found changes to localizable DevTools resources.\nDevTools localizable resources checker has updated the appropriate grd/grdp file(s).'; | 
|  | if (existingError !== '') { | 
|  | message += | 
|  | `\nGrd/Grdp files have been updated. Please verify the updated grdp files and/or the <part> file references in ${ | 
|  | localizationUtils.getRelativeFilePathFromSrc(localizationUtils.GRD_PATH)} are correct.`; | 
|  | } | 
|  | if (resourceAdded) { | 
|  | message += '\nManually write a description for any new <message> entries.'; | 
|  | if (shouldAddExampleTag) { | 
|  | message += ' Add example tag(s) <ex> for messages that contain placeholder(s)'; | 
|  | } | 
|  | message += '\nFor more details, see devtools/docs/langpacks/grdp_files.md'; | 
|  | } | 
|  | if (resourceRemoved && duplicateRemoved(keysToRemoveFromGRD)) | 
|  | message += '\nDuplicate <message> entries are removed. Please verify the retained descriptions are correct.'; | 
|  | message += '\n' | 
|  | message += '\nUse git status to see what has changed.'; | 
|  | throw new Error(message); | 
|  | } | 
|  |  | 
|  | function checkShouldAddExampleTag(keys) { | 
|  | if (keys.size === 0) { | 
|  | return false; | 
|  | } | 
|  | const stringObjs = [...keys.values()]; | 
|  | const stringsWithArgument = stringObjs.filter(stringObj => !!stringObj.arguments); | 
|  | return stringsWithArgument.length > 0; | 
|  | } | 
|  |  | 
|  | function duplicateRemoved(keysToRemoveFromGRD) { | 
|  | for (const [_, messages] of keysToRemoveFromGRD) { | 
|  | if (messages.length > 1) | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Return true if any resources are added | 
|  | async function addResourcesToGRDP(keysToAddToGRD, keysToRemoveFromGRD) { | 
|  | function mapGRDPFilePathToStrings(keysToAddToGRD, keysToRemoveFromGRD) { | 
|  | const grdpFilePathToStrings = new Map(); | 
|  | // Get the grdp files that need to be modified | 
|  | for (const [key, stringObj] of keysToAddToGRD) { | 
|  | if (!grdpFilePathToStrings.has(stringObj.grdpPath)) | 
|  | grdpFilePathToStrings.set(stringObj.grdpPath, []); | 
|  |  | 
|  | // Add the IDS key to stringObj so we have access to it later | 
|  | stringObj.ids = key; | 
|  | // If the same key is to be removed, this is likely a string copy | 
|  | // to another folder. Keep the description. | 
|  | if (keysToRemoveFromGRD.has(key)) | 
|  | stringObj.description = checkLocalizedStrings.getLongestDescription(keysToRemoveFromGRD.get(key)); | 
|  | grdpFilePathToStrings.get(stringObj.grdpPath).push(stringObj); | 
|  | } | 
|  | return grdpFilePathToStrings; | 
|  | } | 
|  |  | 
|  | if (keysToAddToGRD.size === 0) | 
|  | return false; | 
|  |  | 
|  | // 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 = mapGRDPFilePathToStrings(keysToAddToGRD, keysToRemoveFromGRD); | 
|  | 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(grdpFilePath); | 
|  | 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(localizationUtils.addChildGRDPFilePathsToGRD(grdpFilePathsToAdd.sort())); | 
|  | await Promise.all(promises); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Return true if any resources are updated | 
|  | async function modifyResourcesInGRDP() { | 
|  | const messagesToModify = checkLocalizedStrings.getIDSKeysToModify(); | 
|  | if (messagesToModify.size === 0) | 
|  | return false; | 
|  |  | 
|  | const grdpToMessages = mapGRDPFilePathToMessages(messagesToModify); | 
|  | const promises = []; | 
|  | for (const [grdpPath, messages] of grdpToMessages) { | 
|  | let fileContent = await localizationUtils.parseFileContent(grdpPath); | 
|  | for (const message of messages) { | 
|  | const idsRegex = new RegExp(`name="${message.actualIDSKey}"`); | 
|  | fileContent = fileContent.replace(idsRegex, `name="${message.ids}"`); | 
|  | } | 
|  | promises.push(writeFileAsync(grdpPath, fileContent)); | 
|  | } | 
|  | await Promise.all(promises); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Return true if any resources are removed | 
|  | async function removeResourcesFromGRDP(keysToRemoveFromGRD) { | 
|  | function indexOfFirstMatchingMessage(line, messages) { | 
|  | for (let i = 0; i < messages.length; i++) { | 
|  | const message = messages[i]; | 
|  | const match = | 
|  | line.match(new RegExp(`<message[^>]*name="${message.ids}"[^>]*desc="${message.description}"[^>]*>`)); | 
|  | if (match) | 
|  | return i; | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | if (keysToRemoveFromGRD.size === 0) | 
|  | return false; | 
|  |  | 
|  | const grdpToMessages = mapGRDPFilePathToMessages(keysToRemoveFromGRD); | 
|  | const promises = []; | 
|  | for (const [grdpFilePath, messages] of grdpToMessages) { | 
|  | let newGrdpFileContent = ''; | 
|  | const grdpFileContent = await localizationUtils.parseFileContent(grdpFilePath); | 
|  | const grdpFileLines = grdpFileContent.split('\n'); | 
|  |  | 
|  | for (let i = 0; i < grdpFileLines.length; i++) { | 
|  | const index = indexOfFirstMatchingMessage(grdpFileLines[i], messages); | 
|  | if (index === -1) { | 
|  | newGrdpFileContent += grdpFileLines[i]; | 
|  | if (i < grdpFileLines.length - 1) | 
|  | newGrdpFileContent += '\n'; | 
|  | continue; | 
|  | } | 
|  |  | 
|  | messages.splice(index, 1); | 
|  | while (!grdpFileLines[i].includes('</message>')) | 
|  | i++; | 
|  | } | 
|  |  | 
|  | promises.push(writeFileAsync(grdpFilePath, newGrdpFileContent)); | 
|  | } | 
|  | await Promise.all(promises); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Given a map from IDS key to a list of messages, return a map | 
|  | // from grdp file path to a list of messages with a new property | 
|  | // `ids` set to the key. | 
|  | function mapGRDPFilePathToMessages(keyToMessages) { | 
|  | const grdpFilePathToMessages = new Map(); | 
|  | for (const [ids, messages] of keyToMessages) { | 
|  | for (const message of messages) { | 
|  | if (!grdpFilePathToMessages.has(message.grdpPath)) | 
|  | grdpFilePathToMessages.set(message.grdpPath, []); | 
|  |  | 
|  | message.ids = ids; | 
|  | grdpFilePathToMessages.get(message.grdpPath).push(message); | 
|  | } | 
|  | } | 
|  | return grdpFilePathToMessages; | 
|  | } |