Build a Translation App in Vue with a Translator API

Build a Vue 3 translator app with the APIVerve Translator API: working fetch code, auto-detection, a key-safe proxy, and a full GitHub repo. Start free.

  • 8 min read
  • Published March 12, 2026
  • Updated September 13, 2026
Article featured image

You’re going to build a translator app in Vue 3: pick two languages, paste some text, get a translation back from the Translator API. Then you’ll move the API key behind a tiny server, because shipping it in browser code is how keys get stolen.

The finished code lives at github.com/apiverve/translator-app-vue-tutorial. Clone it if you’d rather read than type.

The translation API call

Before touching Vue, make sure the request works. One POST, three fields in the body:

curl -X POST https://api.apiverve.com/v1/translator \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "I'\''m so excited that tomorrow is going to be sunny! Can'\''t wait!", "source": "en", "target": "es"}'

What comes back:

{
  "status": "ok",
  "error": null,
  "data": {
    "sourceLang": "en",
    "targetLang": "es",
    "translatedText": "¡Qué emoción que mañana hará sol! ¡Qué ganas!"
  }
}

text and target are required. source is optional; leave it out and the API auto-detects the language, then reports what it found in sourceLang. text can be up to 50,000 characters.

The translation you want is data.translatedText. Everything else in this tutorial is UI around that one field.

No key yet? Get a free API key first. Header details are in the authentication docs.

Step 1: Scaffold a Vue 3 project

The repo is a plain Vite + Vue 3 app, nothing else installed:

npm create vite@latest translator-app -- --template vue
cd translator-app
npm install

Put your key in a .env file at the project root:

VITE_API_KEY=your-api-key-here

Vite exposes anything prefixed with VITE_ to your client code through import.meta.env. Remember that sentence. It comes back in Step 5.

Step 2: Set up state and the language list

Replace src/App.vue with a <script setup> block. The repo ships 18 common language codes; here’s a shortened version:

<script setup>
import { ref } from 'vue'

const API_KEY = import.meta.env.VITE_API_KEY
const API_URL = 'https://api.apiverve.com/v1/translator'

const languages = [
  { code: 'en', name: 'English' },
  { code: 'es', name: 'Spanish' },
  { code: 'fr', name: 'French' },
  { code: 'de', name: 'German' },
  { code: 'pt', name: 'Portuguese' },
  { code: 'ja', name: 'Japanese' }
]

const sourceText = ref('')
const translatedText = ref('')
const sourceLang = ref('en')
const targetLang = ref('es')
const loading = ref(false)
const error = ref('')
</script>

Step 3: Write the translate function

This goes inside the same <script setup> block. Validate first so you don’t spend credits on empty strings or English-to-English requests:

const translate = async () => {
  if (!sourceText.value.trim()) {
    error.value = 'Please enter text to translate'
    return
  }
  if (!API_KEY) {
    error.value = 'Add your API key to .env file (VITE_API_KEY=your-key)'
    return
  }
  if (sourceLang.value === targetLang.value) {
    error.value = 'Source and target languages must be different'
    return
  }

  loading.value = true
  error.value = ''
  translatedText.value = ''

  try {
    const response = await fetch(API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY
      },
      body: JSON.stringify({
        text: sourceText.value,
        source: sourceLang.value,
        target: targetLang.value
      })
    })

    const data = await response.json()

    if (data.status === 'ok') {
      translatedText.value = data.data.translatedText
    } else {
      error.value = data.error || 'Translation failed'
    }
  } catch (err) {
    error.value = 'Failed to translate. Check your API key.'
    console.error('API Error:', err)
  } finally {
    loading.value = false
  }
}

Checking data.status instead of trusting the HTTP call to succeed matters here. Every APIVerve response uses the same status / error / data envelope, so this exact check works for any API you add later. The error handling docs list what error can contain.

Step 4: Build the template

Two dropdowns, a swap button, two text areas, and a translate button:

<template>
  <div class="container">
    <h1>Translator App</h1>

    <div class="language-selector">
      <select v-model="sourceLang">
        <option v-for="lang in languages" :key="lang.code" :value="lang.code">
          {{ lang.name }}
        </option>
      </select>

      <button class="swap-btn" @click="swapLanguages" title="Swap languages">&#8644;</button>

      <select v-model="targetLang">
        <option v-for="lang in languages" :key="lang.code" :value="lang.code">
          {{ lang.name }}
        </option>
      </select>
    </div>

    <textarea v-model="sourceText" placeholder="Enter text to translate..." rows="6"></textarea>
    <span class="char-count">{{ sourceText.length }} characters</span>

    <textarea v-model="translatedText" placeholder="Translation will appear here..." rows="6" readonly></textarea>
    <button v-if="translatedText" @click="copyTranslation">Copy</button>

    <button @click="translate" :disabled="loading || !sourceText.trim()">
      {{ loading ? 'Translating...' : 'Translate' }}
    </button>

    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

The swap and copy helpers go back in the script block. Swapping also flips the text, so a user can translate the result straight back and sanity-check it:

const swapLanguages = () => {
  const temp = sourceLang.value
  sourceLang.value = targetLang.value
  targetLang.value = temp

  if (translatedText.value) {
    const tempText = sourceText.value
    sourceText.value = translatedText.value
    translatedText.value = tempText
  }
}

const copyTranslation = () => {
  if (translatedText.value) {
    navigator.clipboard.writeText(translatedText.value)
  }
}

Run npm run dev, open http://localhost:5173, and translate something. Styles are in the repo’s src/style.css if you want it to look finished.

Step 5: Get the API key out of the browser

The repo calls the API straight from the browser. Great for a tutorial. Not fine for production.

Everything under VITE_ gets bundled into your JavaScript. Anyone can open DevTools, look at the network tab, copy your x-api-key, and spend your credits. The fix is a small server that holds the key and forwards the request.

Install Express and create server.js next to package.json:

npm install express
// server.js (Node 18+ for built-in fetch)
import express from 'express'

const app = express()
app.use(express.json({ limit: '100kb' }))

app.post('/api/translate', async (req, res) => {
  const { text, source, target } = req.body

  if (!text || !target) {
    return res.status(400).json({ status: 'error', error: 'text and target are required', data: null })
  }

  const upstream = await fetch('https://api.apiverve.com/v1/translator', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.APIVERVE_API_KEY
    },
    body: JSON.stringify({ text, source, target })
  })

  res.status(upstream.status).json(await upstream.json())
})

app.listen(3000, () => console.log('Proxy on http://localhost:3000'))

JSON.stringify drops undefined values, so if the client sends no source, the API never sees one and auto-detects. The project already has "type": "module", so the import syntax works as-is.

Point Vite’s dev server at it:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: { '/api': 'http://localhost:3000' }
  }
})

Then in App.vue, delete API_KEY and the !API_KEY check, and change the fetch:

const response = await fetch('/api/translate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    text: sourceText.value,
    source: sourceLang.value,
    target: targetLang.value
  })
})

Start the server with APIVERVE_API_KEY=your-key node server.js, and delete VITE_API_KEY from .env. The response handling from Step 3 doesn’t change, because the proxy passes the API’s JSON through untouched. Add rate limiting to that route before it goes public; the API security checklist covers why.

Step 6: Add “Detect language”

Auto-detection doesn’t need a second request. Add a dropdown entry with an empty code, and don’t send source when it’s selected:

const languages = [
  { code: '', name: 'Detect language' },
  { code: 'en', name: 'English' },
  // ...
]

// in translate():
const body = { text: sourceText.value, target: targetLang.value }
if (sourceLang.value) body.source = sourceLang.value

// after a successful response:
detectedLang.value = data.data.sourceLang

Declare const detectedLang = ref('') alongside the other refs, and show it under the source box (“Detected: fr”). Users like knowing what the app thought they typed.

If you need to know the language without translating (routing a support ticket, picking which reply template to use), the Text Language API is a separate, lighter call. Run it from your server, same as the proxy:

const response = await fetch('https://api.apiverve.com/v1/textlanguage', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.APIVERVE_API_KEY },
  body: JSON.stringify({ text: 'Bonjour, comment allez-vous aujourd\'hui?' })
})
const { data } = await response.json()
console.log(data.iso)      // "fr"
console.log(data.language) // "french"

Match on iso, not language. The name’s capitalization isn’t something to build logic on, and the code plugs straight into the translator’s target. The Language Detector API also works (primaryCode, primaryLanguage), though its confidenceLevel field needs a paid plan.

Where machine translation helps, and where it hurts

The app works. Whether you should put its output in front of customers depends on the content.

Machine translation is solid on factual, predictable text: order confirmations, password reset emails, error messages, help articles, product specs. It’s weak on wordplay, legal language where one word changes the meaning, and marketing copy where tone is the whole point. Use the API for the first group and pay a human for the second. The second group is usually much smaller.

Three more things to decide before you ship:

  • Don’t translate the same string twice. Static text (UI labels, email templates) should be translated once and stored, keyed by content ID plus locale. Translation costs more credits per call than detection, so live translation only makes sense for content you can’t predict, like chat messages or user reviews. The caching strategies post covers the patterns.
  • Pick the language from the right signal. An explicit account setting beats the browser’s Accept-Language header, and that header beats IP-based guessing. Geography is a bad proxy: travelers, expats, and countries like Switzerland break it. Use detection on text users write, not to decide what they read.
  • Start with the pages people get stuck on. Onboarding and transactional emails first. A password reset email the user can’t read is a support ticket waiting to happen.

Next steps

Get a free API key, drop it into server.js, and translate your first string.

Give your software a way to act on the world.

Scaling in production?

The same key runs from your first prototype to millions of calls — on APIVerve's rails, backed by an uptime SLA.

See pricing