Skip to content

Internationalization and Localization

Overview

Element Plus provides comprehensive internationalization (i18n) support, allowing you to create applications that work seamlessly across different languages and regions. This guide covers setting up i18n, using built-in locales, creating custom translations, and implementing advanced localization features.

Basic Setup

Installing Dependencies

bash
npm install vue-i18n@9

Basic Configuration

javascript
// main.js
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

// Import Element Plus locales
import enLocale from 'element-plus/es/locale/lang/en'
import zhLocale from 'element-plus/es/locale/lang/zh-cn'
import esLocale from 'element-plus/es/locale/lang/es'

// Your custom messages
const messages = {
  en: {
    ...enLocale,
    message: {
      hello: 'Hello World',
      welcome: 'Welcome to our application'
    }
  },
  'zh-cn': {
    ...zhLocale,
    message: {
      hello: '你好世界',
      welcome: '欢迎使用我们的应用程序'
    }
  },
  es: {
    ...esLocale,
    message: {
      hello: 'Hola Mundo',
      welcome: 'Bienvenido a nuestra aplicación'
    }
  }
}

const i18n = createI18n({
  locale: 'en', // default locale
  fallbackLocale: 'en',
  messages
})

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

Component Usage

vue
<template>
  <div>
    <h1>{{ $t('message.hello') }}</h1>
    <p>{{ $t('message.welcome') }}</p>
    
    <!-- Element Plus components will use the configured locale -->
    <el-date-picker 
      v-model="date" 
      type="date" 
      :placeholder="$t('el.datepicker.selectDate')"
    />
    
    <el-button @click="changeLocale">{{ $t('message.changeLanguage') }}</el-button>
  </div>
</template>

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

const { locale, t } = useI18n()
const date = ref('')

const changeLocale = () => {
  locale.value = locale.value === 'en' ? 'zh-cn' : 'en'
}
</script>

Element Plus Locale Configuration

Using ConfigProvider

vue
<template>
  <el-config-provider :locale="currentLocale">
    <div id="app">
      <!-- Your application content -->
      <el-date-picker v-model="date" type="date" />
      <el-pagination 
        :total="100" 
        :page-size="10" 
        layout="prev, pager, next"
      />
    </div>
  </el-config-provider>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import enLocale from 'element-plus/es/locale/lang/en'
import zhLocale from 'element-plus/es/locale/lang/zh-cn'
import esLocale from 'element-plus/es/locale/lang/es'
import frLocale from 'element-plus/es/locale/lang/fr'
import deLocale from 'element-plus/es/locale/lang/de'
import jaLocale from 'element-plus/es/locale/lang/ja'

const { locale } = useI18n()
const date = ref('')

const localeMap = {
  'en': enLocale,
  'zh-cn': zhLocale,
  'es': esLocale,
  'fr': frLocale,
  'de': deLocale,
  'ja': jaLocale
}

const currentLocale = computed(() => localeMap[locale.value] || enLocale)
</script>

Global Locale Configuration

javascript
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'

const app = createApp(App)
app.use(ElementPlus, { locale })

Available Locales

Element Plus supports numerous locales out of the box:

javascript
// Common locales
import en from 'element-plus/es/locale/lang/en'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import zhTw from 'element-plus/es/locale/lang/zh-tw'
import es from 'element-plus/es/locale/lang/es'
import fr from 'element-plus/es/locale/lang/fr'
import de from 'element-plus/es/locale/lang/de'
import ja from 'element-plus/es/locale/lang/ja'
import ko from 'element-plus/es/locale/lang/ko'
import ru from 'element-plus/es/locale/lang/ru'
import it from 'element-plus/es/locale/lang/it'
import pt from 'element-plus/es/locale/lang/pt'
import ar from 'element-plus/es/locale/lang/ar'
import th from 'element-plus/es/locale/lang/th'
import vi from 'element-plus/es/locale/lang/vi'

// And many more...
const supportedLocales = {
  'en': en,
  'zh-cn': zhCn,
  'zh-tw': zhTw,
  'es': es,
  'fr': fr,
  'de': de,
  'ja': ja,
  'ko': ko,
  'ru': ru,
  'it': it,
  'pt': pt,
  'ar': ar,
  'th': th,
  'vi': vi
}

Custom Locale Creation

Creating a Custom Locale

javascript
// locales/custom-en.js
export default {
  name: 'en',
  el: {
    colorpicker: {
      confirm: 'OK',
      clear: 'Clear',
      defaultLabel: 'color picker',
      description: 'current color is {color}. press enter to select a new color.'
    },
    datepicker: {
      now: 'Now',
      today: 'Today',
      cancel: 'Cancel',
      clear: 'Clear',
      confirm: 'OK',
      selectDate: 'Select date',
      selectTime: 'Select time',
      startDate: 'Start Date',
      startTime: 'Start Time',
      endDate: 'End Date',
      endTime: 'End Time',
      prevYear: 'Previous Year',
      nextYear: 'Next Year',
      prevMonth: 'Previous Month',
      nextMonth: 'Next Month',
      year: '',
      month1: 'January',
      month2: 'February',
      month3: 'March',
      month4: 'April',
      month5: 'May',
      month6: 'June',
      month7: 'July',
      month8: 'August',
      month9: 'September',
      month10: 'October',
      month11: 'November',
      month12: 'December',
      weeks: {
        sun: 'Sun',
        mon: 'Mon',
        tue: 'Tue',
        wed: 'Wed',
        thu: 'Thu',
        fri: 'Fri',
        sat: 'Sat'
      },
      months: {
        jan: 'Jan',
        feb: 'Feb',
        mar: 'Mar',
        apr: 'Apr',
        may: 'May',
        jun: 'Jun',
        jul: 'Jul',
        aug: 'Aug',
        sep: 'Sep',
        oct: 'Oct',
        nov: 'Nov',
        dec: 'Dec'
      }
    },
    select: {
      loading: 'Loading',
      noMatch: 'No matching data',
      noData: 'No data',
      placeholder: 'Select'
    },
    cascader: {
      noMatch: 'No matching data',
      loading: 'Loading',
      placeholder: 'Select',
      noData: 'No data'
    },
    pagination: {
      goto: 'Go to',
      pagesize: '/page',
      total: 'Total {total}',
      pageClassifier: '',
      page: 'Page',
      prev: 'Go to previous page',
      next: 'Go to next page',
      currentPage: 'page {pager}',
      prevPages: 'Previous {pager} pages',
      nextPages: 'Next {pager} pages'
    },
    messagebox: {
      title: 'Message',
      confirm: 'OK',
      cancel: 'Cancel',
      error: 'Illegal input'
    },
    upload: {
      deleteTip: 'press delete to remove',
      delete: 'Delete',
      preview: 'Preview',
      continue: 'Continue'
    },
    table: {
      emptyText: 'No Data',
      confirmFilter: 'Confirm',
      resetFilter: 'Reset',
      clearFilter: 'All',
      sumText: 'Sum'
    },
    tree: {
      emptyText: 'No data'
    },
    transfer: {
      noMatch: 'No matching data',
      noData: 'No data',
      titles: ['List 1', 'List 2'],
      filterPlaceholder: 'Enter keyword',
      noCheckedFormat: '{total} items',
      hasCheckedFormat: '{checked}/{total} checked'
    },
    image: {
      error: 'FAILED'
    },
    pageHeader: {
      title: 'Back'
    },
    popconfirm: {
      confirmButtonText: 'Yes',
      cancelButtonText: 'No'
    }
  }
}

Extending Existing Locales

javascript
// locales/extended-en.js
import enLocale from 'element-plus/es/locale/lang/en'

export default {
  ...enLocale,
  el: {
    ...enLocale.el,
    // Override specific translations
    datepicker: {
      ...enLocale.el.datepicker,
      selectDate: 'Choose a date',
      selectTime: 'Choose a time'
    },
    // Add custom component translations
    customComponent: {
      save: 'Save',
      cancel: 'Cancel',
      delete: 'Delete',
      edit: 'Edit'
    }
  },
  // Add your application-specific translations
  app: {
    navigation: {
      home: 'Home',
      about: 'About',
      contact: 'Contact'
    },
    forms: {
      required: 'This field is required',
      email: 'Please enter a valid email address',
      password: 'Password must be at least 8 characters'
    }
  }
}

Advanced I18n Features

Dynamic Locale Loading

javascript
// i18n/index.js
import { createI18n } from 'vue-i18n'

// Lazy load locale messages
const loadLocaleMessages = async (locale) => {
  const messages = await import(`./locales/${locale}.js`)
  return messages.default
}

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

// Load locale dynamically
export const loadLocale = async (locale) => {
  if (!i18n.global.availableLocales.includes(locale)) {
    const messages = await loadLocaleMessages(locale)
    i18n.global.setLocaleMessage(locale, messages)
  }
  i18n.global.locale.value = locale
}

export default i18n

Locale Detection

javascript
// utils/locale-detection.js
export const detectLocale = () => {
  // 1. Check URL parameter
  const urlParams = new URLSearchParams(window.location.search)
  const urlLocale = urlParams.get('lang')
  if (urlLocale) return urlLocale
  
  // 2. Check localStorage
  const savedLocale = localStorage.getItem('locale')
  if (savedLocale) return savedLocale
  
  // 3. Check browser language
  const browserLocale = navigator.language.toLowerCase()
  const supportedLocales = ['en', 'zh-cn', 'es', 'fr', 'de']
  
  // Exact match
  if (supportedLocales.includes(browserLocale)) {
    return browserLocale
  }
  
  // Language code match (e.g., 'en-US' -> 'en')
  const languageCode = browserLocale.split('-')[0]
  if (supportedLocales.includes(languageCode)) {
    return languageCode
  }
  
  // Default fallback
  return 'en'
}

export const saveLocale = (locale) => {
  localStorage.setItem('locale', locale)
}

Language Switcher Component

vue
<template>
  <el-dropdown @command="changeLanguage">
    <el-button>
      <el-icon><Globe /></el-icon>
      {{ currentLanguageLabel }}
      <el-icon class="el-icon--right"><arrow-down /></el-icon>
    </el-button>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item 
          v-for="lang in languages" 
          :key="lang.code"
          :command="lang.code"
          :class="{ 'is-active': currentLocale === lang.code }"
        >
          <span class="flag">{{ lang.flag }}</span>
          {{ lang.label }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Globe, ArrowDown } from '@element-plus/icons-vue'
import { loadLocale, saveLocale } from '@/utils/locale-detection'

const { locale } = useI18n()

const languages = [
  { code: 'en', label: 'English', flag: '🇺🇸' },
  { code: 'zh-cn', label: '简体中文', flag: '🇨🇳' },
  { code: 'es', label: 'Español', flag: '🇪🇸' },
  { code: 'fr', label: 'Français', flag: '🇫🇷' },
  { code: 'de', label: 'Deutsch', flag: '🇩🇪' },
  { code: 'ja', label: '日本語', flag: '🇯🇵' },
  { code: 'ko', label: '한국어', flag: '🇰🇷' },
  { code: 'ru', label: 'Русский', flag: '🇷🇺' }
]

const currentLocale = computed(() => locale.value)

const currentLanguageLabel = computed(() => {
  const current = languages.find(lang => lang.code === currentLocale.value)
  return current ? current.label : 'Language'
})

const changeLanguage = async (langCode) => {
  await loadLocale(langCode)
  saveLocale(langCode)
  
  // Update URL parameter
  const url = new URL(window.location)
  url.searchParams.set('lang', langCode)
  window.history.replaceState({}, '', url)
}
</script>

<style scoped>
.flag {
  margin-right: 8px;
  font-size: 16px;
}

.is-active {
  background-color: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
}
</style>

Pluralization

javascript
// messages/en.js
export default {
  message: {
    item: 'no items | one item | {count} items',
    notification: 'You have {count} notification | You have {count} notifications'
  }
}

// messages/zh-cn.js
export default {
  message: {
    item: '没有项目 | {count} 个项目',
    notification: '您有 {count} 条通知'
  }
}
vue
<template>
  <div>
    <!-- Pluralization usage -->
    <p>{{ $tc('message.item', itemCount, { count: itemCount }) }}</p>
    <p>{{ $tc('message.notification', notificationCount, { count: notificationCount }) }}</p>
  </div>
</template>

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

const itemCount = ref(5)
const notificationCount = ref(1)
</script>

Date and Number Formatting

vue
<template>
  <div>
    <!-- Date formatting -->
    <p>{{ $d(new Date(), 'short') }}</p>
    <p>{{ $d(new Date(), 'long') }}</p>
    
    <!-- Number formatting -->
    <p>{{ $n(1234.56, 'currency') }}</p>
    <p>{{ $n(0.75, 'percent') }}</p>
  </div>
</template>

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

const { d, n } = useI18n({
  dateTimeFormats: {
    'en': {
      short: {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      },
      long: {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long',
        hour: 'numeric',
        minute: 'numeric'
      }
    },
    'zh-cn': {
      short: {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      },
      long: {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long',
        hour: 'numeric',
        minute: 'numeric',
        hour12: false
      }
    }
  },
  numberFormats: {
    'en': {
      currency: {
        style: 'currency',
        currency: 'USD'
      },
      percent: {
        style: 'percent'
      }
    },
    'zh-cn': {
      currency: {
        style: 'currency',
        currency: 'CNY'
      },
      percent: {
        style: 'percent'
      }
    }
  }
})
</script>

RTL (Right-to-Left) Support

RTL Configuration

vue
<template>
  <el-config-provider :locale="currentLocale" :direction="direction">
    <div class="app" :dir="direction">
      <!-- Your application content -->
      <el-form :model="form" label-width="120px">
        <el-form-item :label="$t('form.name')">
          <el-input v-model="form.name" />
        </el-form-item>
        <el-form-item :label="$t('form.email')">
          <el-input v-model="form.email" />
        </el-form-item>
      </el-form>
    </div>
  </el-config-provider>
</template>

<script setup>
import { computed, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import arLocale from 'element-plus/es/locale/lang/ar'
import enLocale from 'element-plus/es/locale/lang/en'

const { locale } = useI18n()

const form = reactive({
  name: '',
  email: ''
})

const rtlLocales = ['ar', 'he', 'fa']

const direction = computed(() => {
  return rtlLocales.includes(locale.value) ? 'rtl' : 'ltr'
})

const currentLocale = computed(() => {
  return locale.value === 'ar' ? arLocale : enLocale
})
</script>

<style>
/* RTL-specific styles */
[dir="rtl"] .el-form-item__label {
  text-align: right;
}

[dir="rtl"] .el-input__inner {
  text-align: right;
}

[dir="rtl"] .el-button {
  margin-left: 0;
  margin-right: 10px;
}
</style>

Testing I18n

Unit Testing with Vue Test Utils

javascript
// tests/components/LanguageSwitcher.test.js
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'

const messages = {
  en: {
    language: 'Language'
  },
  'zh-cn': {
    language: '语言'
  }
}

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

describe('LanguageSwitcher', () => {
  it('displays current language', () => {
    const wrapper = mount(LanguageSwitcher, {
      global: {
        plugins: [i18n]
      }
    })
    
    expect(wrapper.text()).toContain('English')
  })
  
  it('changes language when option is selected', async () => {
    const wrapper = mount(LanguageSwitcher, {
      global: {
        plugins: [i18n]
      }
    })
    
    await wrapper.find('[data-testid="language-option-zh-cn"]').trigger('click')
    
    expect(i18n.global.locale.value).toBe('zh-cn')
  })
})

E2E Testing

javascript
// tests/e2e/i18n.spec.js
import { test, expect } from '@playwright/test'

test('language switching works correctly', async ({ page }) => {
  await page.goto('/')
  
  // Check default language
  await expect(page.locator('[data-testid="welcome-message"]')).toContainText('Welcome')
  
  // Switch to Chinese
  await page.click('[data-testid="language-switcher"]')
  await page.click('[data-testid="language-option-zh-cn"]')
  
  // Check Chinese translation
  await expect(page.locator('[data-testid="welcome-message"]')).toContainText('欢迎')
  
  // Check Element Plus components are translated
  await expect(page.locator('.el-pagination__total')).toContainText('共')
})

Performance Optimization

Lazy Loading Translations

javascript
// composables/useI18nLazy.js
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'

const loadedLocales = ref(new Set(['en'])) // Default locale is pre-loaded

export function useI18nLazy() {
  const { locale, setLocaleMessage } = useI18n()
  
  const isLocaleLoaded = computed(() => {
    return loadedLocales.value.has(locale.value)
  })
  
  const loadLocale = async (targetLocale) => {
    if (loadedLocales.value.has(targetLocale)) {
      locale.value = targetLocale
      return
    }
    
    try {
      // Load Element Plus locale
      const elLocale = await import(`element-plus/es/locale/lang/${targetLocale}.js`)
      
      // Load app-specific translations
      const appMessages = await import(`@/locales/${targetLocale}.js`)
      
      // Merge messages
      const messages = {
        ...elLocale.default,
        ...appMessages.default
      }
      
      setLocaleMessage(targetLocale, messages)
      loadedLocales.value.add(targetLocale)
      locale.value = targetLocale
      
      // Save to localStorage
      localStorage.setItem('locale', targetLocale)
    } catch (error) {
      console.error(`Failed to load locale ${targetLocale}:`, error)
    }
  }
  
  return {
    isLocaleLoaded,
    loadLocale
  }
}

Bundle Optimization

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Separate i18n chunks
          'i18n-en': ['./src/locales/en.js'],
          'i18n-zh': ['./src/locales/zh-cn.js'],
          'i18n-es': ['./src/locales/es.js'],
          // Element Plus locales
          'el-i18n': [
            'element-plus/es/locale/lang/en',
            'element-plus/es/locale/lang/zh-cn',
            'element-plus/es/locale/lang/es'
          ]
        }
      }
    }
  }
})

Best Practices

1. Organization

  • Keep translation files organized by feature or page
  • Use consistent key naming conventions
  • Implement translation validation
  • Document translation guidelines

2. Performance

  • Lazy load translations for better initial load times
  • Use tree-shaking to include only needed locales
  • Cache translations in localStorage
  • Implement fallback strategies

3. Maintenance

  • Use translation management tools
  • Implement automated translation checks
  • Keep translations up to date with code changes
  • Provide context for translators

4. Accessibility

  • Support RTL languages properly
  • Test with screen readers in different languages
  • Ensure proper text direction and alignment
  • Consider cultural differences in UI patterns

5. User Experience

  • Detect user's preferred language automatically
  • Provide easy language switching
  • Remember user's language preference
  • Handle missing translations gracefully

Conclusion

Element Plus provides robust internationalization support that integrates seamlessly with Vue I18n. By following these patterns and best practices, you can create applications that work effectively across different languages and cultures while maintaining good performance and user experience.

Element Plus Study Guide