i18n Localization
MStore Dashboard mendukung 3 bahasa menggunakan@nuxtjs/i18n: English, Bahasa Indonesia, dan Japanese.
Configuration
Copy
// 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
Copy
locales/
├── en.json # English translations
├── id.json # Indonesian translations
└── ja.json # Japanese translations
Example Locale Files
Copy
// 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"
}
}
Copy
// 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"
}
}
Copy
// 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
Copy
<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
Copy
<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
Copy
<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
Copy
// locales/en.json
{
"cart": {
"items": "No items | 1 item | {count} items"
}
}
Copy
<template>
<p>{{ $t('cart.items', itemCount) }}</p>
<!-- 0 items → "No items" -->
<!-- 1 item → "1 item" -->
<!-- 5 items → "5 items" -->
</template>
Language Switcher Component
Copy
<!-- 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
Copy
<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>
Copy
// locales/id.json
{
"dateTimeFormats": {
"short": {
"year": "numeric",
"month": "short",
"day": "numeric"
},
"long": {
"year": "numeric",
"month": "long",
"day": "numeric",
"weekday": "long"
}
}
}
Number Formatting
Copy
<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>
Copy
// locales/id.json
{
"numberFormats": {
"currency": {
"style": "currency",
"currency": "IDR",
"currencyDisplay": "symbol"
},
"decimal": {
"style": "decimal",
"minimumFractionDigits": 2,
"maximumFractionDigits": 2
}
}
}
Persisting Language Preference
Copy
// 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
Copy
// 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
Copy
// 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:Copy
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/
└── ...
Copy
// 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
Key Naming Convention
Key Naming Convention
Use hierarchical keys:Example:
Copy
{
"module.section.action": "Translation"
}
inventory.form.saveauth.error.invalidCredentialscommon.button.cancel
Lazy Loading
Lazy Loading
Enable lazy loading untuk mengurangi initial bundle size:
Copy
i18n: {
lazy: true,
langDir: 'locales'
}
Default Locale
Default Locale
Selalu set default locale:
Copy
i18n: {
defaultLocale: 'id',
fallbackLocale: 'en' // Fallback jika key tidak ditemukan
}
Missing Translations
Missing Translations
Handle missing translations gracefully:
Copy
<template>
<!-- Will show key if translation missing -->
{{ $t('some.missing.key') }}
<!-- With fallback -->
{{ $t('some.missing.key', 'Default Text') }}
</template>