Skip to content
This repository was archived by the owner on May 19, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/scripts/file-system.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import fs from 'fs'

const FileSystem = {
readdir: (path, callback) => fs.readdir(path, callback),
require: path => require(path),
writeFileSync: (path, value) => fs.writeFileSync(path, value),
}

export default FileSystem
78 changes: 78 additions & 0 deletions lib/scripts/file-system.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import fs from 'fs'
import os from 'os'
import path from 'path'

import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'

import FileSystem from './file-system'

describe('FileSystem', () => {
let tempDir

beforeAll(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'file-system-test'))
})

afterAll(() => {
fs.rmdirSync(tempDir, { recursive: true })
})

describe('readdir', () => {
test('should read files in a directory', done => {
const testFilePath1 = path.join(tempDir, 'file1.json')
const testFilePath2 = path.join(tempDir, 'file2.json')

fs.writeFileSync(testFilePath1, '{"hello": "world"}')
fs.writeFileSync(testFilePath2, '{"foo": "bar"}')

FileSystem.readdir(tempDir, (err, files) => {
expect(err).toBeNull()
expect(files).toContain('file1.json')
expect(files).toContain('file2.json')
done()
})
})

test('should return an error if the directory does not exist', done => {
const nonExistentPath = path.join(tempDir, 'non-existent-dir')

FileSystem.readdir(nonExistentPath, (err, files) => {
expect(err).toBeDefined()
expect(files).toBeUndefined()
done()
})
})
})

describe('require', () => {
test('should require a module correctly', () => {
const modulePath = path.resolve(__dirname, './file-system')
const result = FileSystem.require(modulePath)
expect(result).toBeDefined()
})

test('should throw an error if the module does not exist', () => {
const modulePath = path.resolve(__dirname, './non-existent-module')
expect(() => FileSystem.require(modulePath)).toThrow()
})
})

describe('writeFileSync', () => {
test('should write content to a file', () => {
const testFilePath = path.join(tempDir, 'writeTest.txt')
const content = 'Hello, this is a test'

FileSystem.writeFileSync(testFilePath, content)

const fileContent = fs.readFileSync(testFilePath, 'utf-8')
expect(fileContent).toBe(content)
})

test('should throw an error if the file cannot be written', () => {
const testFilePath = path.join(tempDir, 'non-existent-dir', 'writeTest.txt')
const content = 'Hello, this is a test'

expect(() => FileSystem.writeFileSync(testFilePath, content)).toThrow()
})
})
})
25 changes: 18 additions & 7 deletions lib/scripts/sort-translations.js → lib/scripts/i18n.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,50 @@
/* eslint-disable no-console */
import fs from 'fs'
import path from 'path'

import { argv } from 'yargs'

import FileSystem from './file-system'
import { sortKeys } from './sort-keys'
import { addMissingKeys } from './missing-keys'

const { fix } = argv

let status = 0

const localesRoot = path.join(process.cwd(), 'src/locales')

fs.readdir(localesRoot, (err, files) => {
function transform(object, base, isBase) {
if (isBase) {
return sortKeys(object)
} else {
return sortKeys(addMissingKeys(base, object))
}
}

FileSystem.readdir(localesRoot, (err, files) => {
if (err) {
return console.error("🔴 Can't read locales directory", localesRoot)
}

const baseFile = FileSystem.require(`${localesRoot}/en-US.json`)

files.forEach(filename => {
// Only treat .json files
if (path.extname(filename) !== '.json') {
return
}

try {
const filepath = `${localesRoot}/${filename}`
const translations = require(filepath)
const sorted = sortKeys(translations)

const hasChanged = JSON.stringify(translations) !== JSON.stringify(sorted)
const translations = FileSystem.require(filepath)
const output = transform(translations, baseFile, filename.endsWith('en-US.json'))
const hasChanged = JSON.stringify(translations) !== JSON.stringify(output)

// If fix, rewrite the file and show good / updated notification
// Otherwise, show good / error notification
if (fix) {
if (hasChanged) {
fs.writeFileSync(filepath, JSON.stringify(sorted, 0, 2))
FileSystem.writeFileSync(filepath, JSON.stringify(output, 0, 2))
console.info(`🔵 ${filename} updated`)
} else {
console.info(`🟢 ${filename} good`)
Expand Down
170 changes: 170 additions & 0 deletions lib/scripts/i18n.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* eslint-disable no-console */
import path from 'path'

import { beforeEach, describe, expect, jest, test } from '@jest/globals'

import FileSystem from './file-system'

jest.mock('./file-system')

describe('i18n', () => {
const localesRoot = path.join(process.cwd(), 'src/locales')
let processExitSpy

beforeEach(() => {
processExitSpy = jest.spyOn(process, 'exit').mockImplementation(code => code)
})

afterEach(() => {
processExitSpy.mockRestore()
jest.clearAllMocks()
})

test("should log an error if the locales directory can't be read", () => {
jest.isolateModules(() => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})

FileSystem.readdir.mockImplementation((_, callback) => {
callback(new Error('Cannot read directory'))
})

require('./i18n')

expect(consoleErrorSpy).toHaveBeenCalledWith("🔴 Can't read locales directory", localesRoot)
consoleErrorSpy.mockRestore()
})
})

test('should process JSON files and log good status if they are already sorted', () => {
jest.isolateModules(() => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {})

FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['file1.json', 'file2.json'])
})
FileSystem.require.mockReturnValue({ a: 1, b: 2 })

require('./i18n')

expect(consoleInfoSpy).toHaveBeenCalledWith('🟢 file1.json good')
expect(consoleInfoSpy).toHaveBeenCalledWith('🟢 file2.json good')
consoleInfoSpy.mockRestore()
})
})

test('should log an error if JSON files are out-of-sync with base translations and --fix is not set', () => {
jest.isolateModules(() => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {})
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['file1.json', 'en-US.json'])
})
FileSystem.require.mockImplementation(filepath => {
if (filepath.endsWith('en-US.json')) {
return { a: 1, b: 2, c: 3 }
}
return { b: 2, a: 1 }
})

require('./i18n')

expect(consoleInfoSpy).toHaveBeenCalledWith('🟢 en-US.json good')
expect(consoleErrorSpy).toHaveBeenCalledWith(
'🔴 file1.json not ordered correctly. Run this script again with `--fix` to resolve the issue.'
)
consoleErrorSpy.mockRestore()
})
})

test('should log an error if JSON files are not ordered and --fix is not set', () => {
jest.isolateModules(() => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['file1.json'])
})
FileSystem.require.mockReturnValue({ b: 2, a: 1 })

require('./i18n')

expect(consoleErrorSpy).toHaveBeenCalledWith(
'🔴 file1.json not ordered correctly. Run this script again with `--fix` to resolve the issue.'
)
consoleErrorSpy.mockRestore()
})
})

test('should update JSON files if they are not ordered and --fix is set', () => {
jest.isolateModules(() => {
const originalProcessArgv = process.argv
process.argv.push('--fix')
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {})

FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['file1.json'])
})
FileSystem.require.mockReturnValue({ b: 2, a: 1 })

require('./i18n')

expect(FileSystem.writeFileSync).toHaveBeenCalledWith(
`${localesRoot}/file1.json`,
JSON.stringify({ a: 1, b: 2 }, 0, 2)
)
expect(console.info).toHaveBeenCalledWith('🔵 file1.json updated')
consoleInfoSpy.mockRestore()
process.argv = originalProcessArgv
})
})

test('should update JSON files if they are out-of-sync with base translations and --fix is set', () => {
jest.isolateModules(() => {
const originalProcessArgv = process.argv
process.argv.push('--fix')
const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(() => {})
FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['fr-FR.json', 'en-US.json'])
})
FileSystem.require.mockImplementation(filepath => {
if (filepath.endsWith('en-US.json')) {
return { goodbye: 'goodbye', thanks: 'thanks', hello: 'hello' }
}
return { thanks: 'merci', hello: 'bonjour' }
})

require('./i18n')

expect(FileSystem.writeFileSync).toHaveBeenNthCalledWith(
1,
`${localesRoot}/fr-FR.json`,
JSON.stringify({ goodbye: 'goodbye', hello: 'bonjour', thanks: 'merci' }, 0, 2)
)
expect(FileSystem.writeFileSync).toHaveBeenNthCalledWith(
2,
`${localesRoot}/en-US.json`,
JSON.stringify({ goodbye: 'goodbye', hello: 'hello', thanks: 'thanks' }, 0, 2)
)

expect(console.info).toHaveBeenNthCalledWith(1, '🔵 fr-FR.json updated')
expect(console.info).toHaveBeenNthCalledWith(2, '🔵 en-US.json updated')
consoleErrorSpy.mockRestore()
process.argv = originalProcessArgv
})
})

test('should ignore non-JSON files', () => {
jest.isolateModules(() => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {})

FileSystem.readdir.mockImplementation((_, callback) => {
callback(null, ['file1.txt', 'file2.json'])
})
FileSystem.require.mockReturnValue({ a: 1, b: 2 })

require('./i18n')

expect(consoleInfoSpy).toHaveBeenCalledWith('🟢 file2.json good')
expect(consoleInfoSpy).not.toHaveBeenCalledWith('🟢 file1.txt good')
consoleInfoSpy.mockRestore()
})
})
})
15 changes: 15 additions & 0 deletions lib/scripts/missing-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function addMissingKeys(a, b) {
Object.keys(a).forEach(key => {
if (typeof a[key] === 'object' && a[key] !== null) {
if (!b[key] || typeof b[key] !== 'object') {
b[key] = {}
}
addMissingKeys(a[key], b[key])
} else {
if (!(key in b)) {
b[key] = a[key]
}
}
})
return b
}
Loading