-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Labels
Description
邮件翻译系统技术实现文档
概述
本文档记录了邮件翻译系统的技术架构和关键问题解决方案,包括邮件内容展示、国际化翻译、性能优化和用户体验提升等方面。
1. 兼容邮件展示 - iframe 解决方案
1.1 技术背景
邮件内容通常包含复杂的 HTML 结构、内联 CSS 样式和多媒体元素,直接在 React 组件中渲染可能导致:
- CSS 样式冲突
- 安全风险(XSS 攻击)
- 布局破坏
1.2 解决方案
采用 iframe 隔离渲染邮件内容:
const createMailBlobUrl = useCallback(
(content: string, subject: string, isTranslated: boolean = false) => {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${subject}${isTranslated ? ' (翻译)' : ''}</title>
${mailContentStyle}
</head>
<body>
<div class="email-container">
<div class="email-subject">${subject}</div>
<div class="email-content">${content}</div>
</div>
</body>
</html>
`;
const blob = new Blob([htmlContent], { type: 'text/html' });
return URL.createObjectURL(blob);
},
[mailContentStyle],
);1.3 技术优势
- 样式隔离:iframe 提供独立的渲染环境
- 安全性:通过 sandbox 属性限制脚本执行
- 兼容性:支持各种邮件客户端的 HTML 格式
1.4 高度自适应
通过 postMessage 实现 iframe 高度动态调整:
// iframe内部脚本
window.addEventListener('load', function () {
const bodyHeight = document.body.scrollHeight;
const containerHeight = document.querySelector('.email-container').scrollHeight;
window.parent.postMessage(
{
type: 'iframe-height',
iframeId: iframeId,
height: Math.max(bodyHeight, containerHeight),
},
'*',
);
});2. 国际化邮件翻译 - API 集成
2.1 翻译架构
采用流式翻译 API,支持实时翻译进度展示:
// 翻译API调用
await translateStream(filteredContent, 'zh', {
onChunk: (content, translatedText) => {
// 实时更新翻译内容
const reassembledContent = reassembleTranslatedContent(
mdReplace(translatedText),
preservedBlocksRef.current,
);
updateTranslationIframeContent(reassembledContent);
},
onProgress: (progress) => {
setTranslationProgress(Math.round(progress * 100));
},
onComplete: (translatedText) => {
// 翻译完成处理
},
});2.2 内容过滤优化
翻译前过滤不需要翻译的 HTML 标签和内容:
export function filterContentForTranslation(content: string) {
const preservedBlocks: Array<{ placeholder: string; content: string; type: string }> = [];
// 过滤规则
const filterRules = [
{ type: 'style', regex: /<style[^>]*>[\s\S]*?<\/style>/gi },
{ type: 'script', regex: /<script[^>]*>[\s\S]*?<\/script>/gi },
{ type: 'meta', regex: /<meta[^>]*\/?>/gi },
{ type: 'link', regex: /<link[^>]*\/?>/gi },
// ... 更多过滤规则
];
let filteredContent = content;
filterRules.forEach((rule) => {
filteredContent = filteredContent.replace(rule.regex, (match) => {
const placeholder = `__PRESERVED_${rule.type.toUpperCase()}_${preservedBlocks.length}__`;
// 只有当占位符比原内容短时才替换
if (placeholder.length < match.length) {
preservedBlocks.push({
placeholder,
content: match,
type: rule.type,
});
return placeholder;
}
return match;
});
});
return { filteredContent, preservedBlocks };
}2.3 翻译队列管理
实现并发控制,避免 API 调用过载:
class TranslationQueue {
private queue: Array<QueueItem> = [];
private running = 0;
private maxConcurrent = 15; // 最大并发数
async add<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ resolve, reject, task });
this.processQueue();
});
}
private async processQueue() {
while (this.queue.length > 0 && this.running < this.maxConcurrent) {
const item = this.queue.shift();
this.running++;
item
.task()
.then(item.resolve)
.catch(item.reject)
.finally(() => {
this.running--;
this.processQueue();
});
}
}
}3. SSE 闪动优化 - postMessage 内部更新
3.1 问题背景
传统方案在 SSE 流式翻译过程中,每次接收到新内容都会:
- 更新 React state
- 重新创建 Blob URL
- 重新渲染整个 iframe
导致严重的页面闪动和性能问题。
3.2 解决方案
采用 postMessage 机制,在 iframe 内部直接更新 DOM:
外部组件
const updateTranslationIframeContent = useCallback(
(content: string, subject?: string) => {
if (!translationIframeRef.current?.contentWindow) return;
const message = {
type: 'update-content',
iframeId: translationIframeId.current,
content: content,
subject: subject || translatedSubject || '邮件内容',
timestamp: Date.now(),
};
translationIframeRef.current.contentWindow.postMessage(message, '*');
},
[translatedSubject],
);iframe 内部脚本
window.addEventListener('message', function (event) {
if (event.data && event.data.iframeId === iframeId) {
if (event.data.type === 'update-content') {
// 更新内容
const contentContainer = document.querySelector('.email-content');
if (contentContainer && event.data.content) {
contentContainer.innerHTML = event.data.content;
}
// 更新主题
if (event.data.subject) {
const subjectContainer = document.querySelector('.email-subject');
if (subjectContainer) {
subjectContainer.textContent = event.data.subject;
}
}
// 重新计算高度
setTimeout(function () {
const newHeight = Math.max(
document.body.scrollHeight,
document.querySelector('.email-container').scrollHeight,
);
window.parent.postMessage(
{
type: 'iframe-height',
iframeId: iframeId,
height: newHeight,
},
'*',
);
}, 50);
}
}
});3.3 技术优势
- 性能提升:避免频繁的 iframe 重新渲染
- 用户体验:消除翻译过程中的页面闪动
- 实时更新:支持流式翻译的实时展示
3.4 关键实现细节
DOM 结构顺序
<body>
<script>
// 脚本必须在DOM元素之前,确保document.body可用
</script>
<div class="email-container">
<!-- 邮件内容 -->
</div>
</body>MutationObserver 设置
function setupMutationObserver() {
if (document.body) {
const observer = new MutationObserver(function () {
// 监听DOM变化,自动调整高度
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
return true;
}
return false;
}
// 确保DOM准备就绪
if (!setupMutationObserver()) {
document.addEventListener('DOMContentLoaded', setupMutationObserver);
}4. Token 节约 - html-minifier-terser 压缩
4.1 优化背景
邮件 HTML 内容通常包含大量冗余信息:
- 空白字符和换行
- 冗余的 HTML 属性
- 未压缩的 CSS 样式
- 注释和元数据
这些冗余内容会增加翻译 API 的 token 消耗和响应时间。
4.2 压缩配置
const minifyOptions = {
// 基础压缩选项
collapseWhitespace: true, // 折叠空白字符
removeComments: false, // 保留注释(可能包含重要信息)
removeEmptyAttributes: true, // 移除空属性
removeRedundantAttributes: true, // 移除冗余属性
useShortDoctype: true, // 使用短DOCTYPE
// 保持邮件内容完整性
keepClosingSlash: true, // 保持自闭合标签
caseSensitive: false, // 不区分大小写
conservativeCollapse: true, // 保守的空白字符折叠
// 高级压缩选项
minifyCSS: true, // 压缩CSS
minifyJS: false, // 不压缩JS(避免破坏功能)
removeAttributeQuotes: false, // 保留属性引号
// 邮件特定优化
sortAttributes: true, // 排序属性
sortClassName: true, // 排序class名称
};4.3 压缩实现
async function performTranslation(text: string, res: NextApiResponse) {
try {
// HTML压缩处理
let compressedText: string;
try {
compressedText = await minify(text, minifyOptions);
const compressionRatio = (
((text.length - compressedText.length) / text.length) *
100
).toFixed(1);
console.log(
`HTML压缩完成: ${text.length} → ${compressedText.length} 字符 (压缩${compressionRatio}%)`,
);
} catch (minifyError) {
console.warn('HTML压缩失败,使用原始文本:', minifyError);
compressedText = text; // 压缩失败时使用原始文本
}
// 使用压缩后的文本进行翻译
const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.ZHIPU_KEY}`,
},
body: JSON.stringify({
model: 'GLM-4-Flash-250414',
messages: [
{
role: 'system',
content: `你是一个前端专家兼翻译高手,输入的HTML已经经过压缩优化...`,
},
{
role: 'user',
content: compressedText, // 使用压缩后的文本
},
],
stream: true,
}),
});
// ... 处理流式响应
} catch (error) {
console.error('翻译失败:', error);
}
}4.4 优化效果
- Token 节约:平均压缩率 30-50%,显著降低 API 成本
- 响应速度:减少网络传输时间,提升翻译速度
- 兼容性:保持邮件内容的完整性和可读性
5. 用户体验优化
5.1 加载状态管理
实现优雅的翻译进度展示:
const TranslationLoadingOverlay = ({ isVisible, progress, height }) => {
if (!isVisible) return null;
return (
<div className="absolute inset-0 bg-black/5 z-10 flex items-center justify-center">
<div className="bg-white/95 backdrop-blur-sm rounded-xl shadow-lg px-6 py-5">
<div className="flex flex-col items-center space-y-4">
<div className="flex items-center space-x-3">
<Loader2 className="w-5 h-5 animate-spin text-blue-500" />
<span className="text-base font-medium">正在翻译</span>
</div>
<div className="w-56 bg-gray-100 rounded-full h-1.5">
<div
className="bg-gradient-to-r from-blue-500 to-blue-600 h-1.5 rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
<div className="text-xs text-gray-600">{progress}% 完成</div>
</div>
</div>
</div>
);
};5.2 模拟进度优化
为了提供更好的用户体验,实现了智能进度模拟:
const startSimulatedProgress = useCallback(() => {
let progress = 0;
progressIntervalRef.current = setInterval(() => {
progress += Math.random() * 2 + 0.5; // 每次增加0.5-2.5%
// 模拟进度最多到85%,避免超过实际进度
const maxProgress = translationProgress > 0 ? Math.min(translationProgress - 5, 95) : 85;
progress = Math.min(progress, maxProgress);
setSimulatedProgress(Math.round(progress));
// 接近最大值时减慢速度
if (progress > maxProgress - 10) {
clearInterval(progressIntervalRef.current);
// 每500ms缓慢增长
progressIntervalRef.current = setInterval(() => {
progress += Math.random() * 0.5;
progress = Math.min(progress, maxProgress);
setSimulatedProgress(Math.round(progress));
}, 500);
}
}, 100);
}, [translationProgress]);6. 技术总结
6.1 关键技术栈
- 前端框架:React + TypeScript
- 样式方案:TailwindCSS + 内联样式
- 通信机制:postMessage API
- 压缩工具:html-minifier-terser
- 翻译 API:智谱 AI GLM-4-Flash 模型
6.2 架构优势
- 模块化设计:各功能模块独立,易于维护和扩展
- 性能优化:通过压缩、缓存和智能更新机制提升性能
- 用户体验:流畅的翻译过程,实时进度反馈
- 安全可靠:iframe 隔离,防范 XSS 攻击
6.3 未来优化方向
- 缓存机制:实现翻译结果缓存,避免重复翻译
- 批量处理:支持多邮件批量翻译
- 多语言支持:扩展更多目标语言
- 离线翻译:集成本地翻译模型
7. 问题记录与解决
7.1 iframe 内容更新问题
问题:使用 postMessage 更新 iframe 内容时,消息无法正确传递
解决方案:
- 确保 HTML 结构在 script 标签之后,保证 document.body 可用
- 正确设置 MutationObserver,处理 DOM 未就绪的情况
- 使用唯一的 iframeId 进行消息路由
7.2 subject 显示 undefined 问题
问题:iframe 模板中使用了错误的变量作用域
解决方案:
// 错误:使用了函数作用域外的变量
${isTranslated ? translatedContent : content}
// 正确:直接使用函数参数
${content}7.3 翻译进度不准确问题
问题:SSE 进度回调不够准确,用户体验差
解决方案:实现双重进度机制
- 真实进度:基于翻译 API 返回
- 模拟进度:提供流畅的视觉反馈
本文档记录了邮件翻译系统的完整技术实现,为后续开发和维护提供参考。