Demo: Tradutor Inglês→Português com IA Local (Prompt API)

iniciante

Traduza textos do inglês para português brasileiro com streaming local e privacidade total. Inferência on-device via Prompt API — sem enviar dados para nuvem.

Verificando...

Visão Geral

Demo de tradução Inglês → Português Brasileiro executada 100% local no navegador. O texto traduzido aparece em streaming, sem enviar nada pra servidor nenhum. Um bom exemplo de como configurar expectedInputs e expectedOutputs com idiomas.

Pra quem: Desenvolvedores que querem entender a configuração de idiomas na Prompt API.

Técnica principal: promptStreaming() com expectedInputs: [{ type: "text", languages: ["en"] }] e expectedOutputs: [{ type: "text", languages: ["pt"] }].


Wireframe

┌─────────────────────────────────────────────────────────┐
│  🌐 Tradutor EN → PT                                    │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌────────────────────┐    ┌────────────────────┐      │
│  │ 🇺🇸 Inglês          │    │ 🇧🇷 Português       │      │
│  ├────────────────────┤    ├────────────────────┤      │
│  │                    │    │                    │      │
│  │ The quick brown    │ →  │ A rápida raposa    │      │
│  │ fox jumps over     │    │ marrom pula sobre  │      │
│  │ the lazy dog.      │    │ o cão preguiçoso.█ │      │
│  │                    │    │                    │      │
│  └────────────────────┘    └────────────────────┘      │
│                                                         │
│  [ 🌐 Traduzir ]    [ 📋 Copiar Tradução ]              │
│                                                         │
│  ⚠️ Limitações: modelo local compacto; ideal para       │
│     textos curtos e de uso geral.                       │
└─────────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="tradutor-local">
  <h2>🌐 Tradutor EN → PT</h2>

  <div id="status-bar" class="status" hidden>
    <span id="status-message"></span>
  </div>

  <div class="translator-grid">
    <div class="panel panel-source">
      <div class="panel-header">
        <span>🇺🇸 Inglês</span>
        <span id="char-count">0 / 3000</span>
      </div>
      <textarea
        id="input-text"
        placeholder="Type or paste English text here..."
        rows="8"
        maxlength="3000"
      ></textarea>
    </div>

    <div class="panel-arrow">→</div>

    <div class="panel panel-target">
      <div class="panel-header">
        <span>🇧🇷 Português</span>
      </div>
      <div id="output-text" class="output-area"></div>
    </div>
  </div>

  <div class="actions">
    <button id="btn-translate" disabled>🌐 Traduzir</button>
    <button id="btn-stop" hidden>⏹️ Parar</button>
    <button id="btn-copy" hidden>📋 Copiar Tradução</button>
  </div>

  <div class="limitations-note">
    <p>⚠️ <strong>Limitações:</strong> Modelo local compacto (Gemini Nano). Ideal para textos curtos e de uso geral. Para documentos técnicos longos ou termos especializados, use serviços dedicados de tradução.</p>
  </div>
</section>

Código JavaScript

class LocalTranslator {
  constructor() {
    this.session = null;
    this.controller = null;

    this.inputEl = document.getElementById("input-text");
    this.outputEl = document.getElementById("output-text");
    this.btnEl = document.getElementById("btn-translate");
    this.btnStop = document.getElementById("btn-stop");
    this.btnCopy = document.getElementById("btn-copy");
    this.statusBar = document.getElementById("status-bar");
    this.statusMessage = document.getElementById("status-message");
    this.charCount = document.getElementById("char-count");

    this.init();
  }

  async init() {
    if (!("LanguageModel" in window)) {
      this.showStatus("❌ Prompt API não disponível. Use Chrome 148+.", "error");
      return;
    }

    const availability = await LanguageModel.availability({
      expectedInputs: [{ type: "text", languages: ["en"] }],
      expectedOutputs: [{ type: "text", languages: ["pt"] }]
    });

    if (availability === "unavailable") {
      this.showStatus("❌ Modelo não disponível ou idioma não suportado.", "error");
      return;
    }

    try {
      this.session = await LanguageModel.create({
        expectedInputs: [{ type: "text", languages: ["en", "pt"] }],
        expectedOutputs: [{ type: "text", languages: ["pt"] }],
        initialPrompts: [{
          role: "system",
          content: `Você é um tradutor profissional inglês→português brasileiro. Traduza o texto fornecido de forma natural e fluente. Mantenha a formatação original (parágrafos, listas). Não adicione explicações — retorne apenas a tradução.`
        }],
        monitor(m) {
          m.addEventListener("downloadprogress", (e) => {
            document.getElementById("status-message").textContent =
              `⏳ Baixando modelo... ${Math.round(e.loaded * 100)}%`;
          });
        }
      });

      this.btnEl.disabled = false;
      this.hideStatus();
    } catch (err) {
      this.showStatus(`❌ Erro: ${err.message}`, "error");
      return;
    }

    this.btnEl.addEventListener("click", () => this.translate());
    this.btnStop.addEventListener("click", () => this.stop());
    this.btnCopy.addEventListener("click", () => this.copy());
    this.inputEl.addEventListener("input", () => this.updateCharCount());
    this.inputEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && e.ctrlKey) this.translate();
    });
  }

  async translate() {
    const text = this.inputEl.value.trim();
    if (!text) return;

    this.controller = new AbortController();
    this.btnEl.hidden = true;
    this.btnStop.hidden = false;
    this.btnCopy.hidden = true;
    this.outputEl.textContent = "";
    this.outputEl.classList.add("streaming");

    try {
      const stream = this.session.promptStreaming(
        `Traduza para português brasileiro:\n\n${text}`,
        { signal: this.controller.signal }
      );

      for await (const chunk of stream) {
        this.outputEl.textContent = chunk;
      }

      this.btnCopy.hidden = false;
    } catch (err) {
      if (err.name !== "AbortError") {
        this.showStatus(`❌ Erro: ${err.message}`, "error");
      }
    } finally {
      this.outputEl.classList.remove("streaming");
      this.btnEl.hidden = false;
      this.btnStop.hidden = true;
    }
  }

  stop() {
    this.controller?.abort();
  }

  copy() {
    navigator.clipboard.writeText(this.outputEl.textContent).then(() => {
      this.btnCopy.textContent = "✅ Copiado!";
      setTimeout(() => { this.btnCopy.textContent = "📋 Copiar Tradução"; }, 2000);
    });
  }

  updateCharCount() {
    const len = this.inputEl.value.length;
    this.charCount.textContent = `${len} / 3000`;
  }

  showStatus(msg, type) {
    this.statusBar.hidden = false;
    this.statusBar.className = `status status-${type}`;
    this.statusMessage.textContent = msg;
  }

  hideStatus() {
    this.statusBar.hidden = true;
  }
}

document.addEventListener("DOMContentLoaded", () => new LocalTranslator());

Fluxo UX

  1. Página carrega → verifica suporte para par de idiomas EN→PT
  2. Usuário digita/cola texto em inglês no painel esquerdo
  3. Clica “Traduzir” (ou Ctrl+Enter) → botão vira “Parar”
  4. Tradução em streaming aparece no painel direito com cursor piscante
  5. Streaming completo → cursor some, botão “Copiar” aparece
  6. Parar → cancela via AbortController; tradução parcial permanece

Edge Cases e Tratamento de Erros

CenárioTratamento
Idioma EN não suportado pelo modeloavailability() com config de idioma detecta antes
Texto já em portuguêsModelo pode retornar o próprio texto (system prompt instrui traduzir)
Texto misto (EN + PT)Modelo traduz partes em inglês, mantém português
Gírias/expressões idiomáticasModelo local pode não traduzir perfeitamente — nota de limitação visível
Texto muito longomaxlength="3000" limita; context window protege contra overflow
Rede indisponívelFunciona normalmente após modelo baixado
Caracteres especiaisPreservados na tradução

CSS Essencial

.translator-grid {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 1rem;
  align-items: stretch;
}

.panel-arrow {
  display: flex;
  align-items: center;
  font-size: 1.5rem;
  color: #6b7280;
}

.panel-header {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem;
  font-weight: 600;
  border-bottom: 1px solid #e5e7eb;
}

.output-area {
  min-height: 180px;
  padding: 0.75rem;
  white-space: pre-wrap;
  line-height: 1.6;
}

.streaming::after {
  content: "█";
  animation: blink 0.7s steps(1) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

.limitations-note {
  margin-top: 1rem;
  padding: 0.75rem;
  background: #fefce8;
  border: 1px solid #fde047;
  border-radius: 0.5rem;
  font-size: 0.875rem;
}

@media (max-width: 768px) {
  .translator-grid {
    grid-template-columns: 1fr;
  }
  .panel-arrow {
    justify-content: center;
    transform: rotate(90deg);
  }
}

Notas sobre Limitações

O Gemini Nano é um modelo compacto. Para tradução:

  • Funciona bem: textos curtos, linguagem cotidiana, conteúdo geral
  • Pode falhar: terminologia técnica especializada, jogos de palavras, nuances culturais
  • Idiomas suportados na Prompt API (2026): "en", "ja", "es" — e progressivamente mais
  • A nota de limitações é exibida permanentemente na UI para transparência com o usuário
  • Para necessidades profissionais de tradução, recomende a Translator API dedicada do Chrome