“从从容容 游刃有余”🤖利用浏览器内置 AI 实现国际化翻译

17 小时 12 分钟前
 xiaowoli

🌐 浏览器内置翻译 API 完全指南

探索 Chrome 最新的 Translator API ,实现离线、高效、隐私友好的本地翻译功能

💡 本文基于 Chrome 131 Canary 版本编写,API 可能会随版本更新而变化。


第一次使用可能需要下载模型 🚀点击查看在线演示 →

一、前言:为什么需要浏览器内置翻译?

1. 传统翻译方案的痛点

在 Web 开发中,我们通常使用以下翻译方案:

2. Translator API 的优势

Chrome 推出的 Translator API 是浏览器内置的本地翻译解决方案,带来了革命性的改变:

特性 Translator API 云端翻译 静态 i18n
离线支持 ✅ 完全离线 ❌ 需要网络 ✅ 离线可用
隐私保护 ✅ 数据不出浏览器 ❌ 数据传输到服务器 ✅ 无数据传输
动态翻译 ✅ 实时翻译任意文本 ✅ 实时翻译 ❌ 仅预定义文本
响应速度 ⚡ 极快(本地计算) 🐌 受网络影响 ⚡ 极快
成本 💰 免费 💸 API 调用收费 💰 免费
维护成本 🔧 低 🔧 低 🔨 高(多语言文件)

3. 浏览器支持情况

目前 Translator API 正处于实验性阶段,支持情况如下:

💡 提示:虽然目前是实验性功能,但 Chrome 团队正在积极推进标准化,预计未来将成为 Web 标准的一部分。


二、Translator API 官方文档解读

1. API 基本介绍

Translator API 是基于浏览器内置的神经网络翻译模型,能够在本地完成高质量的文本翻译。它是 Chrome AI 计划的一部分,与 Prompt API 、Summarizer API 等共同构成浏览器端 AI 能力。

2. 核心方法详解

2.1 检测 API 可用性

// 检查浏览器是否支持 Translator API
if (!('Translator' in self)) {
  console.error('当前浏览器不支持 Translator API');
}

// 检查特定语言对的可用性
const availability = await self.Translator.availability({
  sourceLanguage: 'en',
  targetLanguage: 'zh'
});

console.log(availability);
// 可能的返回值:
// - "unavailable"  : 用户的设备或所请求的会话选项不受支持。设备可能电量不足或磁盘空间不足
// - "downloadable" : 需要进行额外的下载才能创建会话。可能需要用户激活才能调用 create()
// - "downloading"  : 下载正在进行中,必须先完成下载,然后才能使用会话
// - "available"    : 您可以立即创建会话

返回值说明

参考官方文档 - Model Download

2.2 创建翻译器实例

const translator = await self.Translator.create({
  sourceLanguage: 'en',  // 源语言( ISO 639-1 代码)
  targetLanguage: 'zh'   // 目标语言( ISO 639-1 代码)
});

参数说明

2.3 执行翻译

const result = await translator.translate('Hello, world!');
console.log(result); // "你好,世界!"

3. 语言支持列表

使用 BCP 47 语言短代码作为字符串。例如,'es' 表示西班牙语,'fr' 表示法语。

目前支持的主流语言(不完全列表):

语言 ISO 代码 语言 ISO 代码
中文(简体) zhzh-CN 英语 en
日语 ja 韩语 ko
法语 fr 德语 de
西班牙语 es 俄语 ru
意大利语 it 葡萄牙语 pt
阿拉伯语 ar 印地语 hi

注意:具体支持的语言对可能因 Chrome 版本而异,建议使用前通过 availability() 检测。

4. 与传统翻译方案对比

场景一:翻译用户输入内容

传统方案(云端 API )

// 需要调用外部 API
const response = await fetch('https://api.translate.com/v1/translate', {
  method: 'POST',
  body: JSON.stringify({ text, from: 'en', to: 'zh' })
});
const result = await response.json();

❌ 问题:

本地 AI 方案

const translator = await self.Translator.create({
  sourceLanguage: 'en',
  targetLanguage: 'zh'
});
const result = await translator.translate(text);

✅ 优势:

场景二:整页国际化

传统方案( i18n 文件)

// en.json
{ "welcome": "Welcome", "description": "A translation demo" }

// zh.json
{ "welcome": "欢迎", "description": "翻译演示" }

// 使用
document.getElementById('title').textContent = i18n.t('welcome');

❌ 问题:

本地 AI 方案(动态翻译)

// 直接翻译页面上的所有文本
async function translatePage(targetLang) {
  const translator = await self.Translator.create({
    sourceLanguage: 'zh',
    targetLanguage: targetLang
  });
  
  const elements = document.querySelectorAll('[data-i18n]');
  for (const el of elements) {
    el.textContent = await translator.translate(el.textContent);
  }
}

✅ 优势:


三、实战:Demo 实现步骤详解

基于我开发的 Demo (translator-demo.html),我将详细介绍如何从零构建一个完整的翻译应用。

环境准备

浏览器版本要求

  1. 下载 Chrome Canary 或 Dev 版本

  2. 启用实验性功能

打开以下两个 Chrome flags:

chrome://flags/#translation-api
chrome://flags/#optimization-guide-on-device-model

设置为 Enabled,然后重启浏览器。

  1. 首次使用注意事项
    • 首次调用会自动下载语言模型(约 50-200MB )
    • 下载时间取决于网络速度(通常 2-5 分钟)
    • 模型下载后会缓存,后续使用无需重新下载

功能检测代码

在应用启动时,首先检测 API 是否可用:

async function checkAPIAvailability() {
  try {
    // 1. 检查浏览器是否支持 Translator API
    if (!('Translator' in self)) {
      apiStatus.className = "status-banner error";
      apiStatus.innerHTML = `
        <span>❌</span>
        <span>您的浏览器不支持 Translator API 。请使用 Chrome 131+ 并启用相关实验性功能。</span>
      `;
      translateBtn.disabled = true;
      return;
    }

    // 2. 检查特定语言对是否可用 - 使用 availability() 方法
    const translatorCapabilities = await self.Translator.availability({
      sourceLanguage: 'zh',
      targetLanguage: 'en',
    });
    
    if (translatorCapabilities === "unavailable") {
      apiStatus.className = "status-banner error";
      apiStatus.innerHTML = `
        <span>❌</span>
        <span>Translator API 不可用</span>
      `;
      translateBtn.disabled = true;
    } else {
      apiStatus.className = "status-banner success";
      apiStatus.innerHTML = `
        <span>✅</span>
        <span>Translator API 可用!可以开始使用翻译功能。</span>
      `;
      translateBtn.disabled = false;

      if (translatorCapabilities === "downloadable" || translatorCapabilities === "downloading") {
        apiStatus.innerHTML += `<div style="margin-top: 8px; font-size: 0.9em;">📦 正在下载翻译模型...</div>`;
      }
    }
  } catch (error) {
    apiStatus.className = "status-banner error";
    apiStatus.innerHTML = `
      <span>❌</span>
      <span>Translator API 不可用: ${error.message}</span>
    `;
    translateBtn.disabled = true;
  }
}

核心功能实现

功能一:文本翻译

这是最基础的功能,用户输入文本后点击按钮进行翻译。

步骤 1:HTML 结构
<div class="translate-panel">
  <!-- 源语言选择 -->
  <select id="sourceLang">
    <option value="zh">🇨🇳 中文</option>
    <option value="en">🇺🇸 英语</option>
    <option value="ja">🇯🇵 日语</option>
  </select>

  <!-- 输入框 -->
  <textarea id="inputText" placeholder="请输入要翻译的文本..."></textarea>

  <!-- 目标语言选择 -->
  <select id="targetLang">
    <option value="en">🇺🇸 英语</option>
    <option value="zh">🇨🇳 中文</option>
    <option value="ja">🇯🇵 日语</option>
  </select>

  <!-- 输出框 -->
  <textarea id="outputText" disabled placeholder="翻译结果..."></textarea>

  <!-- 翻译按钮 -->
  <button id="translateBtn" onclick="translateText()">🚀 开始翻译</button>
</div>
步骤 2:JavaScript 实现
async function translateText() {
  const text = document.getElementById('inputText').value.trim();
  const source = document.getElementById('sourceLang').value;
  const target = document.getElementById('targetLang').value;
  const outputText = document.getElementById('outputText');
  const translateBtn = document.getElementById('translateBtn');

  // 输入验证
  if (!text) {
    alert('请输入要翻译的文本');
    return;
  }

  if (source === target) {
    alert('源语言和目标语言相同,无需翻译');
    return;
  }

  try {
    // 禁用按钮,防止重复点击
    translateBtn.disabled = true;
    translateBtn.textContent = '⏳ 翻译中...';
    outputText.value = '';

    // 创建翻译器
    const translator = await self.Translator.create({
      sourceLanguage: source,
      targetLanguage: target
    });

    // 执行翻译
    const result = await translator.translate(text);

    // 显示结果
    outputText.value = result;

    // 可选:销毁翻译器释放资源
    // translator.destroy();

  } catch (error) {
    console.error('翻译失败:', error);
    alert(`翻译失败: ${error.message}`);
    outputText.value = '';
  } finally {
    // 恢复按钮状态
    translateBtn.disabled = false;
    translateBtn.textContent = '🚀 开始翻译';
  }
}
关键要点
  1. 异步处理:所有 API 调用都是异步的,必须使用 async/await
  2. 错误处理:使用 try-catch 捕获翻译失败的情况
  3. 用户体验
    • 翻译时禁用按钮,防止重复点击
    • 显示"翻译中"状态
    • 使用 finally 确保按钮状态恢复

功能二:交换语言

允许用户一键交换源语言和目标语言,并同时交换输入输出文本。

function swapLanguages() {
  const sourceLang = document.getElementById('sourceLang');
  const targetLang = document.getElementById('targetLang');
  const inputText = document.getElementById('inputText');
  const outputText = document.getElementById('outputText');

  // 交换语言选择
  const tempLang = sourceLang.value;
  sourceLang.value = targetLang.value;
  targetLang.value = tempLang;

  // 交换文本内容
  const tempText = inputText.value;
  inputText.value = outputText.value;
  outputText.value = '';  // 清空输出,等待新的翻译
}

功能三:清空文本

function clearText() {
  document.getElementById('inputText').value = '';
  document.getElementById('outputText').value = '';
}

高级功能实现

功能一:整页翻译(自动国际化)

这是 Demo 的核心亮点之一 - 使用 Translator API 实现整个页面的自动翻译。

实现思路
  1. 为需要翻译的元素添加 data-i18n 属性
  2. 存储原始中文文本
  3. 点击语言切换按钮时,调用 API 翻译所有文本
  4. 动态更新页面内容
HTML 标记
<h1 data-i18n="true">🌐 Chrome 内置 AI 翻译器</h1>
<p data-i18n="true">使用浏览器内置的 Translator API 进行实时翻译</p>
<button data-i18n="true">🚀 开始翻译</button>
<textarea placeholder="请输入文本..." data-i18n="true"></textarea>

重要说明

JavaScript 实现
// 当前页面语言
let currentLang = "zh";

// 存储元素的原始文本(用于恢复)
const originalTexts = new Map();

// 翻译器缓存(避免重复创建)
const pageTranslators = {};

/**
 * 切换页面语言
 * @param {string} targetLang - 目标语言代码
 */
async function switchLanguage(targetLang) {
  // 更新按钮状态 - 添加加载动画
  const targetBtn = document.querySelector(`.lang-btn[data-lang="${targetLang}"]`);
  document.querySelectorAll(".lang-btn").forEach((btn) => {
    btn.classList.remove("active", "translating");
  });
  
  targetBtn.classList.add("active", "translating");

  try {
    // 翻译页面内容
    await translatePage(currentLang, targetLang);
    // 更新当前语言
    currentLang = targetLang;
  } catch (error) {
    console.error('切换语言失败:', error);
    alert(`切换语言失败: ${error.message}`);
  } finally {
    // 移除加载动画
    targetBtn.classList.remove("translating");
  }
}

/**
 * 翻译页面中所有标记为 data-i18n="true" 的元素
 * @param {string} sourceLang - 源语言
 * @param {string} targetLang - 目标语言
 */
async function translatePage(sourceLang, targetLang) {
  // 如果源语言和目标语言相同,无需翻译
  if (sourceLang === targetLang) {
    return;
  }

  try {
    console.log(`🌐 开始翻译页面: ${sourceLang} → ${targetLang}`);

    // 获取或创建翻译器
    const translatorKey = `${sourceLang}-${targetLang}`;
    if (!pageTranslators[translatorKey]) {
      console.log(`🔧 创建翻译器: ${sourceLang} → ${targetLang}`);
      pageTranslators[translatorKey] = await self.Translator.create({
        sourceLanguage: sourceLang,
        targetLanguage: targetLang
      });
    }

    const translator = pageTranslators[translatorKey];

    // 收集所有需要翻译的元素
    const elements = document.querySelectorAll('[data-i18n="true"]');
    
    if (elements.length === 0) {
      console.log('⚠️ 没有找到需要翻译的元素 (data-i18n="true")');
      return;
    }

    console.log(`📝 找到 ${elements.length} 个需要翻译的元素`);

    // 遍历每个元素进行翻译
    for (const el of elements) {
      // 跳过特定元素(如翻译功能区的输入框)
      if (el.id === 'inputText' || el.id === 'outputText') {
        continue;
      }

      // 添加翻译中样式
      el.classList.add('translating-text');

      try {
        // 保存原始文本(首次翻译时)
        if (!originalTexts.has(el)) {
          if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
            originalTexts.set(el, {
              placeholder: el.placeholder,
              value: el.value
            });
          } else {
            // 提取纯文本内容(排除子元素)
            const textContent = getTextContent(el);
            originalTexts.set(el, {
              textContent: textContent
            });
          }
        }

        // 获取当前文本内容
        let textToTranslate = '';
        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
          textToTranslate = el.placeholder;
        } else {
          textToTranslate = getTextContent(el);
        }

        // 执行翻译
        if (textToTranslate) {
          const translated = await translator.translate(textToTranslate);
          
          // 更新元素内容
          if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
            el.placeholder = translated;
          } else {
            // 只替换文本节点,保留子元素
            replaceTextContent(el, translated);
          }
        }

      } catch (error) {
        console.error(`翻译元素失败:`, el, error);
      } finally {
        // 移除翻译中样式
        el.classList.remove('translating-text');
      }
    }

    console.log(`✅ 页面翻译完成!`);

  } catch (error) {
    console.error('❌ 页面翻译失败:', error);
    // 移除所有 loading 类
    document.querySelectorAll('.translating-text').forEach(el => {
      el.classList.remove('translating-text');
    });
    throw error;
  }
}

/**
 * 提取元素的纯文本内容(只包含直接文本节点,不包含子元素)
 */
function getTextContent(el) {
  let text = '';
  for (const node of el.childNodes) {
    if (node.nodeType === Node.TEXT_NODE) {
      text += node.textContent;
    }
  }
  return text.trim();
}

/**
 * 替换元素的文本节点内容,保留子元素
 */
function replaceTextContent(el, newText) {
  // 如果元素没有子元素,直接替换 textContent
  if (el.children.length === 0) {
    el.textContent = newText;
    return;
  }

  // 如果有子元素(如链接),只替换文本节点
  for (const node of el.childNodes) {
    if (node.nodeType === Node.TEXT_NODE) {
      node.textContent = newText;
      break; // 只替换第一个文本节点
    }
  }
}

四、总结

📌 核心要点回顾

  1. Translator API 的优势

    • ✅ 完全离线,保护用户隐私
    • ✅ 本地计算,响应速度快
    • ✅ 免费使用,无调用限制
    • ✅ 动态翻译,无需维护多语言文件
  2. 实现整页翻译的关键步骤

    • 使用 data-i18n="true" 标记可翻译元素
    • 缓存翻译器实例,避免重复创建
    • 保存原始文本,支持语言切换
    • 正确处理不同类型元素( input 、textarea 、普通元素)
    • 保留 HTML 结构,只翻译文本节点
  3. API 核心方法

    // 检查可用性
    const availability = await self.Translator.availability({ sourceLanguage, targetLanguage });
    
    // 创建翻译器
    const translator = await self.Translator.create({ sourceLanguage, targetLanguage });
    
    // 执行翻译
    const result = await translator.translate(text);
    
  4. 当前限制

    • 仅 Chrome 131+ 支持(实验性功能)
    • 需要手动启用两个 Chrome flags
    • 首次使用需要下载语言模型
    • 不支持批量翻译 API

🚀 应用场景

🔮 未来展望

Chrome 团队正在积极推进浏览器内置 AI 能力的标准化,未来可能会:


🙏 致谢

感谢 Chrome 团队为 Web 开发者带来了如此强大的本地 AI 能力!

684 次点击
所在节点    分享发现
10 条回复
ohoh
16 小时 22 分钟前
你是个好人。
收藏了
SurgaOrange
16 小时 16 分钟前
等一个本地离线的 AI 翻译插件
uhohoo
16 小时 15 分钟前
好东西
june4
15 小时 8 分钟前
真的好快,这底层用的啥模型,可以扣出来在外面用吗
xiaowoli
15 小时 8 分钟前
@SurgaOrange GET 后面我实现一下
miraku
14 小时 18 分钟前

整页翻译有点慢
```javascript
// ==UserScript==
// @name 网页翻译 + 划词翻译气泡( Translator API )
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 使用浏览器内置 Translator API 翻译网页或选中文本,并缓存语言设置(默认 英→中)
// @author mirakyux
// @match *://*/*
// @grant none
// @run-at document_idle
// ==/UserScript==

(async function() {
'use strict';

// ======== 配置与缓存 ========
const cacheKey = 'translator_langs';
const saved = JSON.parse(localStorage.getItem(cacheKey) || '{}');
let sourceLang = saved.sourceLang || 'en';
let targetLang = saved.targetLang || 'zh';

function saveLang() {
localStorage.setItem(cacheKey, JSON.stringify({ sourceLang, targetLang }));
}

// ======== 样式 ========
function style(el, css) {
Object.assign(el.style, css);
}

// ======== 悬浮按钮(整页) ========
const pageBtn = document.createElement('button');
pageBtn.textContent = '🌐 翻译网页';
style(pageBtn, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 9999,
padding: '10px 16px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
fontSize: '14px',
opacity: '0.85',
transition: 'opacity 0.3s'
});
pageBtn.onmouseenter = () => (pageBtn.style.opacity = '1');
pageBtn.onmouseleave = () => (pageBtn.style.opacity = '0.85');
document.body.appendChild(pageBtn);

// ======== 检测 Translator API ========
async function isTranslatorAvailable() {
if (!('Translator' in self)) {
console.error('❌ 当前浏览器不支持 Translator API 。请启用 chrome://flags/#translation-api');
return false;
}
try {
const avail = await self.Translator.availability({
sourceLanguage: sourceLang,
targetLanguage: targetLang
});
if (avail === 'unavailable') {
console.error(`❌ 不支持语言对 ${sourceLang} → ${targetLang}`);
return false;
}
console.log(`✅ Translator API 可用 (${avail})`);
return true;
} catch (err) {
console.error('❌ Translator API 检测失败:', err);
return false;
}
}

async function createTranslator() {
return await self.Translator.create({
sourceLanguage: sourceLang,
targetLanguage: targetLang
});
}

// ======== 整页翻译逻辑 ========
async function translatePage() {
const available = await isTranslatorAvailable();
if (!available) {
alert('当前浏览器不支持内置翻译 API ,请查看控制台提示。');
return;
}
pageBtn.disabled = true;
pageBtn.textContent = '翻译中…';
const translator = await createTranslator();

const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
if (!node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
if (node.parentElement && ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentElement.tagName))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});

const textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);

for (const node of textNodes) {
const text = node.nodeValue.trim();
if (!text || text.length > 2000) continue;
try {
const translated = await translator.translate(text);
node.nodeValue = translated;
} catch (err) {
console.warn('跳过节点:', err);
}
}

pageBtn.textContent = '✅ 已翻译';
setTimeout(() => {
pageBtn.textContent = '🌐 翻译网页';
pageBtn.disabled = false;
}, 3000);
}

pageBtn.addEventListener('click', translatePage);

// ======== 划词翻译气泡 ========
const bubble = document.createElement('div');
style(bubble, {
position: 'absolute',
background: '#007bff',
color: '#fff',
padding: '6px 10px',
borderRadius: '6px',
fontSize: '13px',
cursor: 'pointer',
display: 'none',
zIndex: 99999,
userSelect: 'none',
whiteSpace: 'nowrap',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)'
});
bubble.textContent = '翻译';
document.body.appendChild(bubble);

let currentSelection = '';
document.addEventListener('selectionchange', () => {
const sel = window.getSelection();
const text = sel.toString().trim();
if (text.length > 0) {
const range = sel.getRangeAt(0);
const rect = range.getBoundingClientRect();
bubble.style.left = `${rect.right + window.scrollX + 10}px`;
bubble.style.top = `${rect.top + window.scrollY - 10}px`;
bubble.style.display = 'block';
currentSelection = text;
} else {
bubble.style.display = 'none';
}
});

bubble.addEventListener('click', async () => {
if (!currentSelection) return;
const available = await isTranslatorAvailable();
if (!available) return alert('当前浏览器不支持 Translator API');
const translator = await createTranslator();
bubble.textContent = '翻译中…';
try {
const translated = await translator.translate(currentSelection);
alert(` [${sourceLang} → ${targetLang}] \n\n${translated}`);
} catch (e) {
alert('翻译失败:' + e.message);
} finally {
bubble.textContent = '翻译';
bubble.style.display = 'none';
}
});

// ======== 设置按钮 ========
const configBtn = document.createElement('button');
configBtn.textContent = '⚙️ 设置';
style(configBtn, {
position: 'fixed',
bottom: '60px',
right: '20px',
zIndex: 9999,
padding: '8px 12px',
backgroundColor: '#28a745',
color: '#fff',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '13px',
opacity: '0.85'
});
configBtn.onmouseenter = () => (configBtn.style.opacity = '1');
configBtn.onmouseleave = () => (configBtn.style.opacity = '0.85');
document.body.appendChild(configBtn);

configBtn.addEventListener('click', async () => {
const src = prompt('源语言(如 en, zh, ja ):', sourceLang);
const tgt = prompt('目标语言(如 zh, en, ko ):', targetLang);
if (src && tgt) {
sourceLang = src;
targetLang = tgt;
saveLang();
alert(`语言设置已保存:${sourceLang} → ${targetLang}`);
}
});
})();

```
miraku
14 小时 12 分钟前
newtype0092
13 小时 46 分钟前
文章读到最后总感觉缺点什么,想了想发现是没有 References ...
junkk
13 小时 12 分钟前
试了下,并没有想象中的快,相比其他的应该也算可以了
xiaowoli
9 小时 42 分钟前

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1167770

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX