Skip to content

Commit 3478f63

Browse files
committed
feat: add unused locales
1 parent cc77ff7 commit 3478f63

File tree

11 files changed

+610
-1
lines changed

11 files changed

+610
-1
lines changed

.changeset/khaki-ways-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"unused-i18n": patch
3+
---
4+
5+
Add unused locales to scaleway-lib
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as fs from 'fs'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { processTranslations } from '../index'
4+
import { analyze } from '../lib/analyze'
5+
import { searchFilesRecursively } from '../lib/search'
6+
import { loadConfig } from '../utils/loadConfig'
7+
import { getMissingTranslations } from '../utils/missingTranslations'
8+
import { shouldExclude } from '../utils/shouldExclude'
9+
10+
// Mock dependencies
11+
vi.mock('fs', () => ({
12+
existsSync: vi.fn(),
13+
readFileSync: vi.fn(),
14+
writeFileSync: vi.fn(),
15+
}))
16+
17+
vi.mock('../utils/loadConfig', () => ({
18+
loadConfig: vi.fn(),
19+
}))
20+
21+
vi.mock('../lib/search', () => ({
22+
searchFilesRecursively: vi.fn(),
23+
}))
24+
25+
vi.mock('../lib/analyze', () => ({
26+
analyze: vi.fn(),
27+
}))
28+
29+
vi.mock('../utils/shouldExclude', () => ({
30+
shouldExclude: vi.fn(),
31+
}))
32+
33+
vi.mock('../utils/missingTranslations', () => ({
34+
getMissingTranslations: vi.fn(),
35+
}))
36+
37+
describe('processTranslations', () => {
38+
beforeEach(() => {
39+
vi.resetAllMocks()
40+
})
41+
42+
it('should process translations correctly', async () => {
43+
const config = {
44+
paths: [
45+
{
46+
srcPath: ['srcPath'],
47+
localPath: 'localPath',
48+
},
49+
],
50+
excludeKey: [],
51+
scopedNames: ['scopedT'],
52+
localesExtensions: 'ts',
53+
localesNames: 'en',
54+
ignorePaths: ['folder/file.ts'],
55+
}
56+
57+
const files = ['file1.ts', 'folder/file.ts']
58+
const extractedTranslations = ['key1', 'key2']
59+
const localeContent = `
60+
export default {
61+
'key1': 'value1',
62+
'key2': 'value2',
63+
'key3': 'value3',
64+
'key4': 'value4',
65+
} as const
66+
`.trim()
67+
68+
const expectedWriteContent = `
69+
export default {
70+
'key1': 'value1',
71+
'key2': 'value2',
72+
} as const
73+
`.trim()
74+
75+
vi.mocked(loadConfig).mockResolvedValue(config)
76+
vi.mocked(searchFilesRecursively).mockReturnValue(files)
77+
vi.mocked(analyze).mockReturnValue(extractedTranslations)
78+
vi.mocked(shouldExclude).mockReturnValue(false)
79+
vi.mocked(getMissingTranslations).mockReturnValue(['key3', 'key4'])
80+
vi.mocked(fs.existsSync).mockReturnValue(true)
81+
vi.mocked(fs.readFileSync).mockReturnValue(localeContent)
82+
vi.mocked(fs.writeFileSync).mockImplementation(vi.fn())
83+
84+
await processTranslations({ action: 'remove' })
85+
86+
expect(fs.existsSync).toHaveBeenCalledWith('localPath/en.ts')
87+
expect(fs.readFileSync).toHaveBeenCalledWith('localPath/en.ts', 'utf-8')
88+
expect(fs.writeFileSync).toHaveBeenCalledWith(
89+
'localPath/en.ts',
90+
expectedWriteContent,
91+
'utf-8',
92+
)
93+
})
94+
})

packages/unused-i18n/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const processTranslations = async ({
115115
)}ms\x1b[0m`,
116116
)
117117

118-
if (totalUnusedLocales > 0) {
118+
if (totalUnusedLocales > 0 && process.env['NODE_ENV'] !== 'test') {
119119
process.exit(1)
120120
}
121121
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as fs from 'fs'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { analyze } from '../analyze'
4+
import { extractGlobalT } from '../global/extractGlobalT'
5+
import { extractNamespaceTranslation } from '../scopedNamespace/extractNamespaceTranslation'
6+
import { extractScopedTs } from '../scopedNamespace/extractScopedTs'
7+
8+
const mockFilePath = '/path/to/test/file.js'
9+
const mockScopedNames = ['scopedT', 'scopedTOne']
10+
11+
const fileContent = `
12+
import { useI18n } from '@scaleway/use-i18n'
13+
const scopedT = namespaceTranslation('namespace');
14+
{keyLabel ?? scopedT('labelKey1')}
15+
{keyLabel ? scopedT('labelKey2') : scopedT('labelKey3')}
16+
{scopedT(keyLabel ? 'labelKey4' : 'labelKey5')}
17+
{scopedT(\`labelKey6.\${variable}\`)}
18+
{scopedT(variable0)}
19+
{scopedT(\`\${variable1}.\${variable2}\`)}
20+
{t(\`\${variable3}.\${variable4}\`)}
21+
{keyLabel ?? t('labelKey8')}
22+
{keyLabel ? t('labelKey9') : t('labelKey10')}
23+
{t(\`labelKey11.\${variable5}\`)}
24+
{t(\`labelKey12.\${variable6}\`)}
25+
toast.success(t('account.user.modal.edit.changeEmail'));
26+
{ [FORM_ERROR]: t('form.errors.formErrorNoRetry') };
27+
{scopedTOne('labelKey13', {
28+
name: scopedT('labelKey14')
29+
})}
30+
`
31+
32+
const expectedTranslationResults = [
33+
'account.user.modal.edit.changeEmail',
34+
'form.errors.formErrorNoRetry',
35+
'labelKey10',
36+
'labelKey11.**',
37+
'labelKey12.**',
38+
'labelKey8',
39+
'labelKey9',
40+
'**.**',
41+
'namespace.**',
42+
'namespace.**.**',
43+
'namespace.labelKey1',
44+
'namespace.labelKey13',
45+
'namespace.labelKey14',
46+
'namespace.labelKey2',
47+
'namespace.labelKey3',
48+
'namespace.labelKey4',
49+
'namespace.labelKey5',
50+
'namespace.labelKey6.**',
51+
]
52+
53+
vi.mock('fs')
54+
55+
vi.mock('../global/extractGlobalT', () => ({
56+
extractGlobalT: vi.fn(() => [
57+
'labelKey8',
58+
'labelKey9',
59+
'labelKey10',
60+
'labelKey11.**',
61+
'labelKey12.**',
62+
'account.user.modal.edit.changeEmail',
63+
'form.errors.formErrorNoRetry',
64+
]),
65+
}))
66+
67+
vi.mock('../scopedNamespace/extractNamespaceTranslation', () => ({
68+
extractNamespaceTranslation: vi.fn(() => [
69+
'namespace.labelKey1',
70+
'namespace.labelKey2',
71+
'namespace.labelKey3',
72+
'namespace.labelKey4',
73+
'namespace.labelKey5',
74+
'namespace.labelKey6.**',
75+
'namespace.**',
76+
'namespace.**.**',
77+
]),
78+
}))
79+
80+
vi.mock('../scopedNamespace/extractScopedTs', () => ({
81+
extractScopedTs: vi.fn(() => [
82+
'**.**',
83+
'namespace.**',
84+
'namespace.**.**',
85+
'namespace.labelKey1',
86+
'namespace.labelKey13',
87+
'namespace.labelKey14',
88+
'namespace.labelKey2',
89+
'namespace.labelKey3',
90+
'namespace.labelKey4',
91+
'namespace.labelKey5',
92+
'namespace.labelKey6.**',
93+
]),
94+
}))
95+
96+
describe('analyze', () => {
97+
it('should extract all translations correctly from the file', () => {
98+
vi.mocked(fs.readFileSync).mockReturnValue(fileContent)
99+
100+
const result = analyze({
101+
filePath: mockFilePath,
102+
scopedNames: mockScopedNames,
103+
})
104+
105+
expect(fs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf-8')
106+
expect(extractGlobalT).toHaveBeenCalledWith({ fileContent })
107+
expect(extractNamespaceTranslation).toHaveBeenCalledWith({ fileContent })
108+
109+
expect(extractScopedTs).toHaveBeenCalledWith({
110+
fileContent,
111+
namespaceTranslation: 'namespace.labelKey1',
112+
scopedName: 'scopedT',
113+
})
114+
115+
expect(extractScopedTs).toHaveBeenCalledTimes(16)
116+
117+
expect(extractScopedTs).toHaveBeenNthCalledWith(5, {
118+
fileContent,
119+
namespaceTranslation: 'namespace.labelKey5',
120+
scopedName: 'scopedT',
121+
})
122+
123+
expect(result).toEqual(expect.arrayContaining(expectedTranslationResults))
124+
expect(expectedTranslationResults).toEqual(expect.arrayContaining(result))
125+
})
126+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as fs from 'fs'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { removeLocaleKeys } from '../remove'
4+
5+
vi.mock('fs')
6+
7+
describe('removeLocaleKeys', () => {
8+
it('should remove specified locale keys from the file', () => {
9+
const localePath = 'path/to/locale/en.js'
10+
const missingTranslations = ['key1', 'key4', 'key2']
11+
12+
const fileContent = `export default {
13+
'key1': 'value1',
14+
'key2': 'value2',
15+
'key3': 'value3',
16+
'key4':
17+
'value4',
18+
'key5': 'value5',
19+
} as const`
20+
21+
const expectedContent = `
22+
export default {
23+
'key3': 'value3',
24+
'key5': 'value5',
25+
} as const`
26+
27+
const fsMock = {
28+
readFileSync: vi.fn().mockReturnValue(fileContent),
29+
writeFileSync: vi.fn(),
30+
}
31+
32+
vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
33+
vi.mocked(fs.writeFileSync).mockImplementation(fsMock.writeFileSync)
34+
35+
removeLocaleKeys({ localePath, missingTranslations })
36+
37+
expect(fs.readFileSync).toHaveBeenCalledWith(localePath, 'utf-8')
38+
expect(fs.writeFileSync).toHaveBeenCalledWith(
39+
localePath,
40+
expectedContent.trim(),
41+
'utf-8',
42+
)
43+
})
44+
it('should remove specified locale keys from the file on multi line', () => {
45+
const localePath = 'path/to/locale/en.js'
46+
const missingTranslations = ['key1', 'key5']
47+
48+
const fileContent = `export default {
49+
'key1': 'value1',
50+
'key2': 'value2',
51+
'key3': 'value3',
52+
'key4':
53+
'value4',
54+
'key5': 'value5',
55+
} as const`
56+
57+
const expectedContent = `
58+
export default {
59+
'key2': 'value2',
60+
'key3': 'value3',
61+
'key4':
62+
'value4',
63+
} as const`
64+
65+
const fsMock = {
66+
readFileSync: vi.fn().mockReturnValue(fileContent),
67+
writeFileSync: vi.fn(),
68+
}
69+
70+
vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
71+
vi.mocked(fs.writeFileSync).mockImplementation(fsMock.writeFileSync)
72+
73+
removeLocaleKeys({ localePath, missingTranslations })
74+
75+
expect(fs.readFileSync).toHaveBeenCalledWith(localePath, 'utf-8')
76+
expect(fs.writeFileSync).toHaveBeenCalledWith(
77+
localePath,
78+
expectedContent.trim(),
79+
'utf-8',
80+
)
81+
})
82+
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { describe, expect, it, vi } from 'vitest'
4+
import { searchFilesRecursively } from "../search"
5+
6+
vi.mock('fs')
7+
8+
describe('searchFilesRecursively', () => {
9+
it('should find files where content matches the regex pattern', () => {
10+
const baseDir = 'testDir'
11+
const regex = /use-i18n/
12+
13+
const fsMock = {
14+
readdirSync: vi.fn(dir => {
15+
if (dir === baseDir) return ['file1.js', 'file2.js', 'subdir']
16+
if (dir === path.join(baseDir, 'subdir')) return ['file3.js']
17+
18+
return []
19+
}),
20+
lstatSync: vi.fn(filePath => ({
21+
isDirectory: () => filePath === path.join(baseDir, 'subdir'),
22+
})),
23+
readFileSync: vi.fn(filePath => {
24+
if (filePath === path.join(baseDir, 'file1.js')) {
25+
return `
26+
import { useI18n } from '@scaleway/use-i18n'
27+
`
28+
}
29+
if (filePath === path.join(baseDir, 'file2.js')) return 'no match here'
30+
if (filePath === path.join(baseDir, 'subdir', 'file3.js')) {
31+
return `
32+
import { useI18n } from '@scaleway/use-i18n'
33+
`
34+
}
35+
36+
return ''
37+
}),
38+
}
39+
40+
vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
41+
// @ts-expect-error mockImplementation no function
42+
vi.mocked(fs.lstatSync).mockImplementation(fsMock.lstatSync)
43+
// @ts-expect-error mockImplementation no function
44+
vi.mocked(fs.readdirSync).mockImplementation(fsMock.readdirSync)
45+
46+
const expected = [
47+
path.join(baseDir, 'file1.js'),
48+
path.join(baseDir, 'subdir', 'file3.js'),
49+
]
50+
51+
const result = searchFilesRecursively({
52+
baseDir,
53+
regex,
54+
excludePatterns: [],
55+
})
56+
57+
expect(result).toEqual(expect.arrayContaining(expected))
58+
expect(expected).toEqual(expect.arrayContaining(result))
59+
})
60+
})

0 commit comments

Comments
 (0)