Appearance
Internationalization (i18n) ​
Multi-language support with English, Spanish, and Hebrew (RTL).
Basic Usage ​
Always use t() from the useI18n() composable:
vue
<script setup>
const { t } = useI18n()
</script>
<template>
<h1>{{ t('employees.management.title') }}</h1>
<p>{{ t('common.actions.save') }}</p>
</template>The r() Helper (Optional but Recommended) ​
The r() function provides TypeScript autocomplete for translation keys:
typescript
// With r() - autocomplete + validation
const title = t(r('employees.management.title'))
// Without r() - works fine, just no autocomplete
const title = t('employees.management.title')For static keys, r() is optional - just nice for autocomplete.
API Enums: r() is CRITICAL ​
When dynamically building keys from API values, r() validates ALL possible keys exist:
vue
<script setup>
const { t } = useI18n()
// API type (string union)
type AccrualMethod = 'annually' | 'monthly' | 'per_pay_period' | 'hourly'
// ❌ Without r() - no validation
const displayMethod = (method: AccrualMethod) => {
return t(`timeAttendance.accrualMethod.${method}`) // No type checking!
}
// âś… With r() - TypeScript validates ALL keys in the union exist
const displayMethod = (method: AccrualMethod) => {
return t(r(`timeAttendance.accrualMethod.${method}`))
// TypeScript checks:
// - timeAttendance.accrualMethod.annually âś“
// - timeAttendance.accrualMethod.monthly âś“
// - timeAttendance.accrualMethod.per_pay_period âś“
// - timeAttendance.accrualMethod.hourly âś“
}
</script>Why r() is Critical for Dynamic Keys
The Problem: Backend adds/removes/changes an enum value, your translations are out of sync.
Example: Backend adds new value 'weekly' to the AccrualMethod type
typescript
type AccrualMethod = 'annually' | 'monthly' | 'per_pay_period' | 'hourly' | 'weekly'
// ❌ Without r()
t(`timeAttendance.accrualMethod.${method}`)
// Compiles fine, breaks at runtime when method='weekly'
// Shows raw key: "timeAttendance.accrualMethod.weekly"
// âś… With r()
t(r(`timeAttendance.accrualMethod.${method}`))
// TypeScript ERROR: "timeAttendance.accrualMethod.weekly" doesn't exist!
// Forces you to add the translation key before deployingr() validates the entire string union - ensures every possible API value has a translation.
Common Pattern: Mapping API Enums ​
vue
<script setup>
const { t } = useI18n()
// API returns: 'annually', 'monthly', 'per_pay_period', 'hourly'
const accrualMethodKey = computed(() => {
const method = policy.value.accrualMethod
// Option 1: Map to keys with r() for validation
if (method === 'annually') return r('timeAttendance.accrualMethod.annual')
if (method === 'per_pay_period') return r('timeAttendance.accrualMethod.perPeriod')
if (method === 'monthly') return r('timeAttendance.accrualMethod.monthly')
if (method === 'hourly') return r('timeAttendance.accrualMethod.hourly')
// Option 2: Direct construction (if keys match API)
// return r(`timeAttendance.accrualMethod.${method}` as any)
return r('timeAttendance.accrualMethod.monthly') // fallback
})
</script>
<template>
<p>{{ t(accrualMethodKey) }}</p>
</template>Common Patterns ​
Pattern 1: Select Options with API Values ​
vue
<script setup>
const { t } = useI18n()
// API values (English constants)
const GENDER_API_VALUES = {
male: 'Male',
female: 'Female',
other: 'Other'
} as const
// Options for select - use r() to ensure keys exist
const genderOptions = computed(() => [
{ label: t(r('employees.genders.male')), value: GENDER_API_VALUES.male },
{ label: t(r('employees.genders.female')), value: GENDER_API_VALUES.female },
{ label: t(r('employees.genders.other')), value: GENDER_API_VALUES.other }
])
// Display function
const getGenderLabel = (apiValue: string | null) => {
if (!apiValue) return null
switch (apiValue) {
case GENDER_API_VALUES.male: return t(r('employees.genders.male'))
case GENDER_API_VALUES.female: return t(r('employees.genders.female'))
case GENDER_API_VALUES.other: return t(r('employees.genders.other'))
default: return apiValue
}
}
</script>Pattern 2: Reactive Translations ​
Use computed() when translations need to update on language change:
vue
<script setup>
const { t } = useI18n()
// âś… Reactive - updates when language changes
const navigationConfig = computed(() => ({
home: t('navigation.home'),
employees: t('navigation.employees')
}))
// ❌ Not reactive - stuck with initial language
const navigationConfig = {
home: t('navigation.home'),
employees: t('navigation.employees')
}
</script>Pattern 3: Placeholders ​
json
{
"pagination": {
"pageOfPages": "Page {current} of {total}"
}
}vue
<template>
<p>{{ t('common.pagination.pageOfPages', { current: 1, total: 10 }) }}</p>
<!-- Output: "Page 1 of 10" -->
</template>Translation File Structure ​
Files are organized by feature in i18n/locales/{locale}/:
i18n/locales/
en/
├── common.json # Buttons, actions, status
├── employees.json # Employee module
├── auth.json # Authentication
└── index.ts # Aggregates all files
es/ [same structure]
he/ [same structure]Adding New Translations ​
Add the key to all language files:
json
// en/employees.json
{
"management": {
"title": "Employee Management"
}
}
// es/employees.json (Spanish)
{
"management": {
"title": "GestiĂłn de Empleados"
}
}
// he/employees.json (Hebrew)
{
"management": {
"title": "× ×™×”×•×ś עובדים"
}
}Current Languages ​
- English (en) - LTR - Default
- Español (es) - LTR
- עברית (he) - RTL (Right-to-left)
Best Practices ​
Do's âś…
- Always use
computed()for reactive translations - Always use
r()for API enum mappings (prevents sync issues) - Use
r()for autocomplete on other keys (optional but helpful) - Use placeholders
{value}for dynamic content
Don'ts ❌
- Don't use
t()indefineProps()defaults (not reactive) - Don't skip
r()when mapping API enums (breaks on backend changes) - Don't hardcode strings - use translations
- Don't use
left/rightin CSS (usestart/endfor RTL)
Related: RTL Support → | Forms & Validation → | UI Components →