| 
 | 1 | +const fs = require('fs');  | 
 | 2 | +const path = require('path');  | 
 | 3 | +const flowParser = require('flow-parser');  | 
 | 4 | + | 
 | 5 | +const { parse } = require('recast');  | 
 | 6 | +const { namedTypes: n, visit } = require('ast-types');  | 
 | 7 | + | 
 | 8 | +const messages_en = require('../static/translations/messages_en.json');  | 
 | 9 | + | 
 | 10 | +// Make a list of files that might contain UI strings, by recursing in src/.  | 
 | 11 | +const possibleUIStringFilePaths = [];  | 
 | 12 | +const kSrcDirName = 'src/';  | 
 | 13 | +function walk(dir, _dirName = '') {  | 
 | 14 | +  let dirent;  | 
 | 15 | +  // eslint-disable-next-line no-cond-assign  | 
 | 16 | +  while ((dirent = dir.readSync())) {  | 
 | 17 | +    // To reduce false negatives, `continue` when nothing in `dirent` can  | 
 | 18 | +    // cause UI strings to appear in the app.  | 
 | 19 | + | 
 | 20 | +    if (dirent.isFile()) {  | 
 | 21 | +      // Non-JS code, and Flow type definitions in .js.flow files.  | 
 | 22 | +      if (!dirent.name.endsWith('.js')) {  | 
 | 23 | +        continue;  | 
 | 24 | +      }  | 
 | 25 | + | 
 | 26 | +      possibleUIStringFilePaths.push(path.join(kSrcDirName, _dirName, dirent.name));  | 
 | 27 | +    } else if (dirent.isDirectory()) {  | 
 | 28 | +      const subdirName = path.join(_dirName, dirent.name);  | 
 | 29 | + | 
 | 30 | +      // Test code.  | 
 | 31 | +      if (subdirName.endsWith('__tests__')) {  | 
 | 32 | +        continue;  | 
 | 33 | +      }  | 
 | 34 | + | 
 | 35 | +      walk(fs.opendirSync(path.join(kSrcDirName, subdirName)), subdirName);  | 
 | 36 | +    } else {  | 
 | 37 | +      // Something we don't expect to find under src/, probably containing  | 
 | 38 | +      // no UI strings. (symlinks? fifos, sockets, devices??)  | 
 | 39 | +      continue;  | 
 | 40 | +    }  | 
 | 41 | +  }  | 
 | 42 | +}  | 
 | 43 | +walk(fs.opendirSync(kSrcDirName));  | 
 | 44 | + | 
 | 45 | +const parseOptions = {  | 
 | 46 | +  parser: {  | 
 | 47 | +    parse(src) {  | 
 | 48 | +      return flowParser.parse(src, {  | 
 | 49 | +        // Comments can't cause UI strings to appear in the app; ignore them.  | 
 | 50 | +        all_comments: false,  | 
 | 51 | +        comments: false,  | 
 | 52 | + | 
 | 53 | +        // We plan to use Flow enums; the parser shouldn't crash on them.  | 
 | 54 | +        enums: true,  | 
 | 55 | + | 
 | 56 | +        // Set `tokens: true` just to work around a mysterious error.  | 
 | 57 | +        //  | 
 | 58 | +        // From the doc for this option:  | 
 | 59 | +        //  | 
 | 60 | +        // > include a list of all parsed tokens in a top-level tokens  | 
 | 61 | +        // > property  | 
 | 62 | +        //  | 
 | 63 | +        // We don't actually want this list of tokens. String literals do  | 
 | 64 | +        // get represented in the list, but as tokens, i.e., meaningful  | 
 | 65 | +        // chunks of the literal source code. They come with surrounding  | 
 | 66 | +        // quotes, escape syntax, etc:  | 
 | 67 | +        //  | 
 | 68 | +        //   'doesn\'t'  | 
 | 69 | +        //   "doesn't"  | 
 | 70 | +        //  | 
 | 71 | +        // What we really want is the *value* of a string literal:  | 
 | 72 | +        //  | 
 | 73 | +        //   doesn't  | 
 | 74 | +        //  | 
 | 75 | +        // and we get that from the AST.  | 
 | 76 | +        //  | 
 | 77 | +        // Anyway, we set `true` for this because otherwise I've been seeing  | 
 | 78 | +        // `parse` throw an error:  | 
 | 79 | +        //  | 
 | 80 | +        //   Error: Line 72: Invalid regular expression: missing /  | 
 | 81 | +        //  | 
 | 82 | +        // TODO: Debug and/or file an issue upstream.  | 
 | 83 | +        tokens: true,  | 
 | 84 | +      });  | 
 | 85 | +    },  | 
 | 86 | +  },  | 
 | 87 | +};  | 
 | 88 | + | 
 | 89 | +// Look at all files in possibleUIStringFilePaths, and collect all string  | 
 | 90 | +// literals that might represent UI strings.  | 
 | 91 | +const possibleUiStringLiterals = new Set();  | 
 | 92 | +possibleUIStringFilePaths.forEach(filePath => {  | 
 | 93 | +  const source = fs.readFileSync(filePath).toString();  | 
 | 94 | +  const ast = parse(source, parseOptions);  | 
 | 95 | + | 
 | 96 | +  visit(ast, {  | 
 | 97 | +    // Find nodes with type "Literal" in the AST.  | 
 | 98 | +    /* eslint-disable no-shadow */  | 
 | 99 | +    visitLiteral(path) {  | 
 | 100 | +      // To reduce false negatives, return false when `path` definitely  | 
 | 101 | +      // doesn't represent a string literal for a UI string in the app.  | 
 | 102 | + | 
 | 103 | +      const { value } = path.value;  | 
 | 104 | + | 
 | 105 | +      // Non-string literals: numbers, booleans, etc.  | 
 | 106 | +      if (typeof value !== 'string') {  | 
 | 107 | +        return false;  | 
 | 108 | +      }  | 
 | 109 | + | 
 | 110 | +      // String literals like 'react' in lines like  | 
 | 111 | +      //   import React from 'react';  | 
 | 112 | +      if (n.ImportDeclaration.check(path.parent.value)) {  | 
 | 113 | +        return false;  | 
 | 114 | +      }  | 
 | 115 | + | 
 | 116 | +      possibleUiStringLiterals.add(value);  | 
 | 117 | + | 
 | 118 | +      return this.traverse(path);  | 
 | 119 | +    },  | 
 | 120 | +  });  | 
 | 121 | +});  | 
 | 122 | + | 
 | 123 | +// Check each key ("message ID" in formatjs's lingo) against  | 
 | 124 | +// possibleUiStringLiterals, and make a list of any that aren't found.  | 
 | 125 | +const danglingMessageIds = Object.keys(messages_en).filter(  | 
 | 126 | +  messageId => !possibleUiStringLiterals.has(messageId),  | 
 | 127 | +);  | 
 | 128 | + | 
 | 129 | +if (danglingMessageIds.length > 0) {  | 
 | 130 | +  console.warn(  | 
 | 131 | +    "Found message IDs in static/translations/messages_en.json that don't seem to be used in the app:",  | 
 | 132 | +  );  | 
 | 133 | +  console.warn(danglingMessageIds);  | 
 | 134 | +}  | 
0 commit comments