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:

javascript
// 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:

vue
<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.):

javascript
// 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#

javascript
// 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#

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

vue
<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#

javascript
// 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#

javascript
// 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#

javascript
// i18n/index.js
export const i18n = createI18n({ ... });
export default i18n;

2. Use Composition API Mode#

javascript
createI18n({
  legacy: false, // Required for useI18n()
  // ...
});

3. Create Type-Safe Helpers (TypeScript)#

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#

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// In Nuxt, use the built-in $t
export default {
  setup() {
    const { $t } = useNuxtApp();
    // or
    const { t } = useI18n();
  }
};

Performance Tips#

Avoid Excessive t() Calls#

javascript
// 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:

  1. useI18n() - Best for Vue components with Composition API
  2. i18n.global.t() - For stores, utilities, and non-component code
  3. Helper functions - For cleaner, reusable translation access
  4. 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.