forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1/* eslint-disable no-console */
2import { join } from 'node:path'
3import { fileURLToPath } from 'node:url'
4import { createI18NReport, type I18NItem } from 'vue-i18n-extract'
5import { colors } from './utils/colors.ts'
6import { readdir, readFile, writeFile } from 'node:fs/promises'
7
8const LOCALES_DIRECTORY = fileURLToPath(new URL('../i18n/locales', import.meta.url))
9const REFERENCE_FILE_NAME = 'en.json'
10const VUE_FILES_GLOB = './app/**/*.?(vue|ts|js)'
11
12type NestedObject = Record<string, unknown>
13
14/** Removes a key path (e.g. "foo.bar.baz") from a nested object. Cleans up empty parents. */
15function removeKey(obj: NestedObject, path: string): boolean {
16 const parts = path.split('.')
17 if (parts.length === 1) {
18 if (path in obj) {
19 delete obj[path]
20 return true
21 }
22 return false
23 }
24 const [first, ...rest] = parts
25 const child = obj[first]
26 if (child && typeof child === 'object' && !Array.isArray(child)) {
27 const removed = removeKey(child as NestedObject, rest.join('.'))
28 if (removed && Object.keys(child as object).length === 0) {
29 delete obj[first]
30 }
31 return removed
32 }
33 return false
34}
35
36/** Removes multiple keys from a nested object. Sorts by depth (deepest first) to avoid parent/child conflicts. */
37function removeKeysFromObject(obj: NestedObject, keys: string[]): number {
38 const sortedKeys = [...keys].sort((a, b) => b.split('.').length - a.split('.').length)
39 let removed = 0
40 for (const key of sortedKeys) {
41 if (removeKey(obj, key)) removed++
42 }
43 return removed
44}
45
46async function run(): Promise<void> {
47 console.log(colors.bold('\n🔍 Removing unused i18n translations...\n'))
48
49 const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME)
50
51 const { unusedKeys } = await createI18NReport({
52 vueFiles: VUE_FILES_GLOB,
53 languageFiles: referenceFilePath,
54 exclude: ['$schema'],
55 })
56
57 if (unusedKeys.length === 0) {
58 console.log(colors.green('✅ No unused translations found. Nothing to remove.\n'))
59 return
60 }
61
62 const uniquePaths = [...new Set(unusedKeys.map((item: I18NItem) => item.path))]
63
64 // Remove from reference file
65 const referenceContent = JSON.parse(await readFile(referenceFilePath, 'utf-8')) as NestedObject
66 const refRemoved = removeKeysFromObject(referenceContent, uniquePaths)
67 await writeFile(referenceFilePath, JSON.stringify(referenceContent, null, 2) + '\n', 'utf-8')
68
69 // Remove from all other locale files
70 const localeFiles = (await readdir(LOCALES_DIRECTORY)).filter(
71 f => f.endsWith('.json') && f !== REFERENCE_FILE_NAME,
72 )
73
74 const otherLocalesSummary: { file: string; removed: number }[] = []
75 let totalOtherRemoved = 0
76
77 for (const localeFile of localeFiles) {
78 const filePath = join(LOCALES_DIRECTORY, localeFile)
79 const content = JSON.parse(await readFile(filePath, 'utf-8')) as NestedObject
80 const removed = removeKeysFromObject(content, uniquePaths)
81 if (removed > 0) {
82 await writeFile(filePath, JSON.stringify(content, null, 2) + '\n', 'utf-8')
83 otherLocalesSummary.push({ file: localeFile, removed })
84 totalOtherRemoved += removed
85 }
86 }
87
88 // Summary
89 console.log(colors.green(`✅ Removed ${refRemoved} keys from ${REFERENCE_FILE_NAME}`))
90 if (otherLocalesSummary.length > 0) {
91 console.log(
92 colors.green(
93 `✅ Removed ${totalOtherRemoved} keys from ${otherLocalesSummary.length} other locale(s)`,
94 ),
95 )
96 for (const { file, removed } of otherLocalesSummary) {
97 console.log(colors.dim(` ${file}: ${removed} keys`))
98 }
99 }
100 console.log(colors.dim(`\nTotal: ${uniquePaths.length} unique unused key(s) cleaned up\n`))
101}
102
103run().catch((error: unknown) => {
104 console.error(colors.red('\n❌ Unexpected error:'), error)
105 process.exit(1)
106})