Introduction#
Vue i18n makes it easy to add translations in Vue templates with the $t() function. But what about translating text in JavaScript files, Vuex/Pinia stores, or utility functions?
This guide shows you how to use Vue i18n translations anywhere in your Vue 3 application, including outside of Vue components.
Prerequisites#
- Vue 3 project with vue-i18n installed
- Basic understanding of Vue Composition API
- Familiarity with i18n concepts
Basic Setup Recap#
First, let's ensure your i18n setup supports JavaScript usage:
// src/i18n/index.js
import { createI18n } from 'vue-i18n';
const messages = {
en: {
greeting: 'Hello, {name}!',
errors: {
required: 'This field is required',
email: 'Please enter a valid email'
}
},
de: {
greeting: 'Hallo, {name}!',
errors: {
required: 'Dieses Feld ist erforderlich',
email: 'Bitte geben Sie eine gültige E-Mail ein'
}
}
};
export const i18n = createI18n({
legacy: false, // Use Composition API mode
locale: 'en',
fallbackLocale: 'en',
messages
});
export default i18n;
Method 1: Using useI18n() Composable#
The recommended way to access translations in Vue 3 components:
<script setup>
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
// Use in script
const greeting = t('greeting', { name: 'John' });
console.log(greeting); // "Hello, John!"
// Change locale
function switchLanguage(lang) {
locale.value = lang;
}
// Use in computed or functions
function getErrorMessage(field) {
return t(`errors.${field}`);
}
</script>
<template>
<p>{{ t('greeting', { name: 'John' }) }}</p>
</template>
Method 2: Global i18n Instance#
For use outside Vue components (stores, utilities, etc.):
// src/utils/notifications.js
import { i18n } from '@/i18n';
export function showSuccessNotification(key, params = {}) {
const message = i18n.global.t(key, params);
// Show notification with translated message
toast.success(message);
}
export function showErrorNotification(errorKey) {
const message = i18n.global.t(`errors.${errorKey}`);
toast.error(message);
}
Using in Pinia Stores#
// src/stores/user.js
import { defineStore } from 'pinia';
import { i18n } from '@/i18n';
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
error: null
}),
actions: {
async login(credentials) {
try {
const response = await api.login(credentials);
this.user = response.data;
// Translated success message
const message = i18n.global.t('auth.loginSuccess');
toast.success(message);
} catch (error) {
// Translated error message
this.error = i18n.global.t('auth.loginFailed');
throw error;
}
},
async logout() {
this.user = null;
const message = i18n.global.t('auth.logoutSuccess');
toast.info(message);
}
}
});
Using in Vuex Stores#
// src/store/modules/auth.js
import { i18n } from '@/i18n';
export default {
namespaced: true,
actions: {
async login({ commit }, credentials) {
try {
const user = await api.login(credentials);
commit('SET_USER', user);
return {
success: true,
message: i18n.global.t('auth.welcome', { name: user.name })
};
} catch (error) {
return {
success: false,
message: i18n.global.t('auth.error')
};
}
}
}
};
Method 3: Creating a Translation Helper#
For cleaner code, create a helper function:
// src/utils/translate.js
import { i18n } from '@/i18n';
/**
* Translate a key outside of Vue components
* @param {string} key - Translation key
* @param {object} params - Interpolation parameters
* @returns {string} Translated string
*/
export function translate(key, params = {}) {
return i18n.global.t(key, params);
}
// Shorthand alias
export const $t = translate;
// For plural translations
export function translatePlural(key, count, params = {}) {
return i18n.global.t(key, { count, ...params });
}
Usage:
// src/services/validation.js
import { $t } from '@/utils/translate';
export const validationRules = {
required: (value) => !!value || $t('errors.required'),
email: (value) => {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(value) || $t('errors.email');
},
minLength: (min) => (value) => {
return value.length >= min || $t('errors.minLength', { min });
},
maxLength: (max) => (value) => {
return value.length <= max || $t('errors.maxLength', { max });
}
};
Method 4: Reactive Translations with Computed#
When you need translations to update reactively:
// src/composables/useTranslatedOptions.js
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
export function useTranslatedOptions() {
const { t } = useI18n();
const statusOptions = computed(() => [
{ value: 'active', label: t('status.active') },
{ value: 'pending', label: t('status.pending') },
{ value: 'inactive', label: t('status.inactive') }
]);
const priorityOptions = computed(() => [
{ value: 'high', label: t('priority.high') },
{ value: 'medium', label: t('priority.medium') },
{ value: 'low', label: t('priority.low') }
]);
return {
statusOptions,
priorityOptions
};
}
Usage in component:
<script setup>
import { useTranslatedOptions } from '@/composables/useTranslatedOptions';
const { statusOptions, priorityOptions } = useTranslatedOptions();
</script>
<template>
<select v-model="status">
<option
v-for="option in statusOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</template>
Method 5: Translation in Route Guards#
// src/router/index.js
import { createRouter } from 'vue-router';
import { i18n } from '@/i18n';
const router = createRouter({
// ... routes
});
router.beforeEach((to, from, next) => {
// Set page title with translation
const title = to.meta.titleKey
? i18n.global.t(to.meta.titleKey)
: 'My App';
document.title = title;
// Check authentication with translated message
if (to.meta.requiresAuth && !isAuthenticated()) {
toast.error(i18n.global.t('auth.loginRequired'));
next('/login');
} else {
next();
}
});
export default router;
Method 6: API Error Handling#
// src/services/api.js
import axios from 'axios';
import { i18n } from '@/i18n';
const api = axios.create({
baseURL: '/api'
});
api.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
let message;
switch (status) {
case 400:
message = i18n.global.t('errors.badRequest');
break;
case 401:
message = i18n.global.t('errors.unauthorized');
break;
case 403:
message = i18n.global.t('errors.forbidden');
break;
case 404:
message = i18n.global.t('errors.notFound');
break;
case 500:
message = i18n.global.t('errors.serverError');
break;
default:
message = i18n.global.t('errors.unknown');
}
toast.error(message);
return Promise.reject(error);
}
);
export default api;
Best Practices#
1. Always Export i18n Instance#
// i18n/index.js
export const i18n = createI18n({ ... });
export default i18n;
2. Use Composition API Mode#
createI18n({
legacy: false, // Required for useI18n()
// ...
});
3. Create Type-Safe Helpers (TypeScript)#
// src/utils/translate.ts
import { i18n } from '@/i18n';
import type { TranslateResult } from 'vue-i18n';
type TranslationKey = keyof typeof import('@/i18n/locales/en.json');
export function translate(
key: TranslationKey,
params?: Record<string, unknown>
): TranslateResult {
return i18n.global.t(key, params ?? {});
}
4. Handle Async Loading#
// For lazy-loaded translations
import { i18n } from '@/i18n';
export async function loadLocaleMessages(locale) {
const messages = await import(`@/i18n/locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages.default);
return messages;
}
// Usage
async function changeLanguage(locale) {
await loadLocaleMessages(locale);
i18n.global.locale.value = locale;
}
Common Issues and Solutions#
Issue 1: t() Returns Key Instead of Translation#
Problem: Getting the key back instead of translated text.
Solutions:
// Check if messages are loaded
console.log(i18n.global.messages.value);
// Ensure correct key path
i18n.global.t('errors.required'); // Not t('required')
// Check locale is set
console.log(i18n.global.locale.value);
Issue 2: Translations Not Reactive#
Problem: Language change doesn't update text.
Solution: Use computed properties:
// Wrong - not reactive
const message = i18n.global.t('greeting');
// Correct - reactive
const message = computed(() => i18n.global.t('greeting'));
Issue 3: useI18n() Outside Setup#
Problem: useI18n() can only be used in setup.
Solution: Use the global instance:
// In regular JS files
import { i18n } from '@/i18n';
const message = i18n.global.t('key');
// In composables (within component context)
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
Issue 4: SSR/Nuxt Compatibility#
Problem: Server-side rendering issues.
Solution: Use the useI18n composable in components:
// In Nuxt, use the built-in $t
export default {
setup() {
const { $t } = useNuxtApp();
// or
const { t } = useI18n();
}
};
Performance Tips#
Avoid Excessive t() Calls#
// Avoid - multiple calls
items.map(item => ({
...item,
statusLabel: i18n.global.t(`status.${item.status}`),
typeLabel: i18n.global.t(`type.${item.type}`)
}));
// Better - batch or memoize
const statusLabels = {
active: i18n.global.t('status.active'),
pending: i18n.global.t('status.pending')
};
items.map(item => ({
...item,
statusLabel: statusLabels[item.status]
}));
Conclusion#
Vue i18n provides flexible ways to use translations in JavaScript:
- useI18n() - Best for Vue components with Composition API
- i18n.global.t() - For stores, utilities, and non-component code
- Helper functions - For cleaner, reusable translation access
- Computed properties - For reactive translations
When translating your Vue i18n JSON files, ensure placeholders and ICU format are preserved. shipglobal.dev automatically handles Vue i18n file formats while maintaining all variable syntax.