Internationalization (i18n)

On this page 41

stx provides comprehensive internationalization support with translation loading, pluralization, ICU MessageFormat, and lazy loading for optimal performance.

Quick Start

// 1. Create translation files
// translations/en.json
{
  "greeting": "Hello, :name!",
  "items": "One item|:count items"
}

// translations/es.json
{
  "greeting": "¡Hola, :name!",
  "items": "Un artículo|:count artículos"
}

// 2. Configure i18n in stx.config.ts
export default {
  i18n: {
    defaultLocale: 'en',
    locale: 'en',
    translationsDir: 'translations',
    format: 'json'
  }
}

// 3. Use in templates
@translate('greeting', { name: 'John' })

Configuration

Basic Configuration

// stx.config.ts
export default {
  i18n: {
    // Default locale (fallback)
    defaultLocale: 'en',

    // Current locale
    locale: 'en',

    // Directory containing translation files
    translationsDir: 'translations',

    // File format: 'json', 'yaml', 'yml', or 'js'
    format: 'json',

    // Return key if translation not found
    fallbackToKey: true,

    // Enable caching
    cache: true,

    // Cache max age in milliseconds (optional)
    cacheMaxAge: 3600000  // 1 hour
  }
}

Translation File Formats

JSON

// translations/en.json
{
  "welcome": "Welcome to our app",
  "messages": {
    "success": "Operation completed successfully",
    "error": "An error occurred"
  }
}

YAML

# translations/en.yaml
welcome: Welcome to our app
messages:
  success: Operation completed successfully
  error: An error occurred

JavaScript

// translations/en.js
export default {
  welcome: 'Welcome to our app',
  messages: {
    success: 'Operation completed successfully',
    error: 'An error occurred'
  }
}

Template Directives

@translate

The main directive for translations:

<!-- Simple translation -->
<h1>@translate('welcome')</h1>

<!-- With parameters -->
<p>@translate('greeting', { name: 'John' })</p>

<!-- Nested keys -->
<p>@translate('messages.success')</p>

<!-- Block syntax with fallback -->
@translate('optional.key')
  Default content if key not found
@endtranslate

Expression Syntax

Use translations in expressions:

<p>{{ __('greeting', { name: user.name }) }}</p>

<!-- With filters -->
<p>{{ 'welcome' | translate }}</p>

Parameter Replacement

Use :parameter syntax for dynamic values:

{
  "greeting": "Hello, :name!",
  "order_status": "Your order #:orderId is :status",
  "welcome_back": "Welcome back, :user! You have :count unread messages."
}
@translate('greeting', { name: 'Alice' })
<!-- Output: Hello, Alice! -->

@translate('order_status', { orderId: '12345', status: 'shipped' })
<!-- Output: Your order #12345 is shipped -->

@translate('welcome_back', { user: 'Bob', count: 5 })
<!-- Output: Welcome back, Bob! You have 5 unread messages. -->

Pluralization

Simple Pluralization

Use the | separator for singular/plural forms:

{
  "apples": "One apple|:count apples",
  "items": "No items|One item|:count items"
}
@translate('apples', { count: 1 })
<!-- Output: One apple -->

@translate('apples', { count: 5 })
<!-- Output: 5 apples -->

@translate('items', { count: 0 })
<!-- Output: No items -->

Advanced Pluralization

Use exact matches and ranges:

{
  "progress": "{0} Not started|{1} Just begun|[2,5] Making progress|[6,*] Almost there!"
}
  • {n} - Exact match for count n
  • [min,max] - Range (inclusive)
  • [n,*] - From n to infinity
@translate('progress', { count: 0 })  <!-- Not started -->
@translate('progress', { count: 1 })  <!-- Just begun -->
@translate('progress', { count: 3 })  <!-- Making progress -->
@translate('progress', { count: 10 }) <!-- Almost there! -->

ICU MessageFormat

stx supports ICU MessageFormat for complex translations:

Plural

{
  "cart": "{count, plural, =0{Your cart is empty} one{# item in cart} other{# items in cart}}"
}
@translate('cart', { count: 0 })  <!-- Your cart is empty -->
@translate('cart', { count: 1 })  <!-- 1 item in cart -->
@translate('cart', { count: 5 })  <!-- 5 items in cart -->

Select

{
  "pronoun": "{gender, select, male{He} female{She} other{They}} liked your post"
}
@translate('pronoun', { gender: 'female' })
<!-- Output: She liked your post -->

Select Ordinal

{
  "ranking": "You finished {pos, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}"
}
@translate('ranking', { pos: 1 })   <!-- You finished 1st -->
@translate('ranking', { pos: 2 })   <!-- You finished 2nd -->
@translate('ranking', { pos: 3 })   <!-- You finished 3rd -->
@translate('ranking', { pos: 4 })   <!-- You finished 4th -->
@translate('ranking', { pos: 21 })  <!-- You finished 21st -->

Number Formatting

{
  "price": "Total: {amount, number, currency}",
  "percentage": "{value, number, percent} complete"
}

Date/Time Formatting

{
  "posted": "Posted on {date, date, long}",
  "event": "Event starts at {time, time, short}"
}

Lazy Loading

stx supports lazy loading translations for better performance, especially with many locales.

Preloading

Load translations before they're needed:

import { preloadTranslations, preloadTranslationsBackground } from 'stx'

// Preload specific locales (blocking)
await preloadTranslations({
  locales: ['en', 'es', 'fr', 'de'],
  parallel: true,  // Load all at once
  onLocaleLoaded: (locale) => console.log(`${locale} ready`),
  onComplete: () => console.log('All translations loaded'),
  onError: (locale, error) => console.error(`Failed to load ${locale}`)
}, options)

// Preload in background (non-blocking)
preloadTranslationsBackground({
  locales: ['en', 'es', 'fr'],
  parallel: true
}, options)
// Execution continues immediately

Priority Loading

Load important locales first:

await preloadTranslations({
  locales: ['en', 'es', 'fr', 'de', 'ja', 'zh'],
  parallel: false,  // Sequential loading
  priority: {
    'en': 100,  // Highest priority
    'es': 90,
    'fr': 80
    // Others load after
  }
}, options)

Loading State

Check and wait for translations:

import {
  isLoaded,
  isLoading,
  getLoadingState,
  waitForLocale,
  getLoadingLocales
} from 'stx'

// Check if locale is ready
if (isLoaded('es')) {
  // Use Spanish translations
}

// Check loading state
const state = getLoadingState('fr')  // 'idle' | 'loading' | 'loaded' | 'error'

// Wait for a locale with timeout
try {
  await waitForLocale('de', 5000)  // Wait up to 5 seconds
} catch (error) {
  // Handle timeout or loading error
}

// Get all currently loading locales
const loading = getLoadingLocales()  // ['fr', 'ja']

Synchronous Access

Get translations without waiting (returns undefined if not loaded):

import { getTranslationSync, getTranslationAsync } from 'stx'

// Non-blocking - returns undefined if not cached
const translations = getTranslationSync('en')
if (translations) {
  // Use translations
} else {
  // Show loading state
}

// Async with automatic loading
const translations = await getTranslationAsync('en', options)
// Always returns translations (loads if needed)

Request Deduplication

Multiple simultaneous requests for the same locale share one load:

import { loadTranslationLazy } from 'stx'

// These share the same underlying request
const [t1, t2, t3] = await Promise.all([
  loadTranslationLazy('en', options),
  loadTranslationLazy('en', options),
  loadTranslationLazy('en', options)
])
// t1 = t2 = t3 (same object)

Cache Management

import {
  clearTranslationCache,
  getTranslationCacheStats
} from 'stx'

// Clear all cached translations
clearTranslationCache()

// Clear specific locale
clearTranslationCache('en')

// Get cache statistics
const stats = getTranslationCacheStats()
console.log(`Cached locales: ${stats.locales.join(', ')}`)
console.log(`Total entries: ${stats.size}`)

stats.entries.forEach(entry => {
  console.log(`${entry.locale}: ${entry.keyCount} keys, loaded at ${entry.loadedAt}`)
})

Programmatic Usage

Loading Translations

import { loadTranslation, getTranslation } from 'stx'

// Load translations for a locale
const translations = await loadTranslation('en', options)

// Get a specific translation
const greeting = getTranslation('greeting', translations, true, { name: 'World' })

Creating Translation Filters

import { createTranslateFilter } from 'stx'

const translations = await loadTranslation('en', options)
const t = createTranslateFilter(translations)

const greeting = t('greeting', { name: 'World' })

ICU Format Detection

import { isICUFormat, formatICU } from 'stx'

const message = '{count, plural, one{# item} other{# items}}'

if (isICUFormat(message)) {
  const result = formatICU(message, { count: 5 })
  // "5 items"
}

Best Practices

1. Organize Translation Keys

Use dot notation for nested structure:

{
  "nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "auth": {
    "login": "Log In",
    "logout": "Log Out",
    "register": "Sign Up"
  },
  "errors": {
    "404": "Page not found",
    "500": "Server error"
  }
}

2. Preload Common Locales

// In your app initialization
preloadTranslationsBackground({
  locales: ['en', 'es'],  // Most common locales
  parallel: true
}, options)

3. Handle Missing Translations

<!-- Use fallback content -->
@translate('new.feature.description')
  This exciting new feature helps you...
@endtranslate

4. Use ICU for Complex Logic

ICU MessageFormat handles edge cases better:

{
  "results": "{count, plural, =0{No results found} =1{1 result found} other{# results found}}"
}

5. Cache in Production

configureDatabase({
  i18n: {
    cache: true,
    cacheMaxAge: 3600000  // 1 hour
  }
})

Locale Switching

import { loadTranslation } from 'stx'

async function setLocale(locale: string) {
  // Update config
  options.i18n.locale = locale

  // Preload new locale
  await loadTranslation(locale, options)

  // Re-render your app
}

Examples

Complete Translation File

{
  "app": {
    "name": "My Application",
    "tagline": "Making things easier"
  },
  "nav": {
    "home": "Home",
    "dashboard": "Dashboard",
    "settings": "Settings"
  },
  "auth": {
    "welcome": "Welcome, :name!",
    "login": "Log In",
    "logout": "Log Out"
  },
  "notifications": "{count, plural, =0{No notifications} one{# notification} other{# notifications}}",
  "cart": {
    "empty": "Your cart is empty",
    "items": "{count, plural, one{# item} other{# items}} in cart",
    "total": "Total: :amount"
  },
  "errors": {
    "required": "This field is required",
    "email": "Please enter a valid email",
    "min": "Must be at least :min characters"
  }
}

Template Usage

<header>
  <h1>@translate('app.name')</h1>
  <nav>
    <a href="/">@translate('nav.home')</a>
    <a href="/dashboard">@translate('nav.dashboard')</a>
  </nav>
  @if(user)
    <span>@translate('auth.welcome', { name: user.name })</span>
    <span>@translate('notifications', { count: user.notificationCount })</span>
  @endif
</header>

<section class="cart">
  @if(cart.isEmpty)
    <p>@translate('cart.empty')</p>
  @else
    <p>@translate('cart.items', { count: cart.itemCount })</p>
    <p>@translate('cart.total', { amount: cart.formattedTotal })</p>
  @endif
</section>