Skip to content

Commit

Permalink
feat: add unused locales
Browse files Browse the repository at this point in the history
  • Loading branch information
Lawndlwd committed Feb 20, 2025
1 parent cc77ff7 commit 3478f63
Show file tree
Hide file tree
Showing 11 changed files with 610 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/khaki-ways-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"unused-i18n": patch
---

Add unused locales to scaleway-lib
94 changes: 94 additions & 0 deletions packages/unused-i18n/src/__tests__/processTranslations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fs from 'fs'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { processTranslations } from '../index'
import { analyze } from '../lib/analyze'
import { searchFilesRecursively } from '../lib/search'
import { loadConfig } from '../utils/loadConfig'
import { getMissingTranslations } from '../utils/missingTranslations'
import { shouldExclude } from '../utils/shouldExclude'

// Mock dependencies
vi.mock('fs', () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
}))

vi.mock('../utils/loadConfig', () => ({
loadConfig: vi.fn(),
}))

vi.mock('../lib/search', () => ({
searchFilesRecursively: vi.fn(),
}))

vi.mock('../lib/analyze', () => ({
analyze: vi.fn(),
}))

vi.mock('../utils/shouldExclude', () => ({
shouldExclude: vi.fn(),
}))

vi.mock('../utils/missingTranslations', () => ({
getMissingTranslations: vi.fn(),
}))

describe('processTranslations', () => {
beforeEach(() => {
vi.resetAllMocks()
})

it('should process translations correctly', async () => {
const config = {
paths: [
{
srcPath: ['srcPath'],
localPath: 'localPath',
},
],
excludeKey: [],
scopedNames: ['scopedT'],
localesExtensions: 'ts',
localesNames: 'en',
ignorePaths: ['folder/file.ts'],
}

const files = ['file1.ts', 'folder/file.ts']
const extractedTranslations = ['key1', 'key2']
const localeContent = `
export default {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
'key4': 'value4',
} as const
`.trim()

const expectedWriteContent = `
export default {
'key1': 'value1',
'key2': 'value2',
} as const
`.trim()

vi.mocked(loadConfig).mockResolvedValue(config)
vi.mocked(searchFilesRecursively).mockReturnValue(files)
vi.mocked(analyze).mockReturnValue(extractedTranslations)
vi.mocked(shouldExclude).mockReturnValue(false)
vi.mocked(getMissingTranslations).mockReturnValue(['key3', 'key4'])
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue(localeContent)
vi.mocked(fs.writeFileSync).mockImplementation(vi.fn())

await processTranslations({ action: 'remove' })

expect(fs.existsSync).toHaveBeenCalledWith('localPath/en.ts')
expect(fs.readFileSync).toHaveBeenCalledWith('localPath/en.ts', 'utf-8')
expect(fs.writeFileSync).toHaveBeenCalledWith(
'localPath/en.ts',
expectedWriteContent,
'utf-8',
)
})
})
2 changes: 1 addition & 1 deletion packages/unused-i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const processTranslations = async ({
)}ms\x1b[0m`,
)

if (totalUnusedLocales > 0) {
if (totalUnusedLocales > 0 && process.env['NODE_ENV'] !== 'test') {
process.exit(1)

Check warning on line 119 in packages/unused-i18n/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/unused-i18n/src/index.ts#L119

Added line #L119 was not covered by tests
}
}
126 changes: 126 additions & 0 deletions packages/unused-i18n/src/lib/__tests__/analyze.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as fs from 'fs'
import { describe, expect, it, vi } from 'vitest'
import { analyze } from '../analyze'
import { extractGlobalT } from '../global/extractGlobalT'
import { extractNamespaceTranslation } from '../scopedNamespace/extractNamespaceTranslation'
import { extractScopedTs } from '../scopedNamespace/extractScopedTs'

const mockFilePath = '/path/to/test/file.js'
const mockScopedNames = ['scopedT', 'scopedTOne']

const fileContent = `
import { useI18n } from '@scaleway/use-i18n'
const scopedT = namespaceTranslation('namespace');
{keyLabel ?? scopedT('labelKey1')}
{keyLabel ? scopedT('labelKey2') : scopedT('labelKey3')}
{scopedT(keyLabel ? 'labelKey4' : 'labelKey5')}
{scopedT(\`labelKey6.\${variable}\`)}
{scopedT(variable0)}
{scopedT(\`\${variable1}.\${variable2}\`)}
{t(\`\${variable3}.\${variable4}\`)}
{keyLabel ?? t('labelKey8')}
{keyLabel ? t('labelKey9') : t('labelKey10')}
{t(\`labelKey11.\${variable5}\`)}
{t(\`labelKey12.\${variable6}\`)}
toast.success(t('account.user.modal.edit.changeEmail'));
{ [FORM_ERROR]: t('form.errors.formErrorNoRetry') };
{scopedTOne('labelKey13', {
name: scopedT('labelKey14')
})}
`

const expectedTranslationResults = [
'account.user.modal.edit.changeEmail',
'form.errors.formErrorNoRetry',
'labelKey10',
'labelKey11.**',
'labelKey12.**',
'labelKey8',
'labelKey9',
'**.**',
'namespace.**',
'namespace.**.**',
'namespace.labelKey1',
'namespace.labelKey13',
'namespace.labelKey14',
'namespace.labelKey2',
'namespace.labelKey3',
'namespace.labelKey4',
'namespace.labelKey5',
'namespace.labelKey6.**',
]

vi.mock('fs')

vi.mock('../global/extractGlobalT', () => ({
extractGlobalT: vi.fn(() => [
'labelKey8',
'labelKey9',
'labelKey10',
'labelKey11.**',
'labelKey12.**',
'account.user.modal.edit.changeEmail',
'form.errors.formErrorNoRetry',
]),
}))

vi.mock('../scopedNamespace/extractNamespaceTranslation', () => ({
extractNamespaceTranslation: vi.fn(() => [
'namespace.labelKey1',
'namespace.labelKey2',
'namespace.labelKey3',
'namespace.labelKey4',
'namespace.labelKey5',
'namespace.labelKey6.**',
'namespace.**',
'namespace.**.**',
]),
}))

vi.mock('../scopedNamespace/extractScopedTs', () => ({
extractScopedTs: vi.fn(() => [
'**.**',
'namespace.**',
'namespace.**.**',
'namespace.labelKey1',
'namespace.labelKey13',
'namespace.labelKey14',
'namespace.labelKey2',
'namespace.labelKey3',
'namespace.labelKey4',
'namespace.labelKey5',
'namespace.labelKey6.**',
]),
}))

describe('analyze', () => {
it('should extract all translations correctly from the file', () => {
vi.mocked(fs.readFileSync).mockReturnValue(fileContent)

const result = analyze({
filePath: mockFilePath,
scopedNames: mockScopedNames,
})

expect(fs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf-8')
expect(extractGlobalT).toHaveBeenCalledWith({ fileContent })
expect(extractNamespaceTranslation).toHaveBeenCalledWith({ fileContent })

expect(extractScopedTs).toHaveBeenCalledWith({
fileContent,
namespaceTranslation: 'namespace.labelKey1',
scopedName: 'scopedT',
})

expect(extractScopedTs).toHaveBeenCalledTimes(16)

expect(extractScopedTs).toHaveBeenNthCalledWith(5, {
fileContent,
namespaceTranslation: 'namespace.labelKey5',
scopedName: 'scopedT',
})

expect(result).toEqual(expect.arrayContaining(expectedTranslationResults))
expect(expectedTranslationResults).toEqual(expect.arrayContaining(result))
})
})
82 changes: 82 additions & 0 deletions packages/unused-i18n/src/lib/__tests__/remove.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as fs from 'fs'
import { describe, expect, it, vi } from 'vitest'
import { removeLocaleKeys } from '../remove'

vi.mock('fs')

describe('removeLocaleKeys', () => {
it('should remove specified locale keys from the file', () => {
const localePath = 'path/to/locale/en.js'
const missingTranslations = ['key1', 'key4', 'key2']

const fileContent = `export default {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
'key4':
'value4',
'key5': 'value5',
} as const`

const expectedContent = `
export default {
'key3': 'value3',
'key5': 'value5',
} as const`

const fsMock = {
readFileSync: vi.fn().mockReturnValue(fileContent),
writeFileSync: vi.fn(),
}

vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
vi.mocked(fs.writeFileSync).mockImplementation(fsMock.writeFileSync)

removeLocaleKeys({ localePath, missingTranslations })

expect(fs.readFileSync).toHaveBeenCalledWith(localePath, 'utf-8')
expect(fs.writeFileSync).toHaveBeenCalledWith(
localePath,
expectedContent.trim(),
'utf-8',
)
})
it('should remove specified locale keys from the file on multi line', () => {
const localePath = 'path/to/locale/en.js'
const missingTranslations = ['key1', 'key5']

const fileContent = `export default {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
'key4':
'value4',
'key5': 'value5',
} as const`

const expectedContent = `
export default {
'key2': 'value2',
'key3': 'value3',
'key4':
'value4',
} as const`

const fsMock = {
readFileSync: vi.fn().mockReturnValue(fileContent),
writeFileSync: vi.fn(),
}

vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
vi.mocked(fs.writeFileSync).mockImplementation(fsMock.writeFileSync)

removeLocaleKeys({ localePath, missingTranslations })

expect(fs.readFileSync).toHaveBeenCalledWith(localePath, 'utf-8')
expect(fs.writeFileSync).toHaveBeenCalledWith(
localePath,
expectedContent.trim(),
'utf-8',
)
})
})
60 changes: 60 additions & 0 deletions packages/unused-i18n/src/lib/__tests__/search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as fs from 'fs'
import * as path from 'path'
import { describe, expect, it, vi } from 'vitest'
import { searchFilesRecursively } from "../search"

vi.mock('fs')

describe('searchFilesRecursively', () => {
it('should find files where content matches the regex pattern', () => {
const baseDir = 'testDir'
const regex = /use-i18n/

const fsMock = {
readdirSync: vi.fn(dir => {
if (dir === baseDir) return ['file1.js', 'file2.js', 'subdir']
if (dir === path.join(baseDir, 'subdir')) return ['file3.js']

return []
}),
lstatSync: vi.fn(filePath => ({
isDirectory: () => filePath === path.join(baseDir, 'subdir'),
})),
readFileSync: vi.fn(filePath => {
if (filePath === path.join(baseDir, 'file1.js')) {
return `
import { useI18n } from '@scaleway/use-i18n'
`
}
if (filePath === path.join(baseDir, 'file2.js')) return 'no match here'
if (filePath === path.join(baseDir, 'subdir', 'file3.js')) {
return `
import { useI18n } from '@scaleway/use-i18n'
`
}

return ''
}),
}

vi.mocked(fs.readFileSync).mockImplementation(fsMock.readFileSync)
// @ts-expect-error mockImplementation no function
vi.mocked(fs.lstatSync).mockImplementation(fsMock.lstatSync)
// @ts-expect-error mockImplementation no function
vi.mocked(fs.readdirSync).mockImplementation(fsMock.readdirSync)

const expected = [
path.join(baseDir, 'file1.js'),
path.join(baseDir, 'subdir', 'file3.js'),
]

const result = searchFilesRecursively({
baseDir,
regex,
excludePatterns: [],
})

expect(result).toEqual(expect.arrayContaining(expected))
expect(expected).toEqual(expect.arrayContaining(result))
})
})
Loading

0 comments on commit 3478f63

Please sign in to comment.