| // 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; | |
| } |