auto-translatr
Version:
An automatic translation library
559 lines (530 loc) • 20.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const openaiClient = __importStar(require("../openaiClient"));
// Mock the OpenAI client to avoid real API calls
jest.mock('../openaiClient', () => ({
prompt: jest.fn()
}));
const mockedPrompt = openaiClient.prompt;
// Import after mocking
const index_1 = require("../index");
// Mock console methods
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
beforeEach(() => {
console.log = jest.fn();
console.warn = jest.fn();
console.error = jest.fn();
// Reset the mock before each test
mockedPrompt.mockReset();
});
afterEach(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
});
describe('Auto-Translate Integration Test', () => {
const testProjectDir = path.join(__dirname, 'mock-react-project');
const i18nDir = path.join(testProjectDir, 'i18n');
const settingsFile = 'auto-translate.settings.json';
beforeEach(() => {
// Clean up any existing test directory
if (fs.existsSync(testProjectDir)) {
fs.rmSync(testProjectDir, { recursive: true });
}
// Create mock React.js project structure
createMockReactProject();
// Mock OpenAI responses
setupOpenAIMocks();
});
afterEach(() => {
// Clean up test directory and translation files
if (fs.existsSync(testProjectDir)) {
fs.rmSync(testProjectDir, { recursive: true });
}
// Clean up any i18n files that might have been created in the current directory
const currentI18nDir = path.join(process.cwd(), 'i18n');
if (fs.existsSync(currentI18nDir)) {
fs.rmSync(currentI18nDir, { recursive: true });
}
// Clean up settings files
const currentSettingsFile = path.join(process.cwd(), settingsFile);
if (fs.existsSync(currentSettingsFile)) {
fs.rmSync(currentSettingsFile);
}
const testSettingsFile = path.join(testProjectDir, settingsFile);
if (fs.existsSync(testSettingsFile)) {
fs.rmSync(testSettingsFile);
}
});
const createMockReactProject = () => {
// Create directory structure
const dirs = [
'src/components',
'src/pages',
'src/utils',
'src/hooks',
'public'
];
dirs.forEach(dir => {
fs.mkdirSync(path.join(testProjectDir, dir), { recursive: true });
});
// Create realistic React component files with translation calls
const files = [
{
path: 'src/components/Header.tsx',
content: `import React from 'react'
import { useAuth } from '../hooks/useAuth'
export const Header: React.FC = () => {
const { user, logout } = useAuth()
return (
<header className="bg-blue-600 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">{t('My Awesome App')}</h1>
<nav>
<ul className="flex space-x-4">
<li><a href="/">{t('Home')}</a></li>
<li><a href="/about">{t('About')}</a></li>
<li><a href="/contact">{t('Contact')}</a></li>
{user ? (
<li>
<button onClick={logout}>{t('Sign Out')}</button>
</li>
) : (
<li>
<a href="/login">{t('Sign In')}</a>
</li>
)}
</ul>
</nav>
</div>
</header>
)
}`
},
{
path: 'src/components/Footer.jsx',
content: `import React from 'react'
export const Footer = () => {
return (
<footer className="bg-gray-800 text-white p-6 mt-8">
<div className="container mx-auto text-center">
<p>© 2024 {t('My Awesome App')}. {t('All rights reserved')}.</p>
<div className="mt-4">
<a href="/privacy" className="text-blue-400 hover:underline mr-4">
{t('Privacy Policy')}
</a>
<a href="/terms" className="text-blue-400 hover:underline">
{t('Terms of Service')}
</a>
</div>
</div>
</footer>
)
}`
},
{
path: 'src/pages/Home.js',
content: `import React, { useState, useEffect } from 'react'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
export default function Home() {
const [loading, setLoading] = useState(true)
const [data, setData] = useState(null)
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(t('Failed to fetch data'))
}
const result = await response.json()
setData(result)
} catch (error) {
console.error(t('Error loading data'), error)
alert(t('Something went wrong. Please try again.'))
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-lg">{t('Loading...')}</div>
</div>
)
}
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">{t('Welcome to our website')}</h1>
<p className="text-lg mb-4">
{t('Discover amazing features and services tailored just for you.')}
</p>
<button
onClick={() => alert(t('Feature coming soon!'))}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
{t('Get Started')}
</button>
</main>
<Footer />
</div>
)
}`
},
{
path: 'src/utils/validation.ts',
content: `export const validateEmail = (email: string): string | null => {
if (!email) {
return t('Email is required')
}
if (!email.includes('@')) {
return t('Please enter a valid email address')
}
return null
}
export const validatePassword = (password: string): string | null => {
if (!password) {
return t('Password is required')
}
if (password.length < 8) {
return t('Password must be at least 8 characters long')
}
return null
}
export const validateForm = (data: any): Record<string, string> => {
const errors: Record<string, string> = {}
if (!data.name) {
errors.name = t('Name is required')
}
const emailError = validateEmail(data.email)
if (emailError) {
errors.email = emailError
}
const passwordError = validatePassword(data.password)
if (passwordError) {
errors.password = passwordError
}
return errors
}`
},
{
path: 'src/hooks/useAuth.ts',
content: `import { useState, useEffect } from 'react'
export const useAuth = () => {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
checkAuthStatus()
}, [])
const checkAuthStatus = async () => {
try {
const response = await fetch('/api/auth/me')
if (response.ok) {
const userData = await response.json()
setUser(userData)
}
} catch (error) {
console.error(t('Authentication check failed'), error)
} finally {
setLoading(false)
}
}
const login = async (email: string, password: string) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (!response.ok) {
throw new Error(t('Invalid credentials'))
}
const userData = await response.json()
setUser(userData)
return { success: true }
} catch (error) {
return {
success: false,
error: error.message || t('Login failed. Please try again.')
}
}
}
const logout = () => {
setUser(null)
localStorage.removeItem('authToken')
alert(t('You have been signed out'))
}
return { user, loading, login, logout }
}`
},
{
path: 'package.json',
content: `{
"name": "mock-react-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}`
},
{
path: 'src/App.js',
content: `import React from 'react'
import Home from './pages/Home'
function App() {
return (
<div className="App">
<Home />
</div>
)
}
export default App`
}
];
// Write all files
files.forEach(file => {
const fullPath = path.join(testProjectDir, file.path);
fs.writeFileSync(fullPath, file.content);
});
};
const setupOpenAIMocks = () => {
// Mock translations for different languages
const translations = {
'fr': {
'My Awesome App': 'Mon Application Géniale',
'Home': 'Accueil',
'About': 'À Propos',
'Contact': 'Contact',
'Sign Out': 'Se Déconnecter',
'Sign In': 'Se Connecter',
'All rights reserved': 'Tous droits réservés',
'Privacy Policy': 'Politique de Confidentialité',
'Terms of Service': 'Conditions d\'Utilisation',
'Loading...': 'Chargement...',
'Welcome to our website': 'Bienvenue sur notre site web',
'Get Started': 'Commencer',
'Email is required': 'L\'email est requis',
'Password is required': 'Le mot de passe est requis',
'Name is required': 'Le nom est requis'
},
'de': {
'My Awesome App': 'Meine Tolle App',
'Home': 'Startseite',
'About': 'Über',
'Contact': 'Kontakt',
'Sign Out': 'Abmelden',
'Sign In': 'Anmelden',
'All rights reserved': 'Alle Rechte vorbehalten',
'Privacy Policy': 'Datenschutzrichtlinie',
'Terms of Service': 'Nutzungsbedingungen',
'Loading...': 'Wird geladen...',
'Welcome to our website': 'Willkommen auf unserer Website',
'Get Started': 'Loslegen',
'Email is required': 'E-Mail ist erforderlich',
'Password is required': 'Passwort ist erforderlich',
'Name is required': 'Name ist erforderlich'
},
'pt': {
'My Awesome App': 'Meu App Incrível',
'Home': 'Início',
'About': 'Sobre',
'Contact': 'Contato',
'Sign Out': 'Sair',
'Sign In': 'Entrar',
'All rights reserved': 'Todos os direitos reservados',
'Privacy Policy': 'Política de Privacidade',
'Terms of Service': 'Termos de Serviço',
'Loading...': 'Carregando...',
'Welcome to our website': 'Bem-vindo ao nosso site',
'Get Started': 'Começar',
'Email is required': 'E-mail é obrigatório',
'Password is required': 'Senha é obrigatória',
'Name is required': 'Nome é obrigatório'
}
};
mockedPrompt.mockImplementation((text) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
// Simulate translation based on the mock data
// In a real scenario, this would be the OpenAI API response
const currentLang = process.env.CURRENT_LANG || 'fr';
return ((_a = translations[currentLang]) === null || _a === void 0 ? void 0 : _a[text]) || `[${currentLang.toUpperCase()}] ${text}`;
}));
};
it('should run full auto-translate workflow on a React.js project', () => __awaiter(void 0, void 0, void 0, function* () {
// Change to the mock project directory
const originalCwd = process.cwd();
const originalEnv = process.env.OPENAI_API_KEY;
// Set a mock API key
process.env.OPENAI_API_KEY = 'mock-api-key';
try {
process.chdir(testProjectDir);
// Run the auto-translate workflow
yield (0, index_1.run)();
// Verify settings file was created
const settingsFilePath = path.join(testProjectDir, settingsFile);
expect(fs.existsSync(settingsFilePath)).toBe(true);
const settings = JSON.parse(fs.readFileSync(settingsFilePath, 'utf-8'));
expect(settings).toEqual({
default: 'en',
supported: ['en', 'fr', 'de'],
openai: {
model: 'gpt-4o-mini',
apiKey: '' // Should be empty in file for security
}
});
// Verify English translations file was created
const enFile = path.join(i18nDir, 'en.json');
expect(fs.existsSync(enFile)).toBe(true);
const enTranslations = JSON.parse(fs.readFileSync(enFile, 'utf-8'));
// Verify key translations are present with correct kebab-case keys
expect(enTranslations).toEqual(expect.objectContaining({
'my-awesome-app': 'My Awesome App',
'home': 'Home',
'about': 'About',
'contact': 'Contact',
'sign-out': 'Sign Out',
'sign-in': 'Sign In',
'all-rights-reserved': 'All rights reserved',
'privacy-policy': 'Privacy Policy',
'terms-of-service': 'Terms of Service',
'loading': 'Loading...',
'welcome-to-our-website': 'Welcome to our website',
'get-started': 'Get Started',
'email-is-required': 'Email is required',
'password-is-required': 'Password is required',
'name-is-required': 'Name is required'
}));
// Verify French translations file was created
process.env.CURRENT_LANG = 'fr';
const frFile = path.join(i18nDir, 'fr.json');
expect(fs.existsSync(frFile)).toBe(true);
// Verify German translations file was created
process.env.CURRENT_LANG = 'de';
const deFile = path.join(i18nDir, 'de.json');
expect(fs.existsSync(deFile)).toBe(true);
// Verify OpenAI API was called for translations
expect(mockedPrompt).toHaveBeenCalled();
// Verify success messages were logged
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully updated en.json'));
// Verify settings-related messages were logged
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Created auto-translate.settings.json with default settings'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Auto-translating from en to: fr, de'));
}
finally {
// Restore original state
process.chdir(originalCwd);
process.env.OPENAI_API_KEY = originalEnv;
delete process.env.CURRENT_LANG;
}
}), 30000); // 30 second timeout for integration test
it('should use existing settings file when available', () => __awaiter(void 0, void 0, void 0, function* () {
const originalCwd = process.cwd();
const originalEnv = process.env.OPENAI_API_KEY;
// Set a mock API key
process.env.OPENAI_API_KEY = 'mock-api-key';
try {
process.chdir(testProjectDir);
// Create a custom settings file
const customSettings = {
default: 'es',
supported: ['es', 'pt'],
openai: {
model: 'gpt-3.5-turbo',
apiKey: 'custom-key-from-file'
}
};
const settingsFilePath = path.join(testProjectDir, settingsFile);
fs.writeFileSync(settingsFilePath, JSON.stringify(customSettings, null, 2));
// Run the auto-translate workflow
yield (0, index_1.run)();
// Verify the existing settings file was used (not overwritten)
const savedSettings = JSON.parse(fs.readFileSync(settingsFilePath, 'utf-8'));
expect(savedSettings).toEqual(customSettings);
// Verify Spanish (es) file was created as the default language
const esFile = path.join(i18nDir, 'es.json');
expect(fs.existsSync(esFile)).toBe(true);
// Verify Portuguese (pt) file was created
const ptFile = path.join(i18nDir, 'pt.json');
expect(fs.existsSync(ptFile)).toBe(true);
// Verify settings loading message was logged
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Loaded settings from auto-translate.settings.json'));
// Verify the custom languages were used
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Auto-translating from es to: pt'));
}
finally {
process.chdir(originalCwd);
process.env.OPENAI_API_KEY = originalEnv;
}
}), 15000);
it('should handle missing OpenAI API key gracefully', () => __awaiter(void 0, void 0, void 0, function* () {
const originalCwd = process.cwd();
const originalEnv = process.env.OPENAI_API_KEY;
// Remove API key
delete process.env.OPENAI_API_KEY;
try {
process.chdir(testProjectDir);
// This should still work for parsing but may fail on translation
yield (0, index_1.run)();
// At minimum, the English file should be created
const enFile = path.join(i18nDir, 'en.json');
expect(fs.existsSync(enFile)).toBe(true);
}
finally {
process.chdir(originalCwd);
process.env.OPENAI_API_KEY = originalEnv;
}
}), 15000);
});