blob: 90dd6b6af56ab8e5555314d553837de0acb2ed87 [file] [log] [blame]
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Functions in this script parse DevTools frontend .js and module.json files,
7 * collect localizable strings, check if frontend strings are in .grd/.grdp
8 * files and report error if present.
9 */
10
Mandy Chen5128cc62019-09-23 16:46:00 +000011const fs = require('fs');
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000012const path = require('path');
Mandy Chen5128cc62019-09-23 16:46:00 +000013const {promisify} = require('util');
14const writeFileAsync = promisify(fs.writeFile);
15const renameFileAsync = promisify(fs.rename);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000016const localizationUtils = require('./localization_utils');
17const escodegen = localizationUtils.escodegen;
18const esprimaTypes = localizationUtils.esprimaTypes;
19const esprima = localizationUtils.esprima;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000020const extensionStringKeys = ['category', 'destination', 'title', 'title-mac'];
21
22// Format of frontendStrings
23// { IDS_md5-hash => {
24// string: string,
25// code: string,
Mandy Chen1e9d87b2019-09-18 17:18:15 +000026// isShared: boolean,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000027// filepath: string,
Mandy Chenc94d52a2019-06-11 22:51:53 +000028// grdpPath: string,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000029// location: {
30// start: {
31// line: number, (1-based)
32// column: number (0-based)
33// },
34// end: {
35// line: number,
36// column: number
37// }
38// },
39// arguments: string[]
40// }
41// }
42const frontendStrings = new Map();
43
44// Format
45// {
Mandy Chen4a7ad052019-07-16 16:09:29 +000046// IDS_KEY => a list of {
Mandy Chen81d4fc42019-07-11 23:12:02 +000047// actualIDSKey: string, // the IDS key in the message tag
Mandy Chenc94d52a2019-06-11 22:51:53 +000048// description: string,
Mandy Chen4a7ad052019-07-16 16:09:29 +000049// grdpPath: string,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000050// location: {
51// start: {
52// line: number
53// },
54// end: {
55// line: number
56// }
57// }
58// }
59// }
60const IDSkeys = new Map();
Mandy Chenc94d52a2019-06-11 22:51:53 +000061const fileToGRDPMap = new Map();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000062
63const devtoolsFrontendPath = path.resolve(__dirname, '..', '..', 'front_end');
Mandy Chen5128cc62019-09-23 16:46:00 +000064let devtoolsFrontendDirs;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000065
Mandy Chen5128cc62019-09-23 16:46:00 +000066/**
67 * The following functions validate and update grd/grdp files.
68 */
69
70async function validateGrdAndGrdpFiles(shouldAutoFix) {
71 const grdError = await validateGrdFile(shouldAutoFix);
72 const grdpError = await validateGrdpFiles(shouldAutoFix);
73 if (grdError !== '' || grdpError !== '')
74 return `${grdError}\n${grdpError}`;
75 else
76 return '';
77}
78
79function expectedGrdpFilePath(dir) {
80 return path.resolve(dir, `${path.basename(dir)}_strings.grdp`);
81}
82
83async function validateGrdFile(shouldAutoFix) {
84 const fileContent = await localizationUtils.parseFileContent(localizationUtils.GRD_PATH);
85 const fileLines = fileContent.split('\n');
86 const newLines = [];
87 let errors = '';
88 fileLines.forEach(line => errors += validateGrdLine(line, newLines));
89 if (errors !== '' && shouldAutoFix)
90 await writeFileAsync(localizationUtils.GRD_PATH, newLines.join('\n'));
91 return errors;
92}
93
94function validateGrdLine(line, newLines) {
95 let error = '';
96 const match = line.match(/<part file="([^"]*)" \/>/);
97 if (!match) {
98 newLines.push(line);
99 return error;
100 }
101 // match[0]: full match
102 // match[1]: relative grdp file path
103 const grdpFilePath = localizationUtils.getAbsoluteGrdpPath(match[1]);
104 const expectedGrdpFile = expectedGrdpFilePath(path.dirname(grdpFilePath));
105 if (fs.existsSync(grdpFilePath) &&
106 (grdpFilePath === expectedGrdpFile || grdpFilePath === localizationUtils.SHARED_STRINGS_PATH)) {
107 newLines.push(line);
108 return error;
109 } else if (!fs.existsSync(grdpFilePath)) {
110 error += `${line.trim()} in ${
111 localizationUtils.getRelativeFilePathFromSrc(
112 localizationUtils.GRD_PATH)} refers to a grdp file that doesn't exist. ` +
113 `Please verify the grdp file and update the <part file="..."> entry to reference the correct grdp file. ` +
114 `Make sure the grdp file name is ${path.basename(expectedGrdpFile)}.`
115 } else {
116 error += `${line.trim()} in ${
117 localizationUtils.getRelativeFilePathFromSrc(localizationUtils.GRD_PATH)} should reference "${
118 localizationUtils.getRelativeGrdpPath(expectedGrdpFile)}".`;
119 }
120 return error;
121}
122
123async function validateGrdpFiles(shouldAutoFix) {
124 const frontendDirsToGrdpFiles = await mapFrontendDirsToGrdpFiles();
125 const grdFileContent = await localizationUtils.parseFileContent(localizationUtils.GRD_PATH);
126 let errors = '';
127 const renameFilePromises = [];
128 const grdpFilesToAddToGrd = [];
129 frontendDirsToGrdpFiles.forEach(
130 (grdpFiles, dir) => errors +=
131 validateGrdpFile(dir, grdpFiles, grdFileContent, shouldAutoFix, renameFilePromises, grdpFilesToAddToGrd));
132 if (grdpFilesToAddToGrd.length > 0)
133 await localizationUtils.addChildGRDPFilePathsToGRD(grdpFilesToAddToGrd.sort());
134 await Promise.all(renameFilePromises);
135 return errors;
136}
137
138async function mapFrontendDirsToGrdpFiles() {
139 devtoolsFrontendDirs =
140 devtoolsFrontendDirs || await localizationUtils.getChildDirectoriesFromDirectory(devtoolsFrontendPath);
141 const dirToGrdpFiles = new Map();
142 const getGrdpFilePromises = devtoolsFrontendDirs.map(dir => {
143 const files = [];
144 dirToGrdpFiles.set(dir, files);
145 return localizationUtils.getFilesFromDirectory(dir, files, ['.grdp']);
146 });
147 await Promise.all(getGrdpFilePromises);
148 return dirToGrdpFiles;
149}
150
151function validateGrdpFile(dir, grdpFiles, grdFileContent, shouldAutoFix, renameFilePromises, grdpFilesToAddToGrd) {
152 let error = '';
153 const expectedGrdpFile = expectedGrdpFilePath(dir);
154 if (grdpFiles.length === 0)
155 return error;
156 if (grdpFiles.length > 1) {
157 throw new Error(`${grdpFiles.length} GRDP files found under ${
158 localizationUtils.getRelativeFilePathFromSrc(dir)}. Please make sure there's only one GRDP file named ${
159 path.basename(expectedGrdpFile)} under this directory.`);
160 }
161
162 // Only one grdp file is under the directory
163 if (grdpFiles[0] !== expectedGrdpFile) {
164 // Rename grdp file and the reference in the grd file
165 if (shouldAutoFix) {
166 renameFilePromises.push(renameFileAsync(grdpFiles[0], expectedGrdpFile));
167 grdpFilesToAddToGrd.push(expectedGrdpFile);
168 } else {
169 error += `${localizationUtils.getRelativeFilePathFromSrc(grdpFiles[0])} should be renamed to ${
170 localizationUtils.getRelativeFilePathFromSrc(expectedGrdpFile)}.`;
171 }
172 return error;
173 }
174
175 // Only one grdp file and its name follows the naming convention
176 if (!grdFileContent.includes(localizationUtils.getRelativeGrdpPath(grdpFiles[0]))) {
177 if (shouldAutoFix) {
178 grdpFilesToAddToGrd.push(grdpFiles[0]);
179 } else {
180 error += `Please add ${localizationUtils.createPartFileEntry(grdpFiles[0]).trim()} to ${
181 localizationUtils.getRelativeFilePathFromSrc(grdpFiles[0])}.`;
182 }
183 }
184 return error;
185}
186
187/**
188 * Parse localizable resources.
189 */
Mandy Chen4a7ad052019-07-16 16:09:29 +0000190async function parseLocalizableResourceMaps() {
Mandy Chen08fdf042019-09-23 19:57:20 +0000191 if (frontendStrings.size === 0 && IDSkeys.size === 0)
192 await parseLocalizableResourceMapsHelper();
193 return [frontendStrings, IDSkeys];
194}
195
196async function parseLocalizableResourceMapsHelper() {
Mandy Chenc94d52a2019-06-11 22:51:53 +0000197 const grdpToFiles = new Map();
Mandy Chen5128cc62019-09-23 16:46:00 +0000198 const dirs = devtoolsFrontendDirs || await localizationUtils.getChildDirectoriesFromDirectory(devtoolsFrontendPath);
Mandy Chenc94d52a2019-06-11 22:51:53 +0000199 const grdpToFilesPromises = dirs.map(dir => {
200 const files = [];
Mandy Chen5128cc62019-09-23 16:46:00 +0000201 grdpToFiles.set(expectedGrdpFilePath(dir), files);
Mandy Chenc94d52a2019-06-11 22:51:53 +0000202 return localizationUtils.getFilesFromDirectory(dir, files, ['.js', 'module.json']);
203 });
204 await Promise.all(grdpToFilesPromises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000205
Mandy Chen4a7ad052019-07-16 16:09:29 +0000206 const promises = [];
Mandy Chenc94d52a2019-06-11 22:51:53 +0000207 for (const [grdpPath, files] of grdpToFiles) {
208 files.forEach(file => fileToGRDPMap.set(file, grdpPath));
Mandy Chen4a7ad052019-07-16 16:09:29 +0000209 promises.push(parseLocalizableStrings(files));
Mandy Chenc94d52a2019-06-11 22:51:53 +0000210 }
211 await Promise.all(promises);
Mandy Chen4a7ad052019-07-16 16:09:29 +0000212 // Parse grd(p) files after frontend strings are processed so we know
213 // what to add or remove based on frontend strings
Mandy Chen5128cc62019-09-23 16:46:00 +0000214 await parseIDSKeys();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000215}
216
217/**
Mandy Chen7a8829b2019-06-25 22:13:07 +0000218 * The following functions parse localizable strings (wrapped in Common.UIString,
219 * Common.UIStringFormat, UI.formatLocalized or ls``) from devtools frontend files.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000220 */
221
Mandy Chen4a7ad052019-07-16 16:09:29 +0000222async function parseLocalizableStrings(devtoolsFiles) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000223 const promises = devtoolsFiles.map(filePath => parseLocalizableStringsFromFile(filePath));
224 await Promise.all(promises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000225}
226
227async function parseLocalizableStringsFromFile(filePath) {
228 const fileContent = await localizationUtils.parseFileContent(filePath);
229 if (path.basename(filePath) === 'module.json')
230 return parseLocalizableStringFromModuleJson(fileContent, filePath);
231
Mandy Chen436efc72019-09-18 17:43:40 +0000232 let ast;
233 try {
234 ast = esprima.parseModule(fileContent, {loc: true});
235 } catch (e) {
236 throw new Error(
237 `DevTools localization parser failed:\n${localizationUtils.getRelativeFilePathFromSrc(filePath)}: ${
238 e.message}` +
239 `\nThis error is likely due to unsupported JavaScript features.` +
240 ` Such features are not supported by eslint either and will cause presubmit to fail.` +
241 ` Please update the code and use official JavaScript features.`);
242 }
243 for (const node of ast.body) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000244 parseLocalizableStringFromNode(node, filePath);
Mandy Chen436efc72019-09-18 17:43:40 +0000245 }
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000246}
247
248function parseLocalizableStringFromModuleJson(fileContent, filePath) {
249 const fileJSON = JSON.parse(fileContent);
250 if (!fileJSON.extensions)
251 return;
252
253 for (const extension of fileJSON.extensions) {
254 for (const key in extension) {
255 if (extensionStringKeys.includes(key)) {
256 addString(extension[key], extension[key], filePath);
257 } else if (key === 'device') {
258 addString(extension.device.title, extension.device.title, filePath);
259 } else if (key === 'options') {
260 for (const option of extension.options) {
261 addString(option.title, option.title, filePath);
262 if (option.text !== undefined)
263 addString(option.text, option.text, filePath);
264 }
Mandy Chen609679b2019-09-10 16:04:08 +0000265 } else if (key === 'defaultValue' && Array.isArray(extension[key])) {
266 for (const defaultVal of extension[key]) {
267 if (defaultVal.title)
268 addString(defaultVal.title, defaultVal.title, filePath);
269 }
Christy Chenfc8ed9f2019-09-19 22:18:44 +0000270 } else if (key === 'tags' && extension[key]) {
271 const tagsList = extension[key].split(',');
272 for (let tag of tagsList) {
273 tag = tag.trim();
274 addString(tag, tag, filePath);
275 }
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000276 }
277 }
278 }
279}
280
281function parseLocalizableStringFromNode(node, filePath) {
282 if (!node)
283 return;
284
285 if (Array.isArray(node)) {
286 for (const child of node)
287 parseLocalizableStringFromNode(child, filePath);
288
289 return;
290 }
291
292 const keys = Object.keys(node);
293 const objKeys = keys.filter(key => key !== 'loc' && typeof node[key] === 'object');
294 if (objKeys.length === 0) {
295 // base case: all values are non-objects -> node is a leaf
296 return;
297 }
298
299 const locCase = localizationUtils.getLocalizationCase(node);
300 switch (locCase) {
301 case 'Common.UIString':
Mandy Chen7a8829b2019-06-25 22:13:07 +0000302 case 'Common.UIStringFormat':
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000303 handleCommonUIString(node, filePath);
304 break;
305 case 'UI.formatLocalized':
306 if (node.arguments !== undefined && node.arguments[1] !== undefined && node.arguments[1].elements !== undefined)
307 handleCommonUIString(node, filePath, node.arguments[1].elements);
308 break;
309 case 'Tagged Template':
310 handleTemplateLiteral(node.quasi, escodegen.generate(node), filePath);
311 break;
312 case null:
313 break;
314 default:
315 throw new Error(
316 `${filePath}${localizationUtils.getLocationMessage(node.loc)}: unexpected localization case for node: ${
317 escodegen.generate(node)}`);
318 }
319
320 for (const key of objKeys) {
321 // recursively parse all the child nodes
322 parseLocalizableStringFromNode(node[key], filePath);
323 }
324}
325
326function handleCommonUIString(node, filePath, argumentNodes) {
327 if (argumentNodes === undefined)
328 argumentNodes = node.arguments.slice(1);
329 const firstArgType = node.arguments[0].type;
330 switch (firstArgType) {
331 case esprimaTypes.LITERAL:
332 const message = node.arguments[0].value;
333 addString(message, escodegen.generate(node), filePath, node.loc, argumentNodes);
334 break;
335 case esprimaTypes.TEMP_LITERAL:
336 handleTemplateLiteral(node.arguments[0], escodegen.generate(node), filePath, argumentNodes);
337 break;
338 default:
339 break;
340 }
341}
342
343function handleTemplateLiteral(node, code, filePath, argumentNodes) {
344 if (node.expressions.length === 0) {
345 // template literal does not contain any variables, parse the value
346 addString(node.quasis[0].value.cooked, code, filePath, node.loc, argumentNodes);
347 return;
348 }
349
350 argumentNodes = node.expressions;
351 let processedMsg = '';
352 for (let i = 0; i < node.quasis.length; i++) {
353 processedMsg += node.quasis[i].value.cooked;
354 if (i < node.expressions.length) {
355 // add placeholder for variable so that
356 // the ph tag gets generated
357 processedMsg += '%s';
358 }
359 }
360 addString(processedMsg, code, filePath, node.loc, argumentNodes);
361}
362
363function addString(str, code, filePath, location, argumentNodes) {
Mandy Chen1e9d87b2019-09-18 17:18:15 +0000364 const ids = localizationUtils.getIDSKey(str);
365
366 // In the case of duplicates, the corresponding grdp message should be added
367 // to the shared strings file only if the duplicate strings span across different
368 // grdp files
369 const existingString = frontendStrings.get(ids);
370 if (existingString) {
371 if (!existingString.isShared && existingString.grdpPath !== fileToGRDPMap.get(filePath)) {
372 existingString.isShared = true;
373 existingString.grdpPath = localizationUtils.SHARED_STRINGS_PATH;
374 }
375 return;
376 }
377
378 const currentString =
379 {string: str, code: code, isShared: false, filepath: filePath, grdpPath: fileToGRDPMap.get(filePath)};
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000380 if (location)
381 currentString.location = location;
382 if (argumentNodes && argumentNodes.length > 0)
383 currentString.arguments = argumentNodes.map(argNode => escodegen.generate(argNode));
384
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000385 frontendStrings.set(ids, currentString);
386}
387
388/**
389 * The following functions parse <message>s and their IDS keys from
390 * devtools frontend grdp files.
391 */
392
Mandy Chen5128cc62019-09-23 16:46:00 +0000393async function parseIDSKeys() {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000394 // NOTE: this function assumes that no <message> tags are present in the parent
Mandy Chen5128cc62019-09-23 16:46:00 +0000395 const grdpFilePaths = await parseGRDFile();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000396 await parseGRDPFiles(grdpFilePaths);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000397}
398
Mandy Chen5128cc62019-09-23 16:46:00 +0000399async function parseGRDFile() {
400 const fileContent = await localizationUtils.parseFileContent(localizationUtils.GRD_PATH);
401 const grdFileDir = path.dirname(localizationUtils.GRD_PATH);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000402 const partFileRegex = /<part file="(.*?)"/g;
403
404 let match;
405 const grdpFilePaths = new Set();
406 while ((match = partFileRegex.exec(fileContent)) !== null) {
407 if (match.index === partFileRegex.lastIndex)
408 partFileRegex.lastIndex++;
409 // match[0]: full match
410 // match[1]: part file path
411 grdpFilePaths.add(path.resolve(grdFileDir, match[1]));
412 }
413 return grdpFilePaths;
414}
415
416function parseGRDPFiles(grdpFilePaths) {
417 const promises = Array.from(grdpFilePaths, grdpFilePath => parseGRDPFile(grdpFilePath));
418 return Promise.all(promises);
419}
420
421function trimGrdpPlaceholder(placeholder) {
422 const exampleRegex = new RegExp('<ex>.*?<\/ex>', 'gms');
423 // $1s<ex>my example</ex> -> $1s
424 return placeholder.replace(exampleRegex, '').trim();
425}
426
427function convertToFrontendPlaceholders(message) {
428 // <ph name="phname">$1s<ex>my example</ex></ph> and <ph name="phname2">$2.3f</ph>
429 // match[0]: <ph name="phname1">$1s</ph>
430 // match[1]: $1s<ex>my example</ex>
431 let placeholderRegex = new RegExp('<ph[^>]*>(.*?)<\/ph>', 'gms');
432 let match;
433 while ((match = placeholderRegex.exec(message)) !== null) {
434 const placeholder = match[0];
435 const placeholderValue = trimGrdpPlaceholder(match[1]);
436 const newPlaceholderValue = placeholderValue.replace(/\$[1-9]/, '%');
437 message =
438 message.substring(0, match.index) + newPlaceholderValue + message.substring(match.index + placeholder.length);
439 // Modified the message, so search from the beginning of the string again.
440 placeholderRegex.lastIndex = 0;
441 }
442 return message;
443}
444
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000445async function parseGRDPFile(filePath) {
446 const fileContent = await localizationUtils.parseFileContent(filePath);
447
Mandy Chen78552632019-06-12 00:55:43 +0000448 function stripWhitespacePadding(message) {
449 let match = message.match(/^'''/);
450 if (match)
451 message = message.substring(3);
452 match = message.match(/(.*?)'''$/);
453 if (match)
454 message = match[1];
455 return message;
456 }
457
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000458 // Example:
Mandy Chen81d4fc42019-07-11 23:12:02 +0000459 // <message name="IDS_DEVTOOLS_md5_hash" desc="Description of this message">
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000460 // Message text here with optional placeholders <ph name="phname">$1s</ph>
461 // </message>
462 // match[0]: the entire '<message>...</message>' block.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000463 // match[1]: 'IDS_DEVTOOLS_md5_hash'
464 // match[2]: 'Description of this message'
465 // match[3]: ' Message text here with optional placeholders <ph name="phname">$1s</ph>\n '
466 const messageRegex = new RegExp('<message[^>]*name="([^"]*)"[^>]*desc="([^"]*)"[^>]*>\s*\n(.*?)<\/message>', 'gms');
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000467 let match;
468 while ((match = messageRegex.exec(fileContent)) !== null) {
Mandy Chend97200b2019-07-29 21:13:39 +0000469 const line = localizationUtils.lineNumberOfIndex(fileContent, match.index);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000470 const actualIDSKey = match[1];
471 const description = match[2];
472 let message = match[3];
Mandy Chen78552632019-06-12 00:55:43 +0000473 message = convertToFrontendPlaceholders(message.trim());
474 message = stripWhitespacePadding(message);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000475 message = localizationUtils.sanitizeStringIntoFrontendFormat(message);
476
477 const ids = localizationUtils.getIDSKey(message);
Mandy Chen4a7ad052019-07-16 16:09:29 +0000478 addMessage(ids, actualIDSKey, filePath, line, description);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000479 }
480}
481
Mandy Chen4a7ad052019-07-16 16:09:29 +0000482function addMessage(expectedIDSKey, actualIDSKey, grdpPath, line, description) {
483 if (!IDSkeys.has(expectedIDSKey))
484 IDSkeys.set(expectedIDSKey, []);
485
486 IDSkeys.get(expectedIDSKey).push({actualIDSKey, grdpPath, location: {start: {line}, end: {line}}, description});
487}
488
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000489/**
490 * The following functions compare frontend localizable strings
Mandy Chen81d4fc42019-07-11 23:12:02 +0000491 * with grdp <message>s and report error of resources to add,
492 * remove or modify.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000493 */
Mandy Chen08fdf042019-09-23 19:57:20 +0000494function getAndReportResourcesToAdd() {
Mandy Chen4a7ad052019-07-16 16:09:29 +0000495 const keysToAddToGRD = getMessagesToAdd();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000496 if (keysToAddToGRD.size === 0)
497 return;
498
499 let errorStr = 'The following frontend string(s) need to be added to GRD/GRDP file(s).\n';
500 errorStr += 'Please refer to auto-generated message(s) below and modify as needed.\n\n';
501
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000502 // Example error message:
503 // third_party/blink/renderer/devtools/front_end/network/NetworkDataGridNode.js Line 973: ls`(disk cache)`
504 // Add a new message tag for this string to third_party\blink\renderer\devtools\front_end\network\network_strings.grdp
505 // <message name="IDS_DEVTOOLS_ad86890fb40822a3b12627efaca4ecd7" desc="Fill in the description.">
506 // (disk cache)
507 // </message>
508 for (const [key, stringObj] of keysToAddToGRD) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000509 errorStr += `${localizationUtils.getRelativeFilePathFromSrc(stringObj.filepath)}${
510 localizationUtils.getLocationMessage(stringObj.location)}: ${stringObj.code}\n`;
511 errorStr += `Add a new message tag for this string to ${
Mandy Chenc94d52a2019-06-11 22:51:53 +0000512 localizationUtils.getRelativeFilePathFromSrc(fileToGRDPMap.get(stringObj.filepath))}\n\n`;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000513 errorStr += localizationUtils.createGrdpMessage(key, stringObj);
514 }
515 return errorStr;
516}
517
Mandy Chen4a7ad052019-07-16 16:09:29 +0000518function getAndReportResourcesToRemove() {
519 const keysToRemoveFromGRD = getMessagesToRemove();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000520 if (keysToRemoveFromGRD.size === 0)
521 return;
522
523 let errorStr =
524 '\nThe message(s) associated with the following IDS key(s) should be removed from its GRD/GRDP file(s):\n';
525 // Example error message:
Mandy Chen4a7ad052019-07-16 16:09:29 +0000526 // third_party/blink/renderer/devtools/front_end/accessibility/accessibility_strings.grdp Line 300: IDS_DEVTOOLS_c9bbad3047af039c14d0e7ec957bb867
527 for (const [ids, messages] of keysToRemoveFromGRD) {
528 messages.forEach(
529 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
530 localizationUtils.getLocationMessage(message.location)}: ${ids}\n\n`);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000531 }
532 return errorStr;
533}
534
Mandy Chen81d4fc42019-07-11 23:12:02 +0000535function getAndReportIDSKeysToModify() {
536 const messagesToModify = getIDSKeysToModify();
537 if (messagesToModify.size === 0)
538 return;
539
540 let errorStr = '\nThe following GRD/GRDP message(s) do not have the correct IDS key.\n';
541 errorStr += 'Please update the key(s) by changing the "name" value.\n\n';
542
Mandy Chen4a7ad052019-07-16 16:09:29 +0000543 for (const [expectedIDSKey, messages] of messagesToModify) {
544 messages.forEach(
545 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
546 localizationUtils.getLocationMessage(
547 message.location)}:\n${message.actualIDSKey} --> ${expectedIDSKey}\n\n`);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000548 }
549 return errorStr;
550}
551
Mandy Chen4a7ad052019-07-16 16:09:29 +0000552function getMessagesToAdd() {
553 // If a message with ids key exists in grdpPath
554 function messageExists(ids, grdpPath) {
555 const messages = IDSkeys.get(ids);
556 return messages.some(message => message.grdpPath === grdpPath);
557 }
558
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000559 const difference = [];
Mandy Chen4a7ad052019-07-16 16:09:29 +0000560 for (const [ids, frontendString] of frontendStrings) {
561 if (!IDSkeys.has(ids) || !messageExists(ids, frontendString.grdpPath))
562 difference.push([ids, frontendString]);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000563 }
564 return new Map(difference.sort());
565}
566
Mandy Chen4a7ad052019-07-16 16:09:29 +0000567// Return a map from the expected IDS key to a list of messages
568// whose actual IDS keys need to be modified.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000569function getIDSKeysToModify() {
570 const messagesToModify = new Map();
Mandy Chen4a7ad052019-07-16 16:09:29 +0000571 for (const [expectedIDSKey, messages] of IDSkeys) {
572 for (const message of messages) {
573 if (expectedIDSKey !== message.actualIDSKey) {
574 if (messagesToModify.has(expectedIDSKey))
575 messagesToModify.get(expectedIDSKey).push(message);
576 else
577 messagesToModify.set(expectedIDSKey, [message]);
578 }
579 }
Mandy Chen81d4fc42019-07-11 23:12:02 +0000580 }
581 return messagesToModify;
582}
583
Mandy Chen4a7ad052019-07-16 16:09:29 +0000584function getMessagesToRemove() {
585 const difference = new Map();
586 for (const [ids, messages] of IDSkeys) {
587 if (!frontendStrings.has(ids)) {
588 difference.set(ids, messages);
589 continue;
590 }
591
592 const expectedGrdpPath = frontendStrings.get(ids).grdpPath;
593 const messagesInGrdp = [];
594 const messagesToRemove = [];
595 messages.forEach(message => {
596 if (message.grdpPath !== expectedGrdpPath)
597 messagesToRemove.push(message);
598 else
599 messagesInGrdp.push(message);
600 });
601
602 if (messagesToRemove.length === 0 && messagesInGrdp.length === 1)
603 continue;
604
605 if (messagesInGrdp.length > 1) {
606 // If there are more than one messages with ids in the
607 // expected grdp file, keep one with the longest
608 // description and delete all the other messages
609 const longestDescription = getLongestDescription(messagesInGrdp);
610 let foundMessageToKeep = false;
611 for (const message of messagesInGrdp) {
612 if (message.description === longestDescription && !foundMessageToKeep) {
613 foundMessageToKeep = true;
614 continue;
615 }
616 messagesToRemove.push(message);
617 }
618 }
619 difference.set(ids, messagesToRemove);
620 }
621 return difference;
622}
623
624function getLongestDescription(messages) {
625 let longestDescription = '';
626 messages.forEach(message => {
627 if (message.description.length > longestDescription.length)
628 longestDescription = message.description;
629 });
630 return longestDescription;
631}
632
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000633module.exports = {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000634 parseLocalizableResourceMaps,
Mandy Chen81d4fc42019-07-11 23:12:02 +0000635 getAndReportIDSKeysToModify,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000636 getAndReportResourcesToAdd,
637 getAndReportResourcesToRemove,
Mandy Chen4a7ad052019-07-16 16:09:29 +0000638 getIDSKeysToModify,
639 getLongestDescription,
640 getMessagesToAdd,
641 getMessagesToRemove,
Mandy Chen5128cc62019-09-23 16:46:00 +0000642 validateGrdAndGrdpFiles,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000643};