Skip to content

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() 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 deploying

r() 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() in defineProps() 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/right in CSS (use start/end for RTL)

Related: RTL Support → | Forms & Validation → | UI Components →