Skip to main content

i18n Localization

MStore Dashboard mendukung 3 bahasa menggunakan @nuxtjs/i18n: English, Bahasa Indonesia, dan Japanese.

Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],

  i18n: {
    locales: [
      { code: 'en', name: 'English', file: 'en.json' },
      { code: 'id', name: 'Bahasa Indonesia', file: 'id.json' },
      { code: 'ja', iso: 'ja-JP', name: '日本語', file: 'ja.json' }
    ],
    defaultLocale: 'id',
    strategy: 'no_prefix',      // No URL prefix
    detectBrowserLanguage: false, // Disable auto-detection
    restructureDir: '.',
    langDir: 'locales',
    lazy: true                  // Lazy load translations
  }
})

Locale Files Structure

locales/
├── en.json     # English translations
├── id.json     # Indonesian translations
└── ja.json     # Japanese translations

Example Locale Files

// locales/en.json
{
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "edit": "Edit",
    "search": "Search",
    "loading": "Loading...",
    "noData": "No data available"
  },
  "auth": {
    "login": "Login",
    "logout": "Logout",
    "email": "Email",
    "password": "Password",
    "forgotPassword": "Forgot Password?",
    "loginSuccess": "Login successful",
    "loginFailed": "Login failed"
  },
  "inventory": {
    "title": "Inventory",
    "addProduct": "Add Product",
    "productName": "Product Name",
    "sku": "SKU",
    "price": "Price",
    "stock": "Stock",
    "category": "Category"
  },
  "validation": {
    "required": "This field is required",
    "email": "Please enter a valid email",
    "minLength": "Minimum {min} characters required",
    "maxLength": "Maximum {max} characters allowed"
  }
}
// locales/id.json
{
  "common": {
    "save": "Simpan",
    "cancel": "Batal",
    "delete": "Hapus",
    "edit": "Ubah",
    "search": "Cari",
    "loading": "Memuat...",
    "noData": "Tidak ada data"
  },
  "auth": {
    "login": "Masuk",
    "logout": "Keluar",
    "email": "Email",
    "password": "Kata Sandi",
    "forgotPassword": "Lupa Kata Sandi?",
    "loginSuccess": "Berhasil masuk",
    "loginFailed": "Gagal masuk"
  },
  "inventory": {
    "title": "Inventaris",
    "addProduct": "Tambah Produk",
    "productName": "Nama Produk",
    "sku": "SKU",
    "price": "Harga",
    "stock": "Stok",
    "category": "Kategori"
  },
  "validation": {
    "required": "Field ini wajib diisi",
    "email": "Masukkan email yang valid",
    "minLength": "Minimal {min} karakter",
    "maxLength": "Maksimal {max} karakter"
  }
}
// locales/ja.json
{
  "common": {
    "save": "保存",
    "cancel": "キャンセル",
    "delete": "削除",
    "edit": "編集",
    "search": "検索",
    "loading": "読み込み中...",
    "noData": "データがありません"
  },
  "auth": {
    "login": "ログイン",
    "logout": "ログアウト",
    "email": "メール",
    "password": "パスワード",
    "forgotPassword": "パスワードをお忘れですか?",
    "loginSuccess": "ログインに成功しました",
    "loginFailed": "ログインに失敗しました"
  },
  "inventory": {
    "title": "在庫",
    "addProduct": "商品を追加",
    "productName": "商品名",
    "sku": "SKU",
    "price": "価格",
    "stock": "在庫",
    "category": "カテゴリー"
  }
}

Usage in Components

Basic Usage with $t

<template>
  <div>
    <h1>{{ $t('inventory.title') }}</h1>

    <UButton>{{ $t('common.save') }}</UButton>
    <UButton variant="outline">{{ $t('common.cancel') }}</UButton>

    <UInput :placeholder="$t('common.search')" />
  </div>
</template>

With useI18n Composable

<script setup lang="ts">
const { t, locale, locales, setLocale } = useI18n()

// Get current locale
console.log(locale.value) // 'id'

// Get available locales
console.log(locales.value) // [{ code: 'en', name: 'English' }, ...]

// Change locale
const switchLanguage = (code: string) => {
  setLocale(code)
}
</script>

<template>
  <div>
    <h1>{{ t('inventory.title') }}</h1>

    <!-- Language Switcher -->
    <USelect
      :model-value="locale"
      :options="locales.map(l => ({ label: l.name, value: l.code }))"
      @update:model-value="switchLanguage"
    />
  </div>
</template>

With Parameters

<template>
  <!-- locales/en.json: "welcome": "Welcome, {name}!" -->
  <p>{{ $t('welcome', { name: user.name }) }}</p>

  <!-- locales/en.json: "items": "{count} item | {count} items" -->
  <p>{{ $t('items', { count: itemCount }, itemCount) }}</p>

  <!-- locales/en.json: "minLength": "Minimum {min} characters required" -->
  <p>{{ $t('validation.minLength', { min: 8 }) }}</p>
</template>

Pluralization

// locales/en.json
{
  "cart": {
    "items": "No items | 1 item | {count} items"
  }
}
<template>
  <p>{{ $t('cart.items', itemCount) }}</p>
  <!-- 0 items → "No items" -->
  <!-- 1 item → "1 item" -->
  <!-- 5 items → "5 items" -->
</template>

Language Switcher Component

<!-- components/LanguageSwitcher.vue -->
<script setup lang="ts">
const { locale, locales, setLocale } = useI18n()

const currentLocale = computed(() =>
  locales.value.find(l => l.code === locale.value)
)

const items = computed(() =>
  locales.value.map(l => [{
    label: l.name,
    click: () => setLocale(l.code),
    active: l.code === locale.value,
  }])
)
</script>

<template>
  <UDropdown :items="items">
    <UButton variant="ghost" class="flex items-center gap-2">
      <UIcon name="i-heroicons-globe-alt" />
      <span>{{ currentLocale?.name }}</span>
      <UIcon name="i-heroicons-chevron-down" class="w-4 h-4" />
    </UButton>
  </UDropdown>
</template>

Formatting

Date Formatting

<script setup lang="ts">
const { d, locale } = useI18n()

const date = new Date()
</script>

<template>
  <p>{{ d(date, 'short') }}</p>
  <p>{{ d(date, 'long') }}</p>
</template>
Configure date formats in each locale:
// locales/id.json
{
  "dateTimeFormats": {
    "short": {
      "year": "numeric",
      "month": "short",
      "day": "numeric"
    },
    "long": {
      "year": "numeric",
      "month": "long",
      "day": "numeric",
      "weekday": "long"
    }
  }
}

Number Formatting

<script setup lang="ts">
const { n, locale } = useI18n()

const price = 1500000
</script>

<template>
  <p>{{ n(price, 'currency') }}</p>
  <!-- id: Rp 1.500.000 -->
  <!-- en: $1,500,000 -->
  <!-- ja: ¥1,500,000 -->
</template>
Configure number formats:
// locales/id.json
{
  "numberFormats": {
    "currency": {
      "style": "currency",
      "currency": "IDR",
      "currencyDisplay": "symbol"
    },
    "decimal": {
      "style": "decimal",
      "minimumFractionDigits": 2,
      "maximumFractionDigits": 2
    }
  }
}

Persisting Language Preference

// composables/useLanguagePreference.ts
export const useLanguagePreference = () => {
  const { locale, setLocale } = useI18n()

  // Load saved preference on mount
  onMounted(() => {
    const saved = localStorage.getItem('language')
    if (saved) {
      setLocale(saved)
    }
  })

  // Save preference when changed
  watch(locale, (newLocale) => {
    localStorage.setItem('language', newLocale)
  })

  return { locale, setLocale }
}

Integration with Backend

Sending Locale to API

// plugins/api.client.ts
export default defineNuxtPlugin(() => {
  const { locale } = useI18n()

  const $api = $fetch.create({
    onRequest({ options }) {
      // Send locale header to backend
      options.headers = {
        ...options.headers,
        'Accept-Language': locale.value,
      }
    },
  })

  return {
    provide: { api: $api },
  }
})

Translating Backend Errors

// composables/useApiError.ts
export const useApiError = () => {
  const { t } = useI18n()

  const translateError = (error: ApiError): string => {
    // Try to find translation for error code
    const translationKey = `errors.${error.code}`

    // Check if translation exists
    if (t(translationKey) !== translationKey) {
      return t(translationKey, error.params || {})
    }

    // Fallback to original message
    return error.message
  }

  return { translateError }
}

Organizing Translations

For large applications, organize translations by feature:
locales/
├── en/
│   ├── common.json
│   ├── auth.json
│   ├── inventory.json
│   ├── sales.json
│   └── index.ts       # Merge all
├── id/
│   ├── common.json
│   ├── auth.json
│   ├── inventory.json
│   ├── sales.json
│   └── index.ts
└── ja/
    └── ...
// locales/en/index.ts
import common from './common.json'
import auth from './auth.json'
import inventory from './inventory.json'
import sales from './sales.json'

export default {
  ...common,
  auth,
  inventory,
  sales,
}

Best Practices

Use hierarchical keys:
{
  "module.section.action": "Translation"
}
Example:
  • inventory.form.save
  • auth.error.invalidCredentials
  • common.button.cancel
Enable lazy loading untuk mengurangi initial bundle size:
i18n: {
  lazy: true,
  langDir: 'locales'
}
Selalu set default locale:
i18n: {
  defaultLocale: 'id',
  fallbackLocale: 'en' // Fallback jika key tidak ditemukan
}
Handle missing translations gracefully:
<template>
  <!-- Will show key if translation missing -->
  {{ $t('some.missing.key') }}

  <!-- With fallback -->
  {{ $t('some.missing.key', 'Default Text') }}
</template>

Next Steps