@logistically/i18n-cli
Version:
Enterprise-grade CLI tool for extracting and managing translations in Logistically microservices
438 lines (367 loc) • 14.8 kB
text/typescript
import { TranslationExtractor } from '../utils/extractor';
import { TranslationGenerator } from '../utils/generator';
import { TranslationReplacer } from '../utils/replacer';
import * as fs from 'fs-extra';
import { glob } from 'glob';
// Mock dependencies
jest.mock('fs-extra');
jest.mock('glob', () => ({
glob: jest.fn()
}));
const mockFs = fs as jest.Mocked<typeof fs>;
const mockGlob = glob as jest.MockedFunction<typeof glob>;
// Mock file stats for size filtering
const mockFileStats = {
size: 1024, // 1KB file
isFile: () => true,
isDirectory: () => false
};
describe('Translation CLI Integration', () => {
let extractor: TranslationExtractor;
let generator: TranslationGenerator;
let replacer: TranslationReplacer;
beforeEach(() => {
extractor = new TranslationExtractor();
generator = new TranslationGenerator();
replacer = new TranslationReplacer();
jest.clearAllMocks();
// Mock file stats for size filtering
(mockFs.stat as any).mockResolvedValue(mockFileStats);
mockFs.existsSync.mockReturnValue(true);
});
describe('Full workflow: extract -> generate -> replace', () => {
it('should complete full translation workflow successfully', async () => {
// Step 1: Extract translation keys
const mockFiles = ['auth/src/services/auth.service.ts', 'profile/src/controllers/profile.controller.ts'];
const mockContent1 = `
throw new BadRequestException('User already exists with this email');
return { message: 'Validation failed' };
errors.push(\`The field '\${fieldName}' is required\`);
`;
const mockContent2 = `
throw new NotFoundException('Profile not found');
return { message: 'Profile updated successfully' };
`;
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any)
.mockResolvedValueOnce(mockContent1)
.mockResolvedValueOnce(mockContent2);
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false,
validate: false,
maxFileSize: 100
});
expect(extractedKeys).toHaveLength(7); // Updated for new patterns
expect(extractedKeys[0].text).toBe('User already exists with this email');
expect(extractedKeys[1].text).toBe('Validation failed');
expect(extractedKeys[2].text).toBe("The field '${fieldName}' is required");
expect(extractedKeys[3].text).toBe('Validation failed'); // MESSAGE_PROPERTY pattern
expect(extractedKeys[4].text).toBe('Profile not found');
expect(extractedKeys[5].text).toBe('Profile updated successfully');
expect(extractedKeys[6].text).toBe('Profile updated successfully'); // MESSAGE_PROPERTY pattern
// Step 2: Generate translation files
const mockKeysData = {
keys: extractedKeys,
totalKeys: extractedKeys.length,
services: ['auth', 'profile'],
generatedAt: new Date().toISOString()
};
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en', 'fr'],
template: true,
verbose: false
});
expect(mockFs.ensureDir).toHaveBeenCalledWith('auth/src/translations');
expect(mockFs.ensureDir).toHaveBeenCalledWith('profile/src/translations');
expect(mockFs.writeFile).toHaveBeenCalledTimes(6); // 2 services × 2 languages + 2 README files
// Step 3: Replace hardcoded strings
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.readFile as any)
.mockResolvedValueOnce(mockContent1)
.mockResolvedValueOnce(mockContent2);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: false,
verbose: false
});
expect(mockFs.readJson).toHaveBeenCalledWith('translation-keys.json');
expect(mockFs.readFile).toHaveBeenCalledWith('auth/src/services/auth.service.ts', 'utf8');
expect(mockFs.readFile).toHaveBeenCalledWith('profile/src/controllers/profile.controller.ts', 'utf8');
});
it('should handle multiple file types and patterns', async () => {
// Step 1: Extract from multiple file types
const mockFiles = [
'auth/src/services/auth.service.ts',
'profile/src/controllers/profile.controller.ts',
'location/src/services/location.service.ts'
];
const mockContent1 = `
throw new BadRequestException('User already exists with this email');
return { message: 'Validation failed' };
errors.push(\`The field '\${fieldName}' is required\`);
`;
const mockContent2 = `
throw new NotFoundException('Profile not found');
errors.push(\`The field '\${fieldName}' is required\`);
`;
const mockContent3 = `
throw new Exception('Cannot create location under parent \${createLocationDto.parentId}');
return { message: 'Location created successfully' };
`;
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any)
.mockResolvedValueOnce(mockContent1)
.mockResolvedValueOnce(mockContent2)
.mockResolvedValueOnce(mockContent3);
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false,
validate: false,
maxFileSize: 100
});
expect(extractedKeys.length).toBeGreaterThan(0);
// Step 2: Generate for multiple services
const mockKeysData = {
keys: extractedKeys,
totalKeys: extractedKeys.length,
services: ['auth', 'profile', 'location'],
generatedAt: new Date().toISOString()
};
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en', 'fr', 'de'],
template: true,
verbose: false
});
expect(mockFs.ensureDir).toHaveBeenCalledWith('auth/src/translations');
expect(mockFs.ensureDir).toHaveBeenCalledWith('profile/src/translations');
expect(mockFs.ensureDir).toHaveBeenCalledWith('location/src/translations');
// Step 3: Replace in all files
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.readFile as any)
.mockResolvedValueOnce(mockContent1)
.mockResolvedValueOnce(mockContent2)
.mockResolvedValueOnce(mockContent3);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: false,
verbose: false
});
expect(mockFs.readFile).toHaveBeenCalledWith('auth/src/services/auth.service.ts', 'utf8');
expect(mockFs.readFile).toHaveBeenCalledWith('profile/src/controllers/profile.controller.ts', 'utf8');
expect(mockFs.readFile).toHaveBeenCalledWith('location/src/services/location.service.ts', 'utf8');
});
it('should handle errors gracefully in workflow', async () => {
// Clear mocks to ensure clean state
jest.clearAllMocks();
// Step 1: Extract with file reading error
const mockFiles = ['auth/src/services/auth.service.ts'];
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any).mockImplementation(() => {
throw new Error('File not found');
});
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false
});
expect(extractedKeys).toHaveLength(0);
// Step 2: Generate with empty keys
const mockKeysData = {
keys: [],
totalKeys: 0,
services: [],
generatedAt: new Date().toISOString()
};
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en'],
template: true,
verbose: false
});
// Should not create any files with empty keys
expect(mockFs.writeFile).toHaveBeenCalledTimes(0);
// Step 3: Replace with no files to process
mockFs.readJson.mockResolvedValue(mockKeysData);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: false,
verbose: false
});
// Should process files but handle errors gracefully
expect(mockFs.readFile).toHaveBeenCalledWith('auth/src/services/auth.service.ts', 'utf8');
});
it('should handle dry run mode in workflow', async () => {
// Clear mocks to ensure clean state
jest.clearAllMocks();
// Step 1: Extract normally
const mockFiles = ['auth/src/services/auth.service.ts'];
const mockContent = `throw new BadRequestException('User already exists with this email');`;
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any).mockResolvedValue(mockContent);
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false,
validate: false,
maxFileSize: 100
});
expect(extractedKeys).toHaveLength(1);
// Step 2: Generate normally
const mockKeysData = {
keys: extractedKeys,
totalKeys: extractedKeys.length,
services: ['auth'],
generatedAt: new Date().toISOString()
};
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en'],
template: true,
verbose: false
});
expect(mockFs.writeFile).toHaveBeenCalledTimes(2); // en.json + README.md
// Step 3: Replace in dry run mode
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.readFile as any).mockResolvedValue(mockContent);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: true,
verbose: false
});
// Should not write any files in dry run mode
expect(mockFs.writeFile).not.toHaveBeenCalledWith(
'auth/src/services/auth.service.ts',
expect.anything()
);
});
it('should handle backup creation in workflow', async () => {
// Step 1: Extract
const mockFiles = ['auth/src/services/auth.service.ts'];
const mockContent = `throw new BadRequestException('User already exists with this email');`;
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any).mockResolvedValue(mockContent);
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false
});
// Step 2: Generate
const mockKeysData = {
keys: extractedKeys,
totalKeys: extractedKeys.length,
services: ['auth'],
generatedAt: new Date().toISOString()
};
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en'],
template: true,
verbose: false
});
// Step 3: Replace with backup enabled
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.readFile as any).mockResolvedValue(mockContent);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: false,
verbose: false
});
// Should create backup file
expect(mockFs.writeFile).toHaveBeenCalledWith(
'auth/src/services/auth.service.ts.backup',
mockContent
);
});
it('should handle complex nested patterns in workflow', async () => {
// Step 1: Extract complex patterns
const mockFiles = ['auth/src/services/auth.service.ts'];
const mockContent = `
if (error) {
throw new BadRequestException('User already exists with this email');
}
return {
success: true,
message: 'Validation failed'
};
`;
mockGlob.mockResolvedValue(mockFiles);
(mockFs.readFile as any).mockResolvedValue(mockContent);
const extractedKeys = await extractor.extract({
path: '.',
ignore: ['**/node_modules/**'],
verbose: false,
validate: false,
maxFileSize: 100
});
expect(extractedKeys).toHaveLength(3); // Updated for new patterns
expect(extractedKeys[0].text).toBe('User already exists with this email');
expect(extractedKeys[1].text).toBe('Validation failed');
expect(extractedKeys[2].text).toBe('Validation failed'); // MESSAGE_PROPERTY pattern
// Step 2: Generate
const mockKeysData = {
keys: extractedKeys,
totalKeys: extractedKeys.length,
services: ['auth'],
generatedAt: new Date().toISOString()
};
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await generator.generate({
input: 'translation-keys.json',
languages: ['en'],
template: true,
verbose: false
});
// Step 3: Replace complex patterns
mockFs.readJson.mockResolvedValue(mockKeysData);
(mockFs.readFile as any).mockResolvedValue(mockContent);
(mockFs.writeFile as any).mockResolvedValue(undefined);
await replacer.replace({
path: '.',
keysFile: 'translation-keys.json',
backup: true,
dryRun: false,
verbose: false
});
// Find the call that writes the replaced content (not backup or README)
const writeFileCalls = (mockFs.writeFile as any).mock.calls;
const replacedContentCall = writeFileCalls.find((call: any) =>
call[0] === 'auth/src/services/auth.service.ts' &&
call[1] !== mockContent
);
expect(replacedContentCall[1]).toContain('this.translate(');
});
});
});