vue-intl

Last updated: August 04, 2025Author: Jakub Pomykała

In this article, you will learn how to configure vue-intl (FormatJS for Vue.js) library with hosted translations via Translation Hosting or locally loaded files. Translation hosting allows you to change translations without re-deploying application as all messages are loaded directly from the cloud. Locally loaded files are useful when you want to keep translations in your repository or load them from a different source.

Installation

Add vue-intl library and @formatjs/cli as development package.

npm install vue-intl
npm install --save-dev @formatjs/cli

Add extract script to your package.json file.

{
  "scripts": {
    "extract": "formatjs extract 'src/**/*.{vue,ts,js}' --ignore='**/*.d.ts' --out-file extracted.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'"
  }
}

The extract script will extract all translation keys from your source code and save them to extracted.json file.

npm run extract

Read more about FormatJS CLI integration.

Configuration

To start using vue-intl library, you need to create a Vue plugin that will provide internationalization functionality to your application. We will create an i18n plugin and configure it to work with SimpleLocalize.

Create i18n plugin

Create a new file src/plugins/i18n.js (or i18n.ts for TypeScript):

// src/plugins/i18n.ts
import { App } from 'vue';
import { IntlProvider, createIntl, IntlConfig } from 'vue-intl';

interface I18nOptions {
  locale: string;
  messages: Record<string, any>;
}

export default {
  install(app: App, options: I18nOptions) {
    const intlConfig: IntlConfig = {
      locale: options.locale,
      messages: options.messages,
      defaultLocale: 'en'
    };

    const intl = createIntl(intlConfig);

    // Make intl available globally
    app.config.globalProperties.$intl = intl;
    app.provide('intl', intl);

    // Global method to change language
    app.config.globalProperties.$changeLanguage = (locale: string, messages: Record<string, any>) => {
      const newIntl = createIntl({
        ...intlConfig,
        locale,
        messages
      });
      app.config.globalProperties.$intl = newIntl;
    };
  }
};

Create language store

Create a Pinia store to manage the current language and translations:

// src/stores/language.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useLanguageStore = defineStore('language', () => {
  const currentLanguage = ref('en');
  const messages = ref<Record<string, any>>({});
  const isLoading = ref(false);

  const PROJECT_TOKEN = "<YOUR_PROJECT_TOKEN>";
  const BASE_URL = "https://cdn.simplelocalize.io";
  const ENVIRONMENT = "_latest"; // or "_production"

  const changeLanguage = async (language: string) => {
    isLoading.value = true;
    try {
      await fetchTranslationMessages(language);
      currentLanguage.value = language;
      localStorage.setItem('preferred-language', language);
    } catch (error) {
      console.error('Failed to load translations:', error);
    } finally {
      isLoading.value = false;
    }
  };

  const fetchTranslationMessages = async (language: string): Promise<void> => {
    const url = `${BASE_URL}/${PROJECT_TOKEN}/${ENVIRONMENT}/${language}`;
    const response = await fetch(url);
    const translationMessages = await response.json();
    messages.value = translationMessages;
  };

  const loadInitialLanguage = async () => {
    const savedLanguage = localStorage.getItem('preferred-language') || 'en';
    await changeLanguage(savedLanguage);
  };

  return {
    currentLanguage: computed(() => currentLanguage.value),
    messages: computed(() => messages.value),
    isLoading: computed(() => isLoading.value),
    changeLanguage,
    loadInitialLanguage
  };
});

Load translations

Option 1: Hosted translations

If you want to use hosted translations, configure your main Vue application file to fetch translations from SimpleLocalize CDN:

// src/main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import i18nPlugin from './plugins/i18n';
import { useLanguageStore } from './stores/language';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);

// Initialize language store and load translations
const languageStore = useLanguageStore();
await languageStore.loadInitialLanguage();

// Install i18n plugin with initial translations
app.use(i18nPlugin, {
  locale: languageStore.currentLanguage,
  messages: languageStore.messages
});

app.mount('#app');

Option 2: Locally loaded files

If you want to load translations from local files, import them directly:

// src/main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import i18nPlugin from './plugins/i18n';

// Import translation files directly
import enMessages from './translations/en.json';
import esMessages from './translations/es.json';
import frMessages from './translations/fr.json';

const TRANSLATIONS: Record<string, Record<string, any>> = {
  en: enMessages,
  es: esMessages,
  fr: frMessages,
};

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);

// Get initial language from localStorage or default to 'en'
const initialLanguage = localStorage.getItem('preferred-language') || 'en';
const initialMessages = TRANSLATIONS[initialLanguage] || TRANSLATIONS['en'];

// Install i18n plugin with local translations
app.use(i18nPlugin, {
  locale: initialLanguage,
  messages: initialMessages
});

app.mount('#app');

Both options are valid, and you can choose the one that fits your needs.

Usage

Using Composition API

Create a composable for accessing internationalization functions:

// src/composables/useI18n.ts
import { inject } from 'vue';
import { IntlShape } from 'vue-intl';

export function useI18n() {
  const intl = inject<IntlShape>('intl');

  if (!intl) {
    throw new Error('useI18n must be used within an IntlProvider');
  }

  const t = (id: string, values?: Record<string, any>, defaultMessage?: string) => {
    return intl.formatMessage({ id, defaultMessage }, values);
  };

  const formatNumber = (value: number, options?: Intl.NumberFormatOptions) => {
    return intl.formatNumber(value, options);
  };

  const formatDate = (value: Date | number, options?: Intl.DateTimeFormatOptions) => {
    return intl.formatDate(value, options);
  };

  const formatTime = (value: Date | number, options?: Intl.DateTimeFormatOptions) => {
    return intl.formatTime(value, options);
  };

  return {
    t,
    formatNumber,
    formatDate,
    formatTime,
    locale: intl.locale
  };
}

Using in components

Use the useI18n composable in your Vue components:

<!-- src/components/WelcomeMessage.vue -->
<template>
  <div class="welcome-message">
    <h1>{{ t('welcome.title', { name: userName }) }}</h1>
    <p>{{ t('welcome.description') }}</p>
    <p>{{ t('welcome.current_time', { time: formatTime(currentTime) }) }}</p>
    <p>{{ t('welcome.user_count', { count: userCount }) }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from '@/composables/useI18n';

const { t, formatTime } = useI18n();

const userName = ref('John Doe');
const currentTime = ref(new Date());
const userCount = ref(1250);
</script>

Using with Options API

For components using Options API, you can access translations through this.$intl:

<!-- src/components/UserProfile.vue -->
<template>
  <div class="user-profile">
    <h2>{{ $intl.formatMessage({ id: 'profile.title' }) }}</h2>
    <p>{{ $intl.formatMessage({ id: 'profile.joined' }, { date: formatDate(joinDate) }) }}</p>
    <p>{{ $intl.formatMessage({ id: 'profile.posts' }, { count: postCount }) }}</p>
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  data() {
    return {
      joinDate: new Date('2023-01-15'),
      postCount: 42
    };
  },
  methods: {
    formatDate(date) {
      return this.$intl.formatDate(date, {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      });
    }
  }
};
</script>

Language switching

Create a language switcher component:

<!-- src/components/LanguageSwitcher.vue -->
<template>
  <div class="language-switcher">
    <select
      :value="currentLanguage"
      @change="handleLanguageChange"
      :disabled="isLoading"
      class="language-select"
    >
      <option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
        {{ lang.name }}
      </option>
    </select>
    <span v-if="isLoading" class="loading-indicator">Loading...</span>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useLanguageStore } from '@/stores/language';

const languageStore = useLanguageStore();

const availableLanguages = [
  { code: 'en', name: 'English' },
  { code: 'es', name: 'Español' },
  { code: 'fr', name: 'Français' },
  { code: 'de', name: 'Deutsch' }
];

const currentLanguage = computed(() => languageStore.currentLanguage);
const isLoading = computed(() => languageStore.isLoading);

const handleLanguageChange = async (event: Event) => {
  const target = event.target as HTMLSelectElement;
  const newLanguage = target.value;

  if (newLanguage !== currentLanguage.value) {
    await languageStore.changeLanguage(newLanguage);

    // Update the intl instance globally
    if (window.location.reload) {
      window.location.reload(); // Simple approach - reload page
    }
  }
};
</script>

<style scoped>
.language-switcher {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.language-select {
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
}

.loading-indicator {
  font-size: 0.875rem;
  color: #666;
}
</style>

Message extraction with FormatJS CLI

Configure extraction

Add extraction script to your package.json:

{
  "scripts": {
    "i18n:extract": "formatjs extract 'src/**/*.{vue,ts,js}' --ignore='**/*.d.ts' --out-file ./lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
    "i18n:compile": "formatjs compile-folder --ast ./lang ./compiled-lang"
  }
}

Extract messages

Run the extraction command to extract all translatable messages from your Vue components:

npm run i18n:extract

This will create an extracted.json file with all the translation keys found in your application.

Example extracted file:

{
  "welcome.title": {
    "defaultMessage": "Welcome, {name}!",
    "description": "Welcome message for users"
  },
  "welcome.description": {
    "defaultMessage": "This is a Vue.js application with internationalization",
    "description": "App description"
  },
  "profile.title": {
    "defaultMessage": "User Profile",
    "description": "Profile page title"
  }
}

Managing translations with SimpleLocalize

Install SimpleLocalize CLI

# macOS / Linux / Windows (WSL) curl -s https://get.simplelocalize.io/2.9/install | bash # Windows (PowerShell) . { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex; # npm npm install @simplelocalize/cli
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.9/install | bash

# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex;

# npm
npm install @simplelocalize/cli

Configure SimpleLocalize

Create a simplelocalize.yml file in the root of your project:

apiKey: YOUR_PROJECT_API_KEY

# Upload configuration
uploadFormat: simplelocalize-json
uploadPath: ./extracted.json
uploadLanguageKey: en
uploadOptions:
  - REPLACE_TRANSLATION_IF_FOUND

# Download configuration
downloadPath: ./src/translations/{lang}.json
downloadLanguageKeys: ['en', 'es', 'fr', 'de']
downloadFormat: single-language-json
downloadOptions:
  - CREATE_DIRECTORIES

Upload source translations

Extract and upload your source translations:

# Extract messages from your Vue components
npm run i18n:extract

# Upload extracted messages to SimpleLocalize
simplelocalize upload

Manage translations

Now you can manage and translate your messages in the SimpleLocalize translation editor:

  1. Add new languages in the Languages tab
  2. Use auto-translation to quickly translate into multiple languages
  3. Manually refine translations for better quality
  4. Add context and descriptions to help translators

Download translations

Download completed translations to your project:

# Download all language translations
simplelocalize download

This will create translation files in ./src/translations/ directory:

  • en.json
  • es.json
  • fr.json
  • de.json

Build workflow

Add these scripts to your package.json for a complete workflow:

{
  "scripts": {
    "i18n:extract": "formatjs extract 'src/**/*.{vue,ts,js}' --ignore='**/*.d.ts' --out-file ./extracted.json",
    "i18n:upload": "simplelocalize upload",
    "i18n:download": "simplelocalize download",
    "i18n:sync": "npm run i18n:extract && npm run i18n:upload && npm run i18n:download",
    "build": "npm run i18n:download && vite build"
  }
}

Advanced features

Pluralization

vue-intl supports ICU message format for pluralization:

{
  "item.count": "{count, plural, =0 {No items} =1 {One item} other {# items}}"
}

Usage in component:

<template>
  <p>{{ t('item.count', { count: itemCount }) }}</p>
</template>

Rich text formatting

Support for HTML and custom components in messages:

{
  "terms.agreement": "By signing up, you agree to our <link>Terms of Service</link>"
}
<template>
  <p v-html="t('terms.agreement', {
    link: (chunks) => `<a href='/terms'>${chunks}</a>`
  })"></p>
</template>

Date and number formatting

<template>
  <div>
    <p>{{ formatDate(new Date(), { dateStyle: 'full' }) }}</p>
    <p>{{ formatNumber(1234.56, { style: 'currency', currency: 'USD' }) }}</p>
  </div>
</template>

<script setup>
import { useI18n } from '@/composables/useI18n';

const { formatDate, formatNumber } = useI18n();
</script>

TypeScript support

Type-safe translation keys

Create type definitions for your translation keys:

// src/types/i18n.ts
export interface TranslationKeys {
  'welcome.title': string;
  'welcome.description': string;
  'profile.title': string;
  'item.count': string;
  // Add more keys as needed
}

export type TranslationKey = keyof TranslationKeys;

Enhanced useI18n hook

// src/composables/useI18n.ts
import { inject } from 'vue';
import { IntlShape } from 'vue-intl';
import type { TranslationKey } from '@/types/i18n';

export function useI18n() {
  const intl = inject<IntlShape>('intl');

  if (!intl) {
    throw new Error('useI18n must be used within an IntlProvider');
  }

  const t = (id: TranslationKey, values?: Record<string, any>, defaultMessage?: string) => {
    return intl.formatMessage({ id, defaultMessage }, values);
  };

  const formatNumber = (value: number, options?: Intl.NumberFormatOptions) => {
    return intl.formatNumber(value, options);
  };

  const formatDate = (value: Date | number, options?: Intl.DateTimeFormatOptions) => {
    return intl.formatDate(value, options);
  };

  const formatTime = (value: Date | number, options?: Intl.DateTimeFormatOptions) => {
    return intl.formatTime(value, options);
  };

  return { t, formatNumber, formatDate, formatTime, locale: intl.locale };
}

Resources

Was this helpful?