2024-10-24 08:18:37 -04:00
|
|
|
import fs from 'fs'
|
|
|
|
import Path from 'path'
|
|
|
|
import { execSync } from 'child_process'
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { loadLocale } from './utils.js'
|
2023-01-12 04:51:34 -05:00
|
|
|
|
2024-10-24 08:18:37 -04:00
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
2023-01-12 04:51:34 -05:00
|
|
|
const EN_JSON = Path.join(__dirname, '../../locales/en.json')
|
|
|
|
const CHECK = process.argv.includes('--check')
|
2023-01-12 04:51:42 -05:00
|
|
|
const SYNC_NON_EN = process.argv.includes('--sync-non-en')
|
2023-01-12 04:51:34 -05:00
|
|
|
|
2023-02-07 09:58:25 -05:00
|
|
|
const COUNT_SUFFIXES = [
|
|
|
|
'_plural',
|
|
|
|
'_zero',
|
|
|
|
'_one',
|
|
|
|
'_two',
|
|
|
|
'_few',
|
|
|
|
'_many',
|
|
|
|
'_other',
|
|
|
|
]
|
|
|
|
|
2023-01-12 04:51:34 -05:00
|
|
|
async function main() {
|
2024-10-24 08:18:37 -04:00
|
|
|
const locales = loadLocale('en')
|
2023-01-12 04:51:34 -05:00
|
|
|
|
|
|
|
const src = execSync(
|
|
|
|
// - find all the app source files in web
|
|
|
|
// - exclude data files
|
2023-02-07 09:58:25 -05:00
|
|
|
// - exclude list of locales used in frontend
|
2023-01-12 04:51:34 -05:00
|
|
|
// - exclude locales files
|
|
|
|
// - exclude public assets
|
|
|
|
// - exclude third-party dependencies
|
2023-02-08 04:23:42 -05:00
|
|
|
// - exclude scripts
|
|
|
|
// - exclude tests
|
2023-01-12 04:51:34 -05:00
|
|
|
// - read all the source files
|
|
|
|
`
|
|
|
|
find . -type f \
|
2023-04-12 10:10:47 -04:00
|
|
|
-not -path './cypress/results/*' \
|
2023-01-12 04:51:34 -05:00
|
|
|
-not -path './data/*' \
|
2023-02-07 09:58:25 -05:00
|
|
|
-not -path './frontend/extracted-translations.json' \
|
2023-01-12 04:51:34 -05:00
|
|
|
-not -path './locales/*' \
|
|
|
|
-not -path './public/*' \
|
2023-02-08 04:23:42 -05:00
|
|
|
-not -path '*/node_modules/*' \
|
|
|
|
-not -path '*/scripts/*' \
|
|
|
|
-not -path '*/tests/*' \
|
2023-04-12 10:10:47 -04:00
|
|
|
-exec cat {} +
|
2023-01-12 04:51:34 -05:00
|
|
|
`,
|
|
|
|
{
|
|
|
|
// run from services/web directory
|
|
|
|
cwd: Path.join(__dirname, '../../'),
|
|
|
|
// 1GB
|
|
|
|
maxBuffer: 1024 * 1024 * 1024,
|
|
|
|
// Docs: https://nodejs.org/docs/latest-v16.x/api/child_process.html#child_process_options_stdio
|
|
|
|
// Entries are [stdin, stdout, stderr]
|
|
|
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
|
|
}
|
|
|
|
).toString()
|
|
|
|
|
|
|
|
const found = new Set([
|
|
|
|
// Month names
|
|
|
|
'january',
|
|
|
|
'february',
|
|
|
|
'march',
|
|
|
|
'april',
|
|
|
|
'may',
|
|
|
|
'june',
|
|
|
|
'july',
|
|
|
|
'august',
|
|
|
|
'september',
|
|
|
|
'october',
|
|
|
|
'november',
|
|
|
|
'december',
|
|
|
|
|
|
|
|
// Notifications created in third-party-datastore
|
|
|
|
'dropbox_email_not_verified',
|
|
|
|
'dropbox_unlinked_because_access_denied',
|
|
|
|
'dropbox_unlinked_because_full',
|
|
|
|
|
|
|
|
// Actually used without the spurious space.
|
|
|
|
// TODO: fix the space and upload the changed locales
|
|
|
|
'the_file_supplied_is_of_an_unsupported_type ',
|
|
|
|
])
|
|
|
|
const matcher = new RegExp(
|
|
|
|
`\\b(${Object.keys(locales)
|
|
|
|
// Sort by length in descending order to match long, compound keys with
|
|
|
|
// special characters (space or -) before short ones.
|
|
|
|
// Examples:
|
|
|
|
// - `\b(x|x-and-y)\b` will match `t('x-and-y')` as 'x'.
|
|
|
|
// This is leaving 'x-and-y' as seemingly unused. Doh!
|
|
|
|
// - `\b(x-and-y|x)\b` will match `t('x-and-y')` as 'x-and-y'. Yay!
|
|
|
|
.sort((a, b) => (a.length < b.length ? 1 : -1))
|
|
|
|
.join('|')})\\b`,
|
|
|
|
'g'
|
|
|
|
)
|
|
|
|
let m
|
|
|
|
while ((m = matcher.exec(src))) {
|
|
|
|
found.add(m[0])
|
2023-02-07 09:58:25 -05:00
|
|
|
for (const suffix of COUNT_SUFFIXES) {
|
|
|
|
found.add(m[0] + suffix)
|
|
|
|
}
|
2023-01-12 04:51:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const unusedKeys = []
|
|
|
|
for (const key of Object.keys(locales)) {
|
|
|
|
if (!found.has(key)) {
|
|
|
|
unusedKeys.push(key)
|
|
|
|
}
|
|
|
|
}
|
2023-01-12 04:51:42 -05:00
|
|
|
|
|
|
|
if (SYNC_NON_EN) {
|
|
|
|
if (CHECK) {
|
|
|
|
throw new Error('--check is incompatible with --sync-non-en')
|
|
|
|
}
|
|
|
|
const LOCALES = Path.join(__dirname, '../../locales')
|
|
|
|
for (const name of await fs.promises.readdir(LOCALES)) {
|
|
|
|
if (name === 'README.md') continue
|
|
|
|
if (name === 'en.json') continue
|
|
|
|
const path = Path.join(LOCALES, name)
|
2024-10-24 08:18:37 -04:00
|
|
|
const locales = loadLocale(name.replace('.json', ''))
|
2023-01-12 04:51:42 -05:00
|
|
|
for (const key of Object.keys(locales)) {
|
|
|
|
if (!found.has(key)) {
|
|
|
|
delete locales[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const sorted =
|
|
|
|
JSON.stringify(locales, Object.keys(locales).sort(), 2) + '\n'
|
|
|
|
await fs.promises.writeFile(path, sorted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-12 04:51:34 -05:00
|
|
|
if (unusedKeys.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
console.warn('---')
|
|
|
|
console.warn(
|
|
|
|
`Found ${unusedKeys.length} unused translations keys:\n${unusedKeys
|
|
|
|
.map(s => ` - '${s}'`)
|
|
|
|
.join('\n')}`
|
|
|
|
)
|
|
|
|
console.warn('---')
|
|
|
|
|
|
|
|
if (CHECK) {
|
|
|
|
console.warn('---')
|
|
|
|
console.warn(
|
|
|
|
'Try running:\n\n',
|
|
|
|
' web$ make cleanup_unused_locales',
|
|
|
|
'\n'
|
|
|
|
)
|
|
|
|
console.warn('---')
|
|
|
|
throw new Error('found unused translations keys')
|
|
|
|
}
|
|
|
|
console.log('Deleting unused translations keys')
|
|
|
|
for (const key of unusedKeys) {
|
|
|
|
delete locales[key]
|
|
|
|
}
|
|
|
|
const sorted = JSON.stringify(locales, Object.keys(locales).sort(), 2) + '\n'
|
|
|
|
await fs.promises.writeFile(EN_JSON, sorted)
|
|
|
|
}
|
|
|
|
|
2024-10-24 08:18:37 -04:00
|
|
|
try {
|
|
|
|
await main()
|
|
|
|
} catch (error) {
|
2023-01-12 04:51:34 -05:00
|
|
|
console.error(error)
|
|
|
|
process.exit(1)
|
2024-10-24 08:18:37 -04:00
|
|
|
}
|