Skip to content

邮件AI翻译实现过程踩坑总结 #34

@p2227

Description

@p2227

邮件翻译系统技术实现文档

概述

本文档记录了邮件翻译系统的技术架构和关键问题解决方案,包括邮件内容展示、国际化翻译、性能优化和用户体验提升等方面。

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 流式翻译过程中,每次接收到新内容都会:

  1. 更新 React state
  2. 重新创建 Blob URL
  3. 重新渲染整个 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 架构优势

  1. 模块化设计:各功能模块独立,易于维护和扩展
  2. 性能优化:通过压缩、缓存和智能更新机制提升性能
  3. 用户体验:流畅的翻译过程,实时进度反馈
  4. 安全可靠:iframe 隔离,防范 XSS 攻击

6.3 未来优化方向

  1. 缓存机制:实现翻译结果缓存,避免重复翻译
  2. 批量处理:支持多邮件批量翻译
  3. 多语言支持:扩展更多目标语言
  4. 离线翻译:集成本地翻译模型

7. 问题记录与解决

7.1 iframe 内容更新问题

问题:使用 postMessage 更新 iframe 内容时,消息无法正确传递

解决方案

  1. 确保 HTML 结构在 script 标签之后,保证 document.body 可用
  2. 正确设置 MutationObserver,处理 DOM 未就绪的情况
  3. 使用唯一的 iframeId 进行消息路由

7.2 subject 显示 undefined 问题

问题:iframe 模板中使用了错误的变量作用域

解决方案

// 错误:使用了函数作用域外的变量
${isTranslated ? translatedContent : content}

// 正确:直接使用函数参数
${content}

7.3 翻译进度不准确问题

问题:SSE 进度回调不够准确,用户体验差

解决方案:实现双重进度机制

  • 真实进度:基于翻译 API 返回
  • 模拟进度:提供流畅的视觉反馈

本文档记录了邮件翻译系统的完整技术实现,为后续开发和维护提供参考。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions