NJ
Vue.js internationalization tools and multilingual development setup Tutorials

Vue-elingual: Teaching Your App to Speak Multiple Languages

@nerajno
#javascript #vue #webdev #i18n

9 min read

Vue.js internationalization workflow with multiple language flags and code snippets

It is said that: If you talk to a man in a language he understands, that goes to his head. If you talk to him in his language, that goes to his heart. ~ Nelson Mandela. Software is and should always be accessible, thus the interfaces created so that it can be used in any language. There are various approaches to this, the primary and most recognized approaches are internationalization (i18n) and localization (l10n) - they aren’t the same but are used interchangeably. For this deep dive, we are going to focus on internationalization.

The easiest way to explain or define this term is this: internationalization enables a piece of software to handle multiple language environments, localization enables a piece of software to support a specific regional language environment.

Overview

This guide will show you how to implement internationalization in your Vue 3 application using vue-i18n. You’ll learn how to structure translations, handle dynamic content, and manage language switching - all within the Vue 3 composition API context and all explained in true developer style.

1. Basic Setup and Simple Translations

Developer setting up Vue i18n configuration with code editor

<!-- App.vue -->
<template>
  <div>
    <h1>{{ t('welcome') }}</h1>
    <p v-if="isLoading">{{ t('loading') }}</p>
  </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
</script>
// i18n.js
import { createI18n } from 'vue-i18n'

const i18n = createI18n({
  locale: 'en', // default language
  fallbackLocale: 'en', // fallback for when things go wrong
  messages: {
    en: {
      welcome: 'Welcome to the Matrix',
      errors: {
        404: 'Page is hiding in another castle',
        500: 'Server is having a moment'
      },
      loading: 'Convincing electrons to do work...'
    },
    es: {
      welcome: 'Bienvenido a la Matrix',
      errors: {
        404: 'La página está escondida en otro castillo',
        500: 'El servidor está teniendo un momento'
      },
      loading: 'Convenciendo a los electrones de trabajar...'
    }
  }
})

export default i18n
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './i18n'

const app = createApp(App)
app.use(i18n)
app.mount('#app')

2. Language Switching in Vue 3: The Polyglot Button

Language switcher dropdown with different country flags

<!-- LocaleSwitcher.vue -->
<template>
  <div class="lang-switcher">
    <select v-model="locale" @change="switchLanguage">
      <option
        v-for="(name, lang) in languages"
        :key="lang"
        :value="lang"
      >
        {{ name }}
      </option>
    </select>
  </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'

const { locale, t } = useI18n()

const languages = {
  en: "🇬🇧 English (Compile-time)",
  es: "🇪🇸 Español (Tiempo de compilación)"
}

const switchLanguage = () => {
  console.log(t('debug.langSwitch', { lang: locale.value }))
}
</script>

<style scoped>
.lang-switcher {
  margin: 1rem 0;
}

.lang-switcher select {
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 1rem;
}
</style>

3. Dynamic Values and Interpolation: When Your App Gets Personal

Code editor showing dynamic string interpolation with variables

<!-- DynamicContent.vue -->
<template>
  <div>
    <p>{{ t('greetings.morning', { name: developerName }) }}</p>
    <p>{{ t('bugs.count', { count: bugCount }) }}</p>
  </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'

const { t } = useI18n()
const developerName = ref('Code Wizard')
const bugCount = ref(42)
</script>
// translations/messages.js
export default {
  en: {
    greetings: {
      morning: "Good morning {name}, have you tried turning it off and on again?",
      night: "Still coding at {time}? Same here!",
      weekend: "It's {day}! Time to debug in pajamas!"
    },
    bugs: {
      count: "You have {count} bugs to fix"
    },
    debug: {
      langSwitch: "Language switched to {lang}"
    }
  },
  es: {
    greetings: {
      morning: "Buenos días {name}, ¿has intentado apagarlo y encenderlo de nuevo?",
      night: "¿Aún programando a las {time}? ¡Yo también!",
      weekend: "¡Es {day}! ¡Hora de depurar en pijama!"
    },
    bugs: {
      count: "Tienes {count} errores que arreglar"
    },
    debug: {
      langSwitch: "Idioma cambiado a {lang}"
    }
  }
}

4. Handling Pluralization: Because Zero is Special

Mathematical symbols and plural forms visualization

<!-- PluralExample.vue -->
<template>
  <div>
    <p>{{ $tc('debug.bugs', bugCount, { count: bugCount }) }}</p>
    <p>{{ $tc('coffee.cups', coffeeCount, { count: coffeeCount }) }}</p>

    <div class="controls">
      <button @click="bugCount++">Add Bug</button>
      <button @click="bugCount = Math.max(0, bugCount - 1)">Fix Bug</button>
      <button @click="coffeeCount++">More Coffee</button>
      <button @click="coffeeCount = Math.max(0, coffeeCount - 1)">Drink Coffee</button>
    </div>
  </div>
</template>

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

const bugCount = ref(0)
const coffeeCount = ref(3)
</script>

<style scoped>
.controls {
  margin-top: 1rem;
}

.controls button {
  margin: 0.25rem;
  padding: 0.5rem 1rem;
  background: #007acc;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls button:hover {
  background: #005a9e;
}
</style>
// translations/plural.js
export default {
  en: {
    debug: {
      bugs: "No bugs found (suspicious...) | Found one bug (there's definitely more) | Found {count} bugs (and counting...)"
    },
    coffee: {
      cups: "Emergency: No coffee remaining! | Last coffee warning! | {count} cups of coffee remaining"
    }
  },
  es: {
    debug: {
      bugs: "No se encontraron errores (sospechoso...) | Se encontró un error (definitivamente hay más) | Se encontraron {count} errores (y contando...)"
    },
    coffee: {
      cups: "¡Emergencia: No queda café! | ¡Última advertencia de café! | {count} tazas de café restantes"
    }
  }
}

5. Organizing Translations with Namespaces: The Developer’s Survival Kit

File organization structure with folders and translation files

// translations/developer-life.js
export const developerLife = {
  en: {
    statusMessages: {
      compiling: "Converting caffeine to code...",
      debugging: "Playing hide and seek with bugs...",
      deploying: "Crossing fingers and deploying...",
      success: "It works! Don't touch anything!",
      error: "Error: Success condition not found"
    },
    excuses: {
      deadline: "The deadline was more of a suggestion",
      bug: "It's not a bug, it's an undocumented feature",
      testing: "But it works on my machine!"
    }
  },
  es: {
    statusMessages: {
      compiling: "Convirtiendo cafeína en código...",
      debugging: "Jugando al escondite con los errores...",
      deploying: "Cruzando los dedos y desplegando...",
      success: "¡Funciona! ¡No toques nada!",
      error: "Error: Condición de éxito no encontrada"
    },
    excuses: {
      deadline: "La fecha límite era más una sugerencia",
      bug: "No es un error, es una característica no documentada",
      testing: "¡Pero funciona en mi máquina!"
    }
  }
}

// translations/error-messages.js
export const errorMessages = {
  en: {
    errors: {
      network: "Internet decided to take a coffee break",
      database: "Database is practicing social distancing",
      validation: "Your code is valid but my heart says no"
    }
  },
  es: {
    errors: {
      network: "Internet decidió tomar un descanso de café",
      database: "La base de datos está practicando distanciamiento social",
      validation: "Tu código es válido pero mi corazón dice que no"
    }
  }
}

6. Composition API Integration: Creating a Developer-Friendly Translation Composable

Vue composition API code structure with reusable functions

<!-- TranslationExample.vue -->
<template>
  <div>
    <button @click="showRandomExcuse">
      Generate Developer Excuse
    </button>
    <p v-if="currentExcuse">{{ currentExcuse }}</p>
    <p>{{ debugStatus.message }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDevTranslations } from '../composables/useDevTranslations'

const { getRandomExcuse, debugStatus } = useDevTranslations()
const currentExcuse = ref('')

const showRandomExcuse = () => {
  currentExcuse.value = getRandomExcuse()
}
</script>

<style scoped>
button {
  background: #42b883;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  margin-bottom: 1rem;
}

button:hover {
  background: #369870;
}

p {
  margin: 0.5rem 0;
  padding: 0.5rem;
  background: #f8f9fa;
  border-left: 4px solid #42b883;
}
</style>
// composables/useDevTranslations.js
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'

export function useDevTranslations() {
  const { t, locale } = useI18n()

  const getRandomExcuse = () => {
    const excuses = ['deadline', 'bug', 'testing']
    const randomExcuse = excuses[Math.floor(Math.random() * excuses.length)]
    return t(`excuses.${randomExcuse}`)
  }

  const getCoffeeStatus = (cups) => {
    return t('coffee.cups', cups)
  }

  const getStatusMessage = (status) => {
    return t(`statusMessages.${status}`)
  }

  const debugStatus = computed(() => ({
    message: t('statusMessages.debugging'),
    excuse: getRandomExcuse()
  }))

  return {
    getRandomExcuse,
    getCoffeeStatus,
    getStatusMessage,
    debugStatus,
    currentLocale: locale
  }
}

7. Complete Working Example

Complete Vue.js application with internationalization features

<!-- App.vue -->
<template>
  <div class="app">
    <header>
      <LocaleSwitcher />
      <h1>{{ t('title') }}</h1>
      <p>{{ t('welcome') }}</p>
    </header>

    <main>
      <DynamicContent />
      <PluralExample />
      <TranslationExample />
    </main>
  </div>
</template>

<script setup>
import LocaleSwitcher from './components/LocaleSwitcher.vue'
import DynamicContent from './components/DynamicContent.vue'
import PluralExample from './components/PluralExample.vue'
import TranslationExample from './components/TranslationExample.vue'
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
</script>

<style scoped>
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

header {
  text-align: center;
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid #eee;
}

main {
  display: grid;
  gap: 2rem;
}
</style>
// Complete i18n setup with all translations
import { createI18n } from 'vue-i18n'
import { developerLife } from './translations/developer-life'
import { errorMessages } from './translations/error-messages'
import messages from './translations/messages'
import plural from './translations/plural'

// Merge all translation objects
const mergedMessages = {
  en: {
    title: 'Vue-elingual: Multi-language Magic',
    welcome: 'Welcome to the world of Vue.js internationalization!',
    ...messages.en,
    ...plural.en,
    ...developerLife.en,
    ...errorMessages.en
  },
  es: {
    title: 'Vue-elingual: Magia Multi-idioma',
    welcome: '¡Bienvenido al mundo de la internacionalización de Vue.js!',
    ...messages.es,
    ...plural.es,
    ...developerLife.es,
    ...errorMessages.es
  }
}

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: mergedMessages,
  legacy: false, // Use composition API mode
  globalInjection: true // Allow $t in templates
})

export default i18n

Best Practices for Vue 3 i18n

1. Modular Organization

Keep translations in separate files by feature/module or sort translations like you sort your coffee cups - by importance.

2. Type Safety

Use TypeScript with vue-i18n for better type checking or because future you will thank present you.

// types/i18n.ts
export interface LocaleMessages {
  welcome: string
  errors: {
    404: string
    500: string
  }
  loading: string
}

3. Lazy Loading

Load language files on demand to improve initial load time or like one should brew coffee - on demand.

// Lazy loading example
const loadLocaleMessages = async (locale) => {
  const messages = await import(`./locales/${locale}.json`)
  i18n.global.setLocaleMessage(locale, messages.default)
}

4. Performance Optimization

5. State Management

Consider using Pinia to manage language preferences or because global state is like coffee - best when managed properly.

6. Testing

Write tests for your translations using Vue Test Utils or they’ll test your patience.

// Example test
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import App from './App.vue'

const i18n = createI18n({
  locale: 'en',
  messages: {
    en: { welcome: 'Welcome' }
  }
})

test('renders welcome message', () => {
  const wrapper = mount(App, {
    global: {
      plugins: [i18n]
    }
  })
  expect(wrapper.text()).toContain('Welcome')
})

Astro Integration Notes

When using this in an Astro project, consider:

  1. Component Islands: Vue components with i18n will work as Astro islands
  2. Build-time vs Runtime: Some translations might be better handled at build time
  3. SEO: Use Astro’s routing for different language versions
  4. Static Generation: Pre-generate pages for different locales

Additional Resources

Conclusion

Implementing internationalization in Vue 3 applications doesn’t have to be a nightmare. With vue-i18n and the composition API, you can create maintainable, scalable multilingual applications that speak your users’ language - literally and figuratively.

Remember: good internationalization is like good coffee - it takes time to set up properly, but once you do, everyone appreciates it.

← Back to Blog