cleanupUnusedLocales.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. const fs = require('fs')
  2. const Path = require('path')
  3. const { execSync } = require('child_process')
  4. const EN_JSON = Path.join(__dirname, '../../locales/en.json')
  5. const CHECK = process.argv.includes('--check')
  6. const SYNC_NON_EN = process.argv.includes('--sync-non-en')
  7. const COUNT_SUFFIXES = [
  8. '_plural',
  9. '_zero',
  10. '_one',
  11. '_two',
  12. '_few',
  13. '_many',
  14. '_other',
  15. ]
  16. async function main() {
  17. const locales = JSON.parse(await fs.promises.readFile(EN_JSON, 'utf-8'))
  18. const src = execSync(
  19. // - find all the app source files in web
  20. // - exclude data files
  21. // - exclude list of locales used in frontend
  22. // - exclude locales files
  23. // - exclude public assets
  24. // - exclude third-party dependencies
  25. // - exclude scripts
  26. // - exclude tests
  27. // - read all the source files
  28. `
  29. find . -type f \
  30. -not -path './data/*' \
  31. -not -path './frontend/extracted-translations.json' \
  32. -not -path './locales/*' \
  33. -not -path './public/*' \
  34. -not -path '*/node_modules/*' \
  35. -not -path '*/scripts/*' \
  36. -not -path '*/tests/*' \
  37. | xargs cat
  38. `,
  39. {
  40. // run from services/web directory
  41. cwd: Path.join(__dirname, '../../'),
  42. // 1GB
  43. maxBuffer: 1024 * 1024 * 1024,
  44. // Docs: https://nodejs.org/docs/latest-v16.x/api/child_process.html#child_process_options_stdio
  45. // Entries are [stdin, stdout, stderr]
  46. stdio: ['ignore', 'pipe', 'inherit'],
  47. }
  48. ).toString()
  49. const found = new Set([
  50. // Month names
  51. 'january',
  52. 'february',
  53. 'march',
  54. 'april',
  55. 'may',
  56. 'june',
  57. 'july',
  58. 'august',
  59. 'september',
  60. 'october',
  61. 'november',
  62. 'december',
  63. // Notifications created in third-party-datastore
  64. 'dropbox_email_not_verified',
  65. 'dropbox_unlinked_because_access_denied',
  66. 'dropbox_unlinked_because_full',
  67. // Actually used without the spurious space.
  68. // TODO: fix the space and upload the changed locales
  69. 'the_file_supplied_is_of_an_unsupported_type ',
  70. ])
  71. const matcher = new RegExp(
  72. `\\b(${Object.keys(locales)
  73. // Sort by length in descending order to match long, compound keys with
  74. // special characters (space or -) before short ones.
  75. // Examples:
  76. // - `\b(x|x-and-y)\b` will match `t('x-and-y')` as 'x'.
  77. // This is leaving 'x-and-y' as seemingly unused. Doh!
  78. // - `\b(x-and-y|x)\b` will match `t('x-and-y')` as 'x-and-y'. Yay!
  79. .sort((a, b) => (a.length < b.length ? 1 : -1))
  80. .join('|')})\\b`,
  81. 'g'
  82. )
  83. let m
  84. while ((m = matcher.exec(src))) {
  85. found.add(m[0])
  86. for (const suffix of COUNT_SUFFIXES) {
  87. found.add(m[0] + suffix)
  88. }
  89. }
  90. const unusedKeys = []
  91. for (const key of Object.keys(locales)) {
  92. if (!found.has(key)) {
  93. unusedKeys.push(key)
  94. }
  95. }
  96. if (SYNC_NON_EN) {
  97. if (CHECK) {
  98. throw new Error('--check is incompatible with --sync-non-en')
  99. }
  100. const LOCALES = Path.join(__dirname, '../../locales')
  101. for (const name of await fs.promises.readdir(LOCALES)) {
  102. if (name === 'README.md') continue
  103. if (name === 'en.json') continue
  104. const path = Path.join(LOCALES, name)
  105. const locales = JSON.parse(await fs.promises.readFile(path, 'utf-8'))
  106. for (const key of Object.keys(locales)) {
  107. if (!found.has(key)) {
  108. delete locales[key]
  109. }
  110. }
  111. const sorted =
  112. JSON.stringify(locales, Object.keys(locales).sort(), 2) + '\n'
  113. await fs.promises.writeFile(path, sorted)
  114. }
  115. }
  116. if (unusedKeys.length === 0) {
  117. return
  118. }
  119. console.warn('---')
  120. console.warn(
  121. `Found ${unusedKeys.length} unused translations keys:\n${unusedKeys
  122. .map(s => ` - '${s}'`)
  123. .join('\n')}`
  124. )
  125. console.warn('---')
  126. if (CHECK) {
  127. console.warn('---')
  128. console.warn(
  129. 'Try running:\n\n',
  130. ' web$ make cleanup_unused_locales',
  131. '\n'
  132. )
  133. console.warn('---')
  134. throw new Error('found unused translations keys')
  135. }
  136. console.log('Deleting unused translations keys')
  137. for (const key of unusedKeys) {
  138. delete locales[key]
  139. }
  140. const sorted = JSON.stringify(locales, Object.keys(locales).sort(), 2) + '\n'
  141. await fs.promises.writeFile(EN_JSON, sorted)
  142. }
  143. main().catch(error => {
  144. console.error(error)
  145. process.exit(1)
  146. })