blob: 4281b65cd01159c72e6e336817e99a391bf47cdb [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
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000011const path = require('path');
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000012const localizationUtils = require('./localization_utils');
13const escodegen = localizationUtils.escodegen;
14const esprimaTypes = localizationUtils.esprimaTypes;
15const esprima = localizationUtils.esprima;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000016const extensionStringKeys = ['category', 'destination', 'title', 'title-mac'];
17
18// Format of frontendStrings
19// { IDS_md5-hash => {
20// string: string,
21// code: string,
22// filepath: string,
Mandy Chenc94d52a2019-06-11 22:51:53 +000023// grdpPath: string,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000024// location: {
25// start: {
26// line: number, (1-based)
27// column: number (0-based)
28// },
29// end: {
30// line: number,
31// column: number
32// }
33// },
34// arguments: string[]
35// }
36// }
37const frontendStrings = new Map();
38
39// Format
40// {
Mandy Chen4a7ad052019-07-16 16:09:29 +000041// IDS_KEY => a list of {
Mandy Chen81d4fc42019-07-11 23:12:02 +000042// actualIDSKey: string, // the IDS key in the message tag
Mandy Chenc94d52a2019-06-11 22:51:53 +000043// description: string,
Mandy Chen4a7ad052019-07-16 16:09:29 +000044// grdpPath: string,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000045// location: {
46// start: {
47// line: number
48// },
49// end: {
50// line: number
51// }
52// }
53// }
54// }
55const IDSkeys = new Map();
Mandy Chenc94d52a2019-06-11 22:51:53 +000056const fileToGRDPMap = new Map();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000057
58const devtoolsFrontendPath = path.resolve(__dirname, '..', '..', 'front_end');
59
Mandy Chen4a7ad052019-07-16 16:09:29 +000060async function parseLocalizableResourceMaps() {
Mandy Chenc94d52a2019-06-11 22:51:53 +000061 const grdpToFiles = new Map();
62 const dirs = await localizationUtils.getChildDirectoriesFromDirectory(devtoolsFrontendPath);
63 const grdpToFilesPromises = dirs.map(dir => {
64 const files = [];
65 grdpToFiles.set(path.resolve(dir, `${path.basename(dir)}_strings.grdp`), files);
66 return localizationUtils.getFilesFromDirectory(dir, files, ['.js', 'module.json']);
67 });
68 await Promise.all(grdpToFilesPromises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000069
Mandy Chen4a7ad052019-07-16 16:09:29 +000070 const promises = [];
Mandy Chenc94d52a2019-06-11 22:51:53 +000071 for (const [grdpPath, files] of grdpToFiles) {
72 files.forEach(file => fileToGRDPMap.set(file, grdpPath));
Mandy Chen4a7ad052019-07-16 16:09:29 +000073 promises.push(parseLocalizableStrings(files));
Mandy Chenc94d52a2019-06-11 22:51:53 +000074 }
75 await Promise.all(promises);
Mandy Chen4a7ad052019-07-16 16:09:29 +000076 // Parse grd(p) files after frontend strings are processed so we know
77 // what to add or remove based on frontend strings
78 await parseIDSKeys(localizationUtils.GRD_PATH);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000079}
80
81/**
Mandy Chen7a8829b2019-06-25 22:13:07 +000082 * The following functions parse localizable strings (wrapped in Common.UIString,
83 * Common.UIStringFormat, UI.formatLocalized or ls``) from devtools frontend files.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000084 */
85
Mandy Chen4a7ad052019-07-16 16:09:29 +000086async function parseLocalizableStrings(devtoolsFiles) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000087 const promises = devtoolsFiles.map(filePath => parseLocalizableStringsFromFile(filePath));
88 await Promise.all(promises);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000089}
90
91async function parseLocalizableStringsFromFile(filePath) {
92 const fileContent = await localizationUtils.parseFileContent(filePath);
93 if (path.basename(filePath) === 'module.json')
94 return parseLocalizableStringFromModuleJson(fileContent, filePath);
95
Tim van der Lippe63a4d3d2019-09-18 15:31:38 +000096 const ast = esprima.parseModule(fileContent, {loc: true});
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +000097 for (const node of ast.body)
98 parseLocalizableStringFromNode(node, filePath);
99}
100
101function parseLocalizableStringFromModuleJson(fileContent, filePath) {
102 const fileJSON = JSON.parse(fileContent);
103 if (!fileJSON.extensions)
104 return;
105
106 for (const extension of fileJSON.extensions) {
107 for (const key in extension) {
108 if (extensionStringKeys.includes(key)) {
109 addString(extension[key], extension[key], filePath);
110 } else if (key === 'device') {
111 addString(extension.device.title, extension.device.title, filePath);
112 } else if (key === 'options') {
113 for (const option of extension.options) {
114 addString(option.title, option.title, filePath);
115 if (option.text !== undefined)
116 addString(option.text, option.text, filePath);
117 }
Mandy Chen609679b2019-09-10 16:04:08 +0000118 } else if (key === 'defaultValue' && Array.isArray(extension[key])) {
119 for (const defaultVal of extension[key]) {
120 if (defaultVal.title)
121 addString(defaultVal.title, defaultVal.title, filePath);
122 }
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000123 }
124 }
125 }
126}
127
128function parseLocalizableStringFromNode(node, filePath) {
129 if (!node)
130 return;
131
132 if (Array.isArray(node)) {
133 for (const child of node)
134 parseLocalizableStringFromNode(child, filePath);
135
136 return;
137 }
138
139 const keys = Object.keys(node);
140 const objKeys = keys.filter(key => key !== 'loc' && typeof node[key] === 'object');
141 if (objKeys.length === 0) {
142 // base case: all values are non-objects -> node is a leaf
143 return;
144 }
145
146 const locCase = localizationUtils.getLocalizationCase(node);
147 switch (locCase) {
148 case 'Common.UIString':
Mandy Chen7a8829b2019-06-25 22:13:07 +0000149 case 'Common.UIStringFormat':
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000150 handleCommonUIString(node, filePath);
151 break;
152 case 'UI.formatLocalized':
153 if (node.arguments !== undefined && node.arguments[1] !== undefined && node.arguments[1].elements !== undefined)
154 handleCommonUIString(node, filePath, node.arguments[1].elements);
155 break;
156 case 'Tagged Template':
157 handleTemplateLiteral(node.quasi, escodegen.generate(node), filePath);
158 break;
159 case null:
160 break;
161 default:
162 throw new Error(
163 `${filePath}${localizationUtils.getLocationMessage(node.loc)}: unexpected localization case for node: ${
164 escodegen.generate(node)}`);
165 }
166
167 for (const key of objKeys) {
168 // recursively parse all the child nodes
169 parseLocalizableStringFromNode(node[key], filePath);
170 }
171}
172
173function handleCommonUIString(node, filePath, argumentNodes) {
174 if (argumentNodes === undefined)
175 argumentNodes = node.arguments.slice(1);
176 const firstArgType = node.arguments[0].type;
177 switch (firstArgType) {
178 case esprimaTypes.LITERAL:
179 const message = node.arguments[0].value;
180 addString(message, escodegen.generate(node), filePath, node.loc, argumentNodes);
181 break;
182 case esprimaTypes.TEMP_LITERAL:
183 handleTemplateLiteral(node.arguments[0], escodegen.generate(node), filePath, argumentNodes);
184 break;
185 default:
186 break;
187 }
188}
189
190function handleTemplateLiteral(node, code, filePath, argumentNodes) {
191 if (node.expressions.length === 0) {
192 // template literal does not contain any variables, parse the value
193 addString(node.quasis[0].value.cooked, code, filePath, node.loc, argumentNodes);
194 return;
195 }
196
197 argumentNodes = node.expressions;
198 let processedMsg = '';
199 for (let i = 0; i < node.quasis.length; i++) {
200 processedMsg += node.quasis[i].value.cooked;
201 if (i < node.expressions.length) {
202 // add placeholder for variable so that
203 // the ph tag gets generated
204 processedMsg += '%s';
205 }
206 }
207 addString(processedMsg, code, filePath, node.loc, argumentNodes);
208}
209
210function addString(str, code, filePath, location, argumentNodes) {
Mandy Chenc94d52a2019-06-11 22:51:53 +0000211 const currentString = {string: str, code: code, filepath: filePath, grdpPath: fileToGRDPMap.get(filePath)};
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000212 if (location)
213 currentString.location = location;
214 if (argumentNodes && argumentNodes.length > 0)
215 currentString.arguments = argumentNodes.map(argNode => escodegen.generate(argNode));
216
217 // In the case of duplicates, to enforce that entries are added to
218 // a consistent GRDP file, we use the file path that sorts lowest as
219 // the winning entry into frontendStrings.
220 const ids = localizationUtils.getIDSKey(str);
221 if (frontendStrings.has(ids) && frontendStrings.get(ids).filepath <= filePath)
222 return;
223 frontendStrings.set(ids, currentString);
224}
225
226/**
227 * The following functions parse <message>s and their IDS keys from
228 * devtools frontend grdp files.
229 */
230
Mandy Chen4a7ad052019-07-16 16:09:29 +0000231async function parseIDSKeys(grdFilePath) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000232 // NOTE: this function assumes that no <message> tags are present in the parent
233 const grdpFilePaths = await parseGRDFile(grdFilePath);
234 await parseGRDPFiles(grdpFilePaths);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000235}
236
237async function parseGRDFile(grdFilePath) {
238 const fileContent = await localizationUtils.parseFileContent(grdFilePath);
239 const grdFileDir = path.dirname(grdFilePath);
240 const partFileRegex = /<part file="(.*?)"/g;
241
242 let match;
243 const grdpFilePaths = new Set();
244 while ((match = partFileRegex.exec(fileContent)) !== null) {
245 if (match.index === partFileRegex.lastIndex)
246 partFileRegex.lastIndex++;
247 // match[0]: full match
248 // match[1]: part file path
249 grdpFilePaths.add(path.resolve(grdFileDir, match[1]));
250 }
251 return grdpFilePaths;
252}
253
254function parseGRDPFiles(grdpFilePaths) {
255 const promises = Array.from(grdpFilePaths, grdpFilePath => parseGRDPFile(grdpFilePath));
256 return Promise.all(promises);
257}
258
259function trimGrdpPlaceholder(placeholder) {
260 const exampleRegex = new RegExp('<ex>.*?<\/ex>', 'gms');
261 // $1s<ex>my example</ex> -> $1s
262 return placeholder.replace(exampleRegex, '').trim();
263}
264
265function convertToFrontendPlaceholders(message) {
266 // <ph name="phname">$1s<ex>my example</ex></ph> and <ph name="phname2">$2.3f</ph>
267 // match[0]: <ph name="phname1">$1s</ph>
268 // match[1]: $1s<ex>my example</ex>
269 let placeholderRegex = new RegExp('<ph[^>]*>(.*?)<\/ph>', 'gms');
270 let match;
271 while ((match = placeholderRegex.exec(message)) !== null) {
272 const placeholder = match[0];
273 const placeholderValue = trimGrdpPlaceholder(match[1]);
274 const newPlaceholderValue = placeholderValue.replace(/\$[1-9]/, '%');
275 message =
276 message.substring(0, match.index) + newPlaceholderValue + message.substring(match.index + placeholder.length);
277 // Modified the message, so search from the beginning of the string again.
278 placeholderRegex.lastIndex = 0;
279 }
280 return message;
281}
282
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000283async function parseGRDPFile(filePath) {
284 const fileContent = await localizationUtils.parseFileContent(filePath);
285
Mandy Chen78552632019-06-12 00:55:43 +0000286 function stripWhitespacePadding(message) {
287 let match = message.match(/^'''/);
288 if (match)
289 message = message.substring(3);
290 match = message.match(/(.*?)'''$/);
291 if (match)
292 message = match[1];
293 return message;
294 }
295
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000296 // Example:
Mandy Chen81d4fc42019-07-11 23:12:02 +0000297 // <message name="IDS_DEVTOOLS_md5_hash" desc="Description of this message">
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000298 // Message text here with optional placeholders <ph name="phname">$1s</ph>
299 // </message>
300 // match[0]: the entire '<message>...</message>' block.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000301 // match[1]: 'IDS_DEVTOOLS_md5_hash'
302 // match[2]: 'Description of this message'
303 // match[3]: ' Message text here with optional placeholders <ph name="phname">$1s</ph>\n '
304 const messageRegex = new RegExp('<message[^>]*name="([^"]*)"[^>]*desc="([^"]*)"[^>]*>\s*\n(.*?)<\/message>', 'gms');
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000305 let match;
306 while ((match = messageRegex.exec(fileContent)) !== null) {
Mandy Chend97200b2019-07-29 21:13:39 +0000307 const line = localizationUtils.lineNumberOfIndex(fileContent, match.index);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000308 const actualIDSKey = match[1];
309 const description = match[2];
310 let message = match[3];
Mandy Chen78552632019-06-12 00:55:43 +0000311 message = convertToFrontendPlaceholders(message.trim());
312 message = stripWhitespacePadding(message);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000313 message = localizationUtils.sanitizeStringIntoFrontendFormat(message);
314
315 const ids = localizationUtils.getIDSKey(message);
Mandy Chen4a7ad052019-07-16 16:09:29 +0000316 addMessage(ids, actualIDSKey, filePath, line, description);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000317 }
318}
319
Mandy Chen4a7ad052019-07-16 16:09:29 +0000320function addMessage(expectedIDSKey, actualIDSKey, grdpPath, line, description) {
321 if (!IDSkeys.has(expectedIDSKey))
322 IDSkeys.set(expectedIDSKey, []);
323
324 IDSkeys.get(expectedIDSKey).push({actualIDSKey, grdpPath, location: {start: {line}, end: {line}}, description});
325}
326
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000327/**
328 * The following functions compare frontend localizable strings
Mandy Chen81d4fc42019-07-11 23:12:02 +0000329 * with grdp <message>s and report error of resources to add,
330 * remove or modify.
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000331 */
Mandy Chen4a7ad052019-07-16 16:09:29 +0000332async function getAndReportResourcesToAdd() {
333 const keysToAddToGRD = getMessagesToAdd();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000334 if (keysToAddToGRD.size === 0)
335 return;
336
337 let errorStr = 'The following frontend string(s) need to be added to GRD/GRDP file(s).\n';
338 errorStr += 'Please refer to auto-generated message(s) below and modify as needed.\n\n';
339
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000340 // Example error message:
341 // third_party/blink/renderer/devtools/front_end/network/NetworkDataGridNode.js Line 973: ls`(disk cache)`
342 // Add a new message tag for this string to third_party\blink\renderer\devtools\front_end\network\network_strings.grdp
343 // <message name="IDS_DEVTOOLS_ad86890fb40822a3b12627efaca4ecd7" desc="Fill in the description.">
344 // (disk cache)
345 // </message>
346 for (const [key, stringObj] of keysToAddToGRD) {
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000347 errorStr += `${localizationUtils.getRelativeFilePathFromSrc(stringObj.filepath)}${
348 localizationUtils.getLocationMessage(stringObj.location)}: ${stringObj.code}\n`;
349 errorStr += `Add a new message tag for this string to ${
Mandy Chenc94d52a2019-06-11 22:51:53 +0000350 localizationUtils.getRelativeFilePathFromSrc(fileToGRDPMap.get(stringObj.filepath))}\n\n`;
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000351 errorStr += localizationUtils.createGrdpMessage(key, stringObj);
352 }
353 return errorStr;
354}
355
Mandy Chen4a7ad052019-07-16 16:09:29 +0000356function getAndReportResourcesToRemove() {
357 const keysToRemoveFromGRD = getMessagesToRemove();
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000358 if (keysToRemoveFromGRD.size === 0)
359 return;
360
361 let errorStr =
362 '\nThe message(s) associated with the following IDS key(s) should be removed from its GRD/GRDP file(s):\n';
363 // Example error message:
Mandy Chen4a7ad052019-07-16 16:09:29 +0000364 // third_party/blink/renderer/devtools/front_end/accessibility/accessibility_strings.grdp Line 300: IDS_DEVTOOLS_c9bbad3047af039c14d0e7ec957bb867
365 for (const [ids, messages] of keysToRemoveFromGRD) {
366 messages.forEach(
367 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
368 localizationUtils.getLocationMessage(message.location)}: ${ids}\n\n`);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000369 }
370 return errorStr;
371}
372
Mandy Chen81d4fc42019-07-11 23:12:02 +0000373function getAndReportIDSKeysToModify() {
374 const messagesToModify = getIDSKeysToModify();
375 if (messagesToModify.size === 0)
376 return;
377
378 let errorStr = '\nThe following GRD/GRDP message(s) do not have the correct IDS key.\n';
379 errorStr += 'Please update the key(s) by changing the "name" value.\n\n';
380
Mandy Chen4a7ad052019-07-16 16:09:29 +0000381 for (const [expectedIDSKey, messages] of messagesToModify) {
382 messages.forEach(
383 message => errorStr += `${localizationUtils.getRelativeFilePathFromSrc(message.grdpPath)}${
384 localizationUtils.getLocationMessage(
385 message.location)}:\n${message.actualIDSKey} --> ${expectedIDSKey}\n\n`);
Mandy Chen81d4fc42019-07-11 23:12:02 +0000386 }
387 return errorStr;
388}
389
Mandy Chen4a7ad052019-07-16 16:09:29 +0000390function getMessagesToAdd() {
391 // If a message with ids key exists in grdpPath
392 function messageExists(ids, grdpPath) {
393 const messages = IDSkeys.get(ids);
394 return messages.some(message => message.grdpPath === grdpPath);
395 }
396
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000397 const difference = [];
Mandy Chen4a7ad052019-07-16 16:09:29 +0000398 for (const [ids, frontendString] of frontendStrings) {
399 if (!IDSkeys.has(ids) || !messageExists(ids, frontendString.grdpPath))
400 difference.push([ids, frontendString]);
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000401 }
402 return new Map(difference.sort());
403}
404
Mandy Chen4a7ad052019-07-16 16:09:29 +0000405// Return a map from the expected IDS key to a list of messages
406// whose actual IDS keys need to be modified.
Mandy Chen81d4fc42019-07-11 23:12:02 +0000407function getIDSKeysToModify() {
408 const messagesToModify = new Map();
Mandy Chen4a7ad052019-07-16 16:09:29 +0000409 for (const [expectedIDSKey, messages] of IDSkeys) {
410 for (const message of messages) {
411 if (expectedIDSKey !== message.actualIDSKey) {
412 if (messagesToModify.has(expectedIDSKey))
413 messagesToModify.get(expectedIDSKey).push(message);
414 else
415 messagesToModify.set(expectedIDSKey, [message]);
416 }
417 }
Mandy Chen81d4fc42019-07-11 23:12:02 +0000418 }
419 return messagesToModify;
420}
421
Mandy Chen4a7ad052019-07-16 16:09:29 +0000422function getMessagesToRemove() {
423 const difference = new Map();
424 for (const [ids, messages] of IDSkeys) {
425 if (!frontendStrings.has(ids)) {
426 difference.set(ids, messages);
427 continue;
428 }
429
430 const expectedGrdpPath = frontendStrings.get(ids).grdpPath;
431 const messagesInGrdp = [];
432 const messagesToRemove = [];
433 messages.forEach(message => {
434 if (message.grdpPath !== expectedGrdpPath)
435 messagesToRemove.push(message);
436 else
437 messagesInGrdp.push(message);
438 });
439
440 if (messagesToRemove.length === 0 && messagesInGrdp.length === 1)
441 continue;
442
443 if (messagesInGrdp.length > 1) {
444 // If there are more than one messages with ids in the
445 // expected grdp file, keep one with the longest
446 // description and delete all the other messages
447 const longestDescription = getLongestDescription(messagesInGrdp);
448 let foundMessageToKeep = false;
449 for (const message of messagesInGrdp) {
450 if (message.description === longestDescription && !foundMessageToKeep) {
451 foundMessageToKeep = true;
452 continue;
453 }
454 messagesToRemove.push(message);
455 }
456 }
457 difference.set(ids, messagesToRemove);
458 }
459 return difference;
460}
461
462function getLongestDescription(messages) {
463 let longestDescription = '';
464 messages.forEach(message => {
465 if (message.description.length > longestDescription.length)
466 longestDescription = message.description;
467 });
468 return longestDescription;
469}
470
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000471module.exports = {
Mandy Chenc94d52a2019-06-11 22:51:53 +0000472 frontendStrings,
473 IDSkeys,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000474 parseLocalizableResourceMaps,
Mandy Chen81d4fc42019-07-11 23:12:02 +0000475 getAndReportIDSKeysToModify,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000476 getAndReportResourcesToAdd,
477 getAndReportResourcesToRemove,
Mandy Chen4a7ad052019-07-16 16:09:29 +0000478 getIDSKeysToModify,
479 getLongestDescription,
480 getMessagesToAdd,
481 getMessagesToRemove,
Lorne Mitchellc56ff2d2019-05-28 23:35:03 +0000482};