cleanupUnusedLocales.js 4.5 KB

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