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
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:
- Add new languages in the Languages tab
- Use auto-translation to quickly translate into multiple languages
- Manually refine translations for better quality
- 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
- vue-intl Documentation
- FormatJS CLI Documentation
- SimpleLocalize CLI Documentation
- Translation Hosting
- Vue.js Internationalization Guide