Files
react-native-devtools-frontend/scripts/localization/check_localizable_resources.js
T
Christy Chen 465b102104 [Loc] Fix <LOCKED> and <ex> get removed when messages move across grdps
When the presubmit moves a message from one grdp file to another, the <ph name=LOCKED> and <ex> tags are lost because all the messages are re-created from the frontend version of the string.
This change preserve the grd version of the strings that contains all the tags and use it when the messages are moving across different grdp files.

To test this:
In DeviceModeToolbar.js Line 351
Change
ls`Close DevTools`
To
'Close DevTools'

In WarningErrorCounter.js Line 163
Change
ls`Issues pertaining to ${issues} operation detected.`
To
ls`Close DevTools`

Run presubmit and see the tags are preserved.

Bug: 1071004
Change-Id: I5c724f0dc025dbfd06963373a8cb5fe7caf52803
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2275727
Commit-Queue: Christy Chen <chrche@microsoft.com>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Reviewed-by: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: Vidal Diazleal <vidorteg@microsoft.com>
2020-07-01 00:30:49 +00:00

291 lines
10 KiB
JavaScript

// Copyright 2020 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.
/**
* 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 src/docs/localization/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 string from grd to preserve all <ex> and <ph> tags and the description.
if (keysToRemoveFromGRD.has(key)) {
const existingMessage = keysToRemoveFromGRD.get(key);
stringObj.grdString = existingMessage[0].grdString;
stringObj.description = checkLocalizedStrings.getLongestDescription(existingMessage);
}
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 (const [grdpFilePath, initialStringsToAdd] of grdpFilePathToStrings) {
let stringsToAdd = initialStringsToAdd;
// 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="(.*)?"[^>]*>`));
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;
}