blob: ab4d65a4bda104a9b8505611735080076d0da37c [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 Chenc94d52a2019-06-11 22:51:53 +0000191 const grdpToFiles = new Map();
Mandy Chen5128cc62019-09-23 16:46:00 +0000192 const dirs = devtoolsFrontendDirs || await localizationUtils.getChildDirectoriesFromDirectory(devtoolsFrontendPath);
Mandy Chenc94d52a2019-06-11 22:51:53 +0000193 const grdpToFilesPromises = dirs.map(dir => {
194 const files = [];
Mandy Chen5128cc62019-09-23 16:46:00 +0000195 grdpToFiles.set(expectedGrdpFilePath(dir), files);
Mandy Chenc94d52a2019-06-11 22:51:53 +0000196 return localizationUtils.getFilesFromDirectory(dir, files, ['.js', 'module.json']);
197 });
198 await Promise.all(grdpToFilesPromises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000199
Mandy Chen4a7ad052019-07-16 16:09:29 +0000200 const promises = [];
Mandy Chenc94d52a2019-06-11 22:51:53 +0000201 for (const [grdpPath, files] of grdpToFiles) {
202 files.forEach(file => fileToGRDPMap.set(file, grdpPath));
Mandy Chen4a7ad052019-07-16 16:09:29 +0000203 promises.push(parseLocalizableStrings(files));
Mandy Chenc94d52a2019-06-11 22:51:53 +0000204 }
205 await Promise.all(promises);
Mandy Chen4a7ad052019-07-16 16:09:29 +0000206 // Parse grd(p) files after frontend strings are processed so we know
207 // what to add or remove based on frontend strings
Mandy Chen5128cc62019-09-23 16:46:00 +0000208 await parseIDSKeys();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000209}
210
211/**
Mandy Chen7a8829b2019-06-25 22:13:07 +0000212 * The following functions parse localizable strings (wrapped in Common.UIString,
213 * Common.UIStringFormat, UI.formatLocalized or ls``) from devtools frontend files.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000214 */
215
Mandy Chen4a7ad052019-07-16 16:09:29 +0000216async function parseLocalizableStrings(devtoolsFiles) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000217 const promises = devtoolsFiles.map(filePath => parseLocalizableStringsFromFile(filePath));
218 await Promise.all(promises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000219}
220
221async function parseLocalizableStringsFromFile(filePath) {
222 const fileContent = await localizationUtils.parseFileContent(filePath);
223 if (path.basename(filePath) === 'module.json')
224 return parseLocalizableStringFromModuleJson(fileContent, filePath);
225
Mandy Chen436efc72019-09-18 17:43:40 +0000226 let ast;
227 try {
228 ast = esprima.parseModule(fileContent, {loc: true});
229 } catch (e) {
230 throw new Error(
231 `DevTools localization parser failed:\n${localizationUtils.getRelativeFilePathFromSrc(filePath)}: ${
232 e.message}` +
233 `\nThis error is likely due to unsupported JavaScript features.` +
234 ` Such features are not supported by eslint either and will cause presubmit to fail.` +
235 ` Please update the code and use official JavaScript features.`);
236 }
237 for (const node of ast.body) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000238 parseLocalizableStringFromNode(node, filePath);
Mandy Chen436efc72019-09-18 17:43:40 +0000239 }
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000240}
241
242function parseLocalizableStringFromModuleJson(fileContent, filePath) {
243 const fileJSON = JSON.parse(fileContent);
244 if (!fileJSON.extensions)
245 return;
246
247 for (const extension of fileJSON.extensions) {
248 for (const key in extension) {
249 if (extensionStringKeys.includes(key)) {
250 addString(extension[key], extension[key], filePath);
251 } else if (key === 'device') {
252 addString(extension.device.title, extension.device.title, filePath);
253 } else if (key === 'options') {
254 for (const option of extension.options) {
255 addString(option.title, option.title, filePath);
256 if (option.text !== undefined)
257 addString(option.text, option.text, filePath);
258 }
Mandy Chen609679b2019-09-10 16:04:08 +0000259 } else if (key === 'defaultValue' && Array.isArray(extension[key])) {
260 for (const defaultVal of extension[key]) {
261 if (defaultVal.title)
262 addString(defaultVal.title, defaultVal.title, filePath);
263 }
Christy Chenfc8ed9f2019-09-19 22:18:44 +0000264 } else if (key === 'tags' && extension[key]) {
265 const tagsList = extension[key].split(',');
266 for (let tag of tagsList) {
267 tag = tag.trim();
268 addString(tag, tag, filePath);
269 }
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000270 }
271 }
272 }
273}
274
275function parseLocalizableStringFromNode(node, filePath) {
276 if (!node)
277 return;
278
279 if (Array.isArray(node)) {
280 for (const child of node)
281 parseLocalizableStringFromNode(child, filePath);
282
283 return;
284 }
285
286 const keys = Object.keys(node);
287 const objKeys = keys.filter(key => key !== 'loc' && typeof node[key] === 'object');
288 if (objKeys.length === 0) {
289 // base case: all values are non-objects -> node is a leaf
290 return;
291 }
292
293 const locCase = localizationUtils.getLocalizationCase(node);
294 switch (locCase) {
295 case 'Common.UIString':
Mandy Chen7a8829b2019-06-25 22:13:07 +0000296 case 'Common.UIStringFormat':
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000297 handleCommonUIString(node, filePath);
298 break;
299 case 'UI.formatLocalized':
300 if (node.arguments !== undefined && node.arguments[1] !== undefined && node.arguments[1].elements !== undefined)
301 handleCommonUIString(node, filePath, node.arguments[1].elements);
302 break;
303 case 'Tagged Template':
304 handleTemplateLiteral(node.quasi, escodegen.generate(node), filePath);
305 break;
306 case null:
307 break;
308 default:
309 throw new Error(
310 `${filePath}${localizationUtils.getLocationMessage(node.loc)}: unexpected localization case for node: ${
311 escodegen.generate(node)}`);
312 }
313
314 for (const key of objKeys) {
315 // recursively parse all the child nodes
316 parseLocalizableStringFromNode(node[key], filePath);
317 }
318}
319
320function handleCommonUIString(node, filePath, argumentNodes) {
321 if (argumentNodes === undefined)
322 argumentNodes = node.arguments.slice(1);
323 const firstArgType = node.arguments[0].type;
324 switch (firstArgType) {
325 case esprimaTypes.LITERAL:
326 const message = node.arguments[0].value;
327 addString(message, escodegen.generate(node), filePath, node.loc, argumentNodes);
328 break;
329 case esprimaTypes.TEMP_LITERAL:
330 handleTemplateLiteral(node.arguments[0], escodegen.generate(node), filePath, argumentNodes);
331 break;
332 default:
333 break;
334 }
335}
336
337function handleTemplateLiteral(node, code, filePath, argumentNodes) {
338 if (node.expressions.length === 0) {
339 // template literal does not contain any variables, parse the value
340 addString(node.quasis[0].value.cooked, code, filePath, node.loc, argumentNodes);
341 return;
342 }
343
344 argumentNodes = node.expressions;
345 let processedMsg = '';
346 for (let i = 0; i < node.quasis.length; i++) {
347 processedMsg += node.quasis[i].value.cooked;
348 if (i < node.expressions.length) {
349 // add placeholder for variable so that
350 // the ph tag gets generated
351 processedMsg += '%s';
352 }
353 }
354 addString(processedMsg, code, filePath, node.loc, argumentNodes);
355}
356
357function addString(str, code, filePath, location, argumentNodes) {
Mandy Chen1e9d87b2019-09-18 17:18:15 +0000358 const ids = localizationUtils.getIDSKey(str);
359
360 // In the case of duplicates, the corresponding grdp message should be added
361 // to the shared strings file only if the duplicate strings span across different
362 // grdp files
363 const existingString = frontendStrings.get(ids);
364 if (existingString) {
365 if (!existingString.isShared && existingString.grdpPath !== fileToGRDPMap.get(filePath)) {
366 existingString.isShared = true;
367 existingString.grdpPath = localizationUtils.SHARED_STRINGS_PATH;
368 }
369 return;
370 }
371
372 const currentString =
373 {string: str, code: code, isShared: false, filepath: filePath, grdpPath: fileToGRDPMap.get(filePath)};
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000374 if (location)
375 currentString.location = location;
376 if (argumentNodes && argumentNodes.length > 0)
377 currentString.arguments = argumentNodes.map(argNode => escodegen.generate(argNode));
378
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000379 frontendStrings.set(ids, currentString);
380}
381
382/**
383 * The following functions parse <message>s and their IDS keys from
384 * devtools frontend grdp files.
385 */
386
Mandy Chen5128cc62019-09-23 16:46:00 +0000387async function parseIDSKeys() {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000388 // NOTE: this function assumes that no <message> tags are present in the parent
Mandy Chen5128cc62019-09-23 16:46:00 +0000389 const grdpFilePaths = await parseGRDFile();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000390 await parseGRDPFiles(grdpFilePaths);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000391}
392
Mandy Chen5128cc62019-09-23 16:46:00 +0000393async function parseGRDFile() {
394 const fileContent = await localizationUtils.parseFileContent(localizationUtils.GRD_PATH);
395 const grdFileDir = path.dirname(localizationUtils.GRD_PATH);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000396 const partFileRegex = /<part file="(.*?)"/g;
397
398 let match;
399 const grdpFilePaths = new Set();
400 while ((match = partFileRegex.exec(fileContent)) !== null) {
401 if (match.index === partFileRegex.lastIndex)
402 partFileRegex.lastIndex++;
403 // match[0]: full match
404 // match[1]: part file path
405 grdpFilePaths.add(path.resolve(grdFileDir, match[1]));
406 }
407 return grdpFilePaths;
408}
409
410function parseGRDPFiles(grdpFilePaths) {
411 const promises = Array.from(grdpFilePaths, grdpFilePath => parseGRDPFile(grdpFilePath));
412 return Promise.all(promises);
413}
414
415function trimGrdpPlaceholder(placeholder) {
416 const exampleRegex = new RegExp('<ex>.*?<\/ex>', 'gms');
417 // $1s<ex>my example</ex> -> $1s
418 return placeholder.replace(exampleRegex, '').trim();
419}
420
421function convertToFrontendPlaceholders(message) {
422 // <ph name="phname">$1s<ex>my example</ex></ph> and <ph name="phname2">$2.3f</ph>
423 // match[0]: <ph name="phname1">$1s</ph>
424 // match[1]: $1s<ex>my example</ex>
425 let placeholderRegex = new RegExp('<ph[^>]*>(.*?)<\/ph>', 'gms');
426 let match;
427 while ((match = placeholderRegex.exec(message)) !== null) {
428 const placeholder = match[0];
429 const placeholderValue = trimGrdpPlaceholder(match[1]);
430 const newPlaceholderValue = placeholderValue.replace(/\$[1-9]/, '%');
431 message =
432 message.substring(0, match.index) + newPlaceholderValue + message.substring(match.index + placeholder.length);
433 // Modified the message, so search from the beginning of the string again.
434 placeholderRegex.lastIndex = 0;
435 }
436 return message;
437}
438
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000439async function parseGRDPFile(filePath) {
440 const fileContent = await localizationUtils.parseFileContent(filePath);
441
Mandy Chen78552632019-06-12 00:55:43 +0000442 function stripWhitespacePadding(message) {
443 let match = message.match(/^'''/);
444 if (match)
445 message = message.substring(3);
446 match = message.match(/(.*?)'''$/);
447 if (match)
448 message = match[1];
449 return message;
450 }
451
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000452 // Example:
Mandy Chen81d4fc42019-07-11 23:12:02 +0000453 // <message name="IDS_DEVTOOLS_md5_hash" desc="Description of this message">
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000454 // Message text here with optional placeholders <ph name="phname">$1s</ph>
455 // </message>
456 // match[0]: the entire '<message>...</message>' block.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000457 // match[1]: 'IDS_DEVTOOLS_md5_hash'
458 // match[2]: 'Description of this message'
459 // match[3]: ' Message text here with optional placeholders <ph name="phname">$1s</ph>\n '
460 const messageRegex = new RegExp('<message[^>]*name="([^"]*)"[^>]*desc="([^"]*)"[^>]*>\s*\n(.*?)<\/message>', 'gms');
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000461 let match;
462 while ((match = messageRegex.exec(fileContent)) !== null) {
Mandy Chend97200b2019-07-29 21:13:39 +0000463 const line = localizationUtils.lineNumberOfIndex(fileContent, match.index);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000464 const actualIDSKey = match[1];
465 const description = match[2];
466 let message = match[3];
Mandy Chen78552632019-06-12 00:55:43 +0000467 message = convertToFrontendPlaceholders(message.trim());
468 message = stripWhitespacePadding(message);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000469 message = localizationUtils.sanitizeStringIntoFrontendFormat(message);
470
471 const ids = localizationUtils.getIDSKey(message);
Mandy Chen4a7ad052019-07-16 16:09:29 +0000472 addMessage(ids, actualIDSKey, filePath, line, description);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000473 }
474}
475
Mandy Chen4a7ad052019-07-16 16:09:29 +0000476function addMessage(expectedIDSKey, actualIDSKey, grdpPath, line, description) {
477 if (!IDSkeys.has(expectedIDSKey))
478 IDSkeys.set(expectedIDSKey, []);
479
480 IDSkeys.get(expectedIDSKey).push({actualIDSKey, grdpPath, location: {start: {line}, end: {line}}, description});
481}
482
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000483/**
484 * The following functions compare frontend localizable strings
Mandy Chen81d4fc42019-07-11 23:12:02 +0000485 * with grdp <message>s and report error of resources to add,
486 * remove or modify.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000487 */
Mandy Chen4a7ad052019-07-16 16:09:29 +0000488async function getAndReportResourcesToAdd() {
489 const keysToAddToGRD = getMessagesToAdd();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000490 if (keysToAddToGRD.size === 0)
491 return;
492
493 let errorStr = 'The following frontend string(s) need to be added to GRD/GRDP file(s).\n';
494 errorStr += 'Please refer to auto-generated message(s) below and modify as needed.\n\n';
495
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000496 // Example error message:
497 // third_party/blink/renderer/devtools/front_end/network/NetworkDataGridNode.js Line 973: ls`(disk cache)`
498 // Add a new message tag for this string to third_party\blink\renderer\devtools\front_end\network\network_strings.grdp
499 // <message name="IDS_DEVTOOLS_ad86890fb40822a3b12627efaca4ecd7" desc="Fill in the description.">
500 // (disk cache)
501 // </message>
502 for (const [key, stringObj] of keysToAddToGRD) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000503 errorStr += `${localizationUtils.getRelativeFilePathFromSrc(stringObj.filepath)}${
504 localizationUtils.getLocationMessage(stringObj.location)}: ${stringObj.code}\n`;
505 errorStr += `Add a new message tag for this string to ${
Mandy Chenc94d52a2019-06-11 22:51:53 +0000506 localizationUtils.getRelativeFilePathFromSrc(fileToGRDPMap.get(stringObj.filepath))}\n\n`;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000507 errorStr += localizationUtils.createGrdpMessage(key, stringObj);
508 }
509 return errorStr;
510}
511
Mandy Chen4a7ad052019-07-16 16:09:29 +0000512function getAndReportResourcesToRemove() {
513 const keysToRemoveFromGRD = getMessagesToRemove();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000514 if (keysToRemoveFromGRD.size === 0)
515 return;
516
517 let errorStr =
518 '\nThe message(s) associated with the following IDS key(s) should be removed from its GRD/GRDP file(s):\n';
519 // Example error message:
Mandy Chen4a7ad052019-07-16 16:09:29 +0000520 // third_party/blink/renderer/devtools/front_end/accessibility/accessibility_strings.grdp Line 300: IDS_DEVTOOLS_c9bbad3047af039c14d0e7ec957bb867
521 for (const [ids, messages] of keysToRemoveFromGRD) {
522 messages.forEach(
523 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
524 localizationUtils.getLocationMessage(message.location)}: ${ids}\n\n`);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000525 }
526 return errorStr;
527}
528
Mandy Chen81d4fc42019-07-11 23:12:02 +0000529function getAndReportIDSKeysToModify() {
530 const messagesToModify = getIDSKeysToModify();
531 if (messagesToModify.size === 0)
532 return;
533
534 let errorStr = '\nThe following GRD/GRDP message(s) do not have the correct IDS key.\n';
535 errorStr += 'Please update the key(s) by changing the "name" value.\n\n';
536
Mandy Chen4a7ad052019-07-16 16:09:29 +0000537 for (const [expectedIDSKey, messages] of messagesToModify) {
538 messages.forEach(
539 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
540 localizationUtils.getLocationMessage(
541 message.location)}:\n${message.actualIDSKey} --> ${expectedIDSKey}\n\n`);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000542 }
543 return errorStr;
544}
545
Mandy Chen4a7ad052019-07-16 16:09:29 +0000546function getMessagesToAdd() {
547 // If a message with ids key exists in grdpPath
548 function messageExists(ids, grdpPath) {
549 const messages = IDSkeys.get(ids);
550 return messages.some(message => message.grdpPath === grdpPath);
551 }
552
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000553 const difference = [];
Mandy Chen4a7ad052019-07-16 16:09:29 +0000554 for (const [ids, frontendString] of frontendStrings) {
555 if (!IDSkeys.has(ids) || !messageExists(ids, frontendString.grdpPath))
556 difference.push([ids, frontendString]);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000557 }
558 return new Map(difference.sort());
559}
560
Mandy Chen4a7ad052019-07-16 16:09:29 +0000561// Return a map from the expected IDS key to a list of messages
562// whose actual IDS keys need to be modified.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000563function getIDSKeysToModify() {
564 const messagesToModify = new Map();
Mandy Chen4a7ad052019-07-16 16:09:29 +0000565 for (const [expectedIDSKey, messages] of IDSkeys) {
566 for (const message of messages) {
567 if (expectedIDSKey !== message.actualIDSKey) {
568 if (messagesToModify.has(expectedIDSKey))
569 messagesToModify.get(expectedIDSKey).push(message);
570 else
571 messagesToModify.set(expectedIDSKey, [message]);
572 }
573 }
Mandy Chen81d4fc42019-07-11 23:12:02 +0000574 }
575 return messagesToModify;
576}
577
Mandy Chen4a7ad052019-07-16 16:09:29 +0000578function getMessagesToRemove() {
579 const difference = new Map();
580 for (const [ids, messages] of IDSkeys) {
581 if (!frontendStrings.has(ids)) {
582 difference.set(ids, messages);
583 continue;
584 }
585
586 const expectedGrdpPath = frontendStrings.get(ids).grdpPath;
587 const messagesInGrdp = [];
588 const messagesToRemove = [];
589 messages.forEach(message => {
590 if (message.grdpPath !== expectedGrdpPath)
591 messagesToRemove.push(message);
592 else
593 messagesInGrdp.push(message);
594 });
595
596 if (messagesToRemove.length === 0 && messagesInGrdp.length === 1)
597 continue;
598
599 if (messagesInGrdp.length > 1) {
600 // If there are more than one messages with ids in the
601 // expected grdp file, keep one with the longest
602 // description and delete all the other messages
603 const longestDescription = getLongestDescription(messagesInGrdp);
604 let foundMessageToKeep = false;
605 for (const message of messagesInGrdp) {
606 if (message.description === longestDescription && !foundMessageToKeep) {
607 foundMessageToKeep = true;
608 continue;
609 }
610 messagesToRemove.push(message);
611 }
612 }
613 difference.set(ids, messagesToRemove);
614 }
615 return difference;
616}
617
618function getLongestDescription(messages) {
619 let longestDescription = '';
620 messages.forEach(message => {
621 if (message.description.length > longestDescription.length)
622 longestDescription = message.description;
623 });
624 return longestDescription;
625}
626
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000627module.exports = {
Mandy Chenc94d52a2019-06-11 22:51:53 +0000628 frontendStrings,
629 IDSkeys,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000630 parseLocalizableResourceMaps,
Mandy Chen81d4fc42019-07-11 23:12:02 +0000631 getAndReportIDSKeysToModify,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000632 getAndReportResourcesToAdd,
633 getAndReportResourcesToRemove,
Mandy Chen4a7ad052019-07-16 16:09:29 +0000634 getIDSKeysToModify,
635 getLongestDescription,
636 getMessagesToAdd,
637 getMessagesToRemove,
Mandy Chen5128cc62019-09-23 16:46:00 +0000638 validateGrdAndGrdpFiles,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000639};