/** * 多語言翻譯 E2E 測試 * * 測試涵蓋: * - PDFMathTranslate (pdf2zh) * - BabelDOC * - 支援語言:中文(簡繁)、英文、日文、韓文、德文、法文等 * * 注意:這些測試需要: * 1. 網路連接(使用翻譯 API) * 2. 設置翻譯服務 API 金鑰 * 3. PDF 測試檔案 * * 執行方式: * bun test tests/e2e/translation.e2e.test.ts */ import { describe, test, expect, beforeAll, afterAll } from "bun:test"; import { existsSync, mkdirSync, writeFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; // ============================================================================= // 配置 // ============================================================================= const E2E_OUTPUT_DIR = "tests/e2e/output/translation"; const E2E_FIXTURES_DIR = "tests/e2e/fixtures"; const TIMEOUT = 300_000; // 5 分鐘(翻譯可能很慢) // ============================================================================= // 語言定義 // ============================================================================= interface Language { code: string; name: string; nativeName: string; testPhrase: string; } const SUPPORTED_LANGUAGES: Language[] = [ { code: "zh", name: "Simplified Chinese", nativeName: "简体中文", testPhrase: "这是一个测试" }, { code: "zh-TW", name: "Traditional Chinese", nativeName: "繁體中文", testPhrase: "這是一個測試", }, { code: "en", name: "English", nativeName: "English", testPhrase: "This is a test" }, { code: "ja", name: "Japanese", nativeName: "日本語", testPhrase: "これはテストです" }, { code: "ko", name: "Korean", nativeName: "한국어", testPhrase: "이것은 테스트입니다" }, { code: "de", name: "German", nativeName: "Deutsch", testPhrase: "Das ist ein Test" }, { code: "fr", name: "French", nativeName: "Français", testPhrase: "C'est un test" }, { code: "es", name: "Spanish", nativeName: "Español", testPhrase: "Esta es una prueba" }, { code: "ru", name: "Russian", nativeName: "Русский", testPhrase: "Это тест" }, { code: "pt", name: "Portuguese", nativeName: "Português", testPhrase: "Isso é um teste" }, { code: "it", name: "Italian", nativeName: "Italiano", testPhrase: "Questo è un test" }, { code: "ar", name: "Arabic", nativeName: "العربية", testPhrase: "هذا اختبار" }, { code: "th", name: "Thai", nativeName: "ไทย", testPhrase: "นี่คือการทดสอบ" }, { code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt", testPhrase: "Đây là một bài kiểm tra", }, ]; // ============================================================================= // 工具檢測 // ============================================================================= interface TranslatorStatus { name: string; available: boolean; version?: string; } function checkTranslator(command: string): TranslatorStatus { try { const result = spawnSync(command, ["--version"], { timeout: 10000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); return { name: command, available: result.status === 0, version: result.stdout?.split("\n")[0]?.trim(), }; } catch { return { name: command, available: false }; } } // ============================================================================= // 測試 Fixture 生成 // ============================================================================= function ensureDir(dir: string): void { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } /** * 使用 LaTeX 創建一個簡單的 PDF 測試檔案 * 如果 xelatex 不可用,則創建一個基本的文字 PDF */ async function createTestPdf(path: string): Promise { const texContent = `\\documentclass{article} \\usepackage{fontspec} \\usepackage{xeCJK} \\setCJKmainfont{Noto Sans CJK SC} \\begin{document} \\section{Introduction} This is a test document for translation testing. The quick brown fox jumps over the lazy dog. \\section{Technical Content} Machine learning is a subset of artificial intelligence. Neural networks are inspired by the human brain. \\subsection{Mathematical Formulas} $E = mc^2$ $\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$ \\section{Conclusion} This document is used for testing multilingual translation capabilities. \\end{document} `; const texPath = path.replace(".pdf", ".tex"); writeFileSync(texPath, texContent); try { // 嘗試使用 xelatex const result = spawnSync( "xelatex", ["-interaction=nonstopmode", "-output-directory=" + join(path, ".."), texPath], { timeout: 60000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }, ); return result.status === 0 && existsSync(path); } catch { // 如果 xelatex 不可用,創建一個簡單的文字檔案作為替代 // 注意:這不是真正的 PDF,但可以用於基本測試 console.log(" ⚠️ xelatex 不可用,使用替代方法"); return false; } } // ============================================================================= // 測試結果追蹤 // ============================================================================= interface TranslationResult { translator: string; sourceLang: string; targetLang: string; success: boolean; duration: number; inputSize: number; outputSize: number; error?: string; } interface TestStats { total: number; passed: number; failed: number; skipped: number; results: TranslationResult[]; } const stats: TestStats = { total: 0, passed: 0, failed: 0, skipped: 0, results: [], }; // ============================================================================= // 測試套件 // ============================================================================= let translators: Record = {}; let testPdfPath: string; beforeAll(async () => { console.log("\n🔧 初始化翻譯測試... Initializing translation tests...\n"); ensureDir(E2E_OUTPUT_DIR); // 檢測翻譯工具 console.log(" 檢測翻譯工具..."); translators = { pdf2zh: checkTranslator("pdf2zh"), babeldoc: checkTranslator("babeldoc"), }; for (const [name, status] of Object.entries(translators)) { const icon = status.available ? "✅" : "❌"; console.log( ` ${icon} ${name}: ${status.available ? status.version || "available" : "not found"}`, ); } // 檢查或創建測試 PDF testPdfPath = join(E2E_FIXTURES_DIR, "sample.pdf"); if (!existsSync(testPdfPath)) { console.log("\n 📄 創建測試 PDF..."); const created = await createTestPdf(testPdfPath); if (!created) { console.log(" ⚠️ 無法創建測試 PDF,某些測試將被跳過"); } } else { console.log(`\n ✅ 使用現有測試 PDF: ${testPdfPath}`); } console.log(); }); afterAll(() => { console.log("\n" + "=".repeat(70)); console.log("📊 多語言翻譯測試摘要 Multilingual Translation Test Summary"); console.log("=".repeat(70)); console.log(`總測試數 Total: ${stats.total}`); console.log(`✅ 通過 Passed: ${stats.passed}`); console.log(`❌ 失敗 Failed: ${stats.failed}`); console.log(`⏭ 跳過 Skipped: ${stats.skipped}`); console.log( `成功率 Success Rate: ${((stats.passed / (stats.total - stats.skipped || 1)) * 100).toFixed(1)}%`, ); console.log("=".repeat(70)); // 生成報告 const reportPath = join(E2E_OUTPUT_DIR, "translation-report.json"); writeFileSync( reportPath, JSON.stringify( { generatedAt: new Date().toISOString(), translators, stats, }, null, 2, ), ); console.log(`📁 報告已保存: ${reportPath}`); }); // ============================================================================= // PDFMathTranslate 測試 // ============================================================================= describe("📚 PDFMathTranslate (pdf2zh)", () => { // 主要語言測試 describe("主要語言翻譯 Primary Languages", () => { const primaryLanguages = SUPPORTED_LANGUAGES.filter((l) => ["zh", "en", "ja", "ko"].includes(l.code), ); for (const lang of primaryLanguages) { test( `英文 → ${lang.nativeName} (${lang.code})`, async () => { stats.total++; if (!translators.pdf2zh?.available) { stats.skipped++; console.log(` ⏭ 跳過: pdf2zh 不可用`); return; } if (!existsSync(testPdfPath)) { stats.skipped++; console.log(` ⏭ 跳過: 測試 PDF 不存在`); return; } const outputPath = join(E2E_OUTPUT_DIR, `pdf2zh_en_to_${lang.code}.tar`); const startTime = Date.now(); const inputSize = statSync(testPdfPath).size; try { // 動態導入轉換器 const module = await import("../../src/converters/pdfmathtranslate"); // 設置目標語言(透過環境變數或參數) process.env.PDF2ZH_TARGET_LANG = lang.code; await module.convert(testPdfPath, "pdf", "tar", outputPath); const duration = Date.now() - startTime; const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; const success = outputSize > 0; stats.results.push({ translator: "pdf2zh", sourceLang: "en", targetLang: lang.code, success, duration, inputSize, outputSize, }); if (success) { stats.passed++; console.log( ` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`, ); } else { stats.failed++; console.log(` ✗ en → ${lang.code}: 輸出為空`); } expect(success).toBe(true); } catch (error) { const duration = Date.now() - startTime; stats.failed++; stats.results.push({ translator: "pdf2zh", sourceLang: "en", targetLang: lang.code, success: false, duration, inputSize, outputSize: 0, error: String(error), }); console.log(` ✗ en → ${lang.code}: ${error}`); throw error; } }, TIMEOUT, ); } }); // 次要語言測試(可選) describe("次要語言翻譯 Secondary Languages", () => { const secondaryLanguages = SUPPORTED_LANGUAGES.filter((l) => ["de", "fr", "es", "ru"].includes(l.code), ); for (const lang of secondaryLanguages) { test.skip(`英文 → ${lang.nativeName} (${lang.code})`, async () => { // 這些測試預設跳過,可以手動啟用 stats.total++; stats.skipped++; console.log(` ⏭ 跳過: 次要語言測試已禁用`); }); } }); }); // ============================================================================= // BabelDOC 測試 // ============================================================================= describe("🌐 BabelDOC", () => { describe("PDF 翻譯", () => { const testLanguages = SUPPORTED_LANGUAGES.filter((l) => ["zh", "ja"].includes(l.code)); for (const lang of testLanguages) { test( `英文 → ${lang.nativeName} (${lang.code})`, async () => { stats.total++; if (!translators.babeldoc?.available) { stats.skipped++; console.log(` ⏭ 跳過: babeldoc 不可用`); return; } if (!existsSync(testPdfPath)) { stats.skipped++; console.log(` ⏭ 跳過: 測試 PDF 不存在`); return; } const outputPath = join(E2E_OUTPUT_DIR, `babeldoc_en_to_${lang.code}.tar`); const startTime = Date.now(); const inputSize = statSync(testPdfPath).size; try { const module = await import("../../src/converters/babeldoc"); // 設置目標語言 process.env.BABELDOC_TARGET_LANG = lang.code; await module.convert(testPdfPath, "pdf", "tar", outputPath); const duration = Date.now() - startTime; const outputSize = existsSync(outputPath) ? statSync(outputPath).size : 0; const success = outputSize > 0; stats.results.push({ translator: "babeldoc", sourceLang: "en", targetLang: lang.code, success, duration, inputSize, outputSize, }); if (success) { stats.passed++; console.log( ` ✓ en → ${lang.code}: ${outputSize} bytes (${(duration / 1000).toFixed(1)}s)`, ); } else { stats.failed++; console.log(` ✗ en → ${lang.code}: 輸出為空`); } expect(success).toBe(true); } catch (error) { const duration = Date.now() - startTime; stats.failed++; stats.results.push({ translator: "babeldoc", sourceLang: "en", targetLang: lang.code, success: false, duration, inputSize, outputSize: 0, error: String(error), }); console.log(` ✗ en → ${lang.code}: ${error}`); throw error; } }, TIMEOUT, ); } }); }); // ============================================================================= // 語言矩陣測試 // ============================================================================= describe("🔤 語言矩陣測試 Language Matrix", () => { test("生成支援語言矩陣", () => { console.log("\n📋 支援的語言 Supported Languages:\n"); console.log("| 代碼 | 名稱 | 本地名稱 | 測試短語 |"); console.log("|------|------|----------|----------|"); for (const lang of SUPPORTED_LANGUAGES) { console.log( `| ${lang.code.padEnd(6)} | ${lang.name.padEnd(20)} | ${lang.nativeName} | ${lang.testPhrase} |`, ); } // 生成語言矩陣報告 const matrixPath = join(E2E_OUTPUT_DIR, "language-matrix.json"); writeFileSync( matrixPath, JSON.stringify( { languages: SUPPORTED_LANGUAGES, translators: Object.keys(translators), generatedAt: new Date().toISOString(), }, null, 2, ), ); console.log(`\n📁 語言矩陣已保存: ${matrixPath}`); expect(SUPPORTED_LANGUAGES.length).toBeGreaterThan(0); }); }); // ============================================================================= // 翻譯品質測試(概念性) // ============================================================================= describe("📊 翻譯品質指標 Translation Quality Metrics", () => { test.skip("驗證翻譯輸出完整性", async () => { // 這個測試會檢查翻譯輸出是否包含所有預期的頁面 // 需要實際的翻譯輸出來執行 }); test.skip("驗證數學公式保留", async () => { // 檢查數學公式是否正確保留在翻譯後的文檔中 }); test.skip("驗證格式保持", async () => { // 檢查翻譯後的文檔格式是否與原始文檔一致 }); });