|  | 
|  | 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