Skip to content

Internationalization (i18n) API Reference

This document covers stx's internationalization system for building multilingual applications.

Overview

stx provides a comprehensive i18n system that:

  • Supports multiple translation file formats (JSON, YAML, JS)
  • Provides @translate directive for inline translations
  • Supports nested translation keys with dot notation
  • Includes parameter substitution
  • Caches translations for performance
  • Falls back to default locale when translations are missing

Configuration

Basic Setup

typescript
// stx.config.ts
export default {
  i18n: {
    defaultLocale: 'en',
    locale: 'en',
    translationsDir: 'translations',
    format: 'yaml', // 'json', 'yaml', 'yml', or 'js'
    fallbackToKey: true,
    cache: true
  }
}

Configuration Options

OptionTypeDefaultDescription
defaultLocalestring'en'Default fallback locale
localestring'en'Current active locale
translationsDirstring'translations'Translation files directory
formatstring'yaml'File format (json, yaml, yml, js)
fallbackToKeybooleantrueReturn key if translation not found
cachebooleantrueCache translation files

Translation Files

Directory Structure

translations/
├── en.yaml        # English translations
├── es.yaml        # Spanish translations
├── fr.yaml        # French translations
├── de.yaml        # German translations
└── ja.yaml        # Japanese translations

YAML Format

yaml
# translations/en.yaml
welcome: "Welcome"
hello: "Hello, :name!"

nav:
  home: "Home"
  about: "About"
  contact: "Contact"

messages:
  success: "Operation completed successfully"
  error: "An error occurred"
  loading: "Loading..."

user:
  profile:
    title: "User Profile"
    edit: "Edit Profile"
  settings:
    title: "Settings"
    language: "Language"

JSON Format

json
{
  "welcome": "Welcome",
  "hello": "Hello, :name!",
  "nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "messages": {
    "success": "Operation completed successfully",
    "error": "An error occurred",
    "loading": "Loading..."
  }
}

JavaScript Format

javascript
// translations/en.js
export default {
  welcome: "Welcome",
  hello: (name) => `Hello, ${name}!`,
  nav: {
    home: "Home",
    about: "About",
    contact: "Contact"
  },
  messages: {
    success: "Operation completed successfully",
    error: "An error occurred",
    loading: "Loading..."
  }
}

@translate Directive

Basic Usage

html
<!-- Simple translation -->
<h1>@translate('welcome')</h1>
<!-- Output: Welcome -->

<!-- Nested keys with dot notation -->
<nav>
  <a href="/">@translate('nav.home')</a>
  <a href="/about">@translate('nav.about')</a>
  <a href="/contact">@translate('nav.contact')</a>
</nav>

With Parameters

html
<!-- Parameter substitution -->
<p>@translate('hello', {name: 'Alice'})</p>
<!-- Output: Hello, Alice! -->

<p>@translate('user.welcome', {name: user.name, role: user.role})</p>
<!-- Output: Welcome back, Alice! You are logged in as Admin. -->

Translation file:

yaml
# translations/en.yaml
hello: "Hello, :name!"
user:
  welcome: "Welcome back, :name! You are logged in as :role."

Block Syntax

html
<!-- Translation with fallback content -->
@translate('custom.message')
  This is the default content if translation is not found
@endtranslate

Using Template Context

html
<script>
  module.exports = {
    userName: "Bob",
    itemCount: 5
  };
</script>

<p>@translate('greeting', {name: userName})</p>
<p>@translate('items.count', {count: itemCount})</p>

Programmatic API

loadTranslation()

Load a translation file for a specific locale.

typescript
import { loadTranslation } from '@stacksjs/stx/i18n'

const translations = await loadTranslation('en', {
  i18n: {
    translationsDir: 'translations',
    format: 'yaml',
    cache: true
  }
})

console.log(translations)
// { welcome: "Welcome", hello: "Hello, :name!", ... }

Parameters:

  • locale (string): Locale code (e.g., 'en', 'es', 'fr')
  • options (StxOptions): Configuration options

Returns: Promise<Record<string, any>> - Translation object

getTranslation()

Get a specific translation by key.

typescript
import { getTranslation } from '@stacksjs/stx/i18n'

const translations = {
  hello: "Hello, :name!",
  nav: {
    home: "Home"
  }
}

// Simple key
const hello = getTranslation('hello', translations, true, { name: 'Alice' })
console.log(hello) // "Hello, Alice!"

// Nested key
const navHome = getTranslation('nav.home', translations)
console.log(navHome) // "Home"

// Missing key with fallback
const missing = getTranslation('missing.key', translations, true)
console.log(missing) // "missing.key"

// Missing key without fallback
const noFallback = getTranslation('missing.key', translations, false)
console.log(noFallback) // ""

Parameters:

  • key (string): Translation key (supports dot notation)
  • translations (object): Translations object
  • fallbackToKey (boolean): Return key if not found
  • params (object): Parameters for substitution

Returns: string - Translated string

processTranslateDirective()

Process all @translate directives in a template.

typescript
import { processTranslateDirective } from '@stacksjs/stx/i18n'

const template = `
  <h1>@translate('welcome')</h1>
  <p>@translate('hello', {name: 'Alice'})</p>
`

const result = await processTranslateDirective(
  template,
  {},
  'template.stx',
  {
    i18n: {
      locale: 'en',
      translationsDir: 'translations'
    }
  }
)

console.log(result)

Dynamic Locale Switching

Runtime Locale Change

html
<script>
  module.exports = {
    currentLocale: 'en'
  };
</script>

<!-- Update config based on context -->
<div lang="{{ currentLocale }}">
  @translate('welcome')
</div>

URL-based Locale

html
<script>
  // Extract locale from URL
  const url = new URL(Bun.env.REQUEST_URL || 'http://localhost');
  const locale = url.searchParams.get('lang') || 'en';

  module.exports = {
    locale
  };
</script>

<!-- Configure locale from context -->
@translate('hello', {name: 'User'})
html
<script>
  // Get locale from cookie
  const cookies = parseCookies(Bun.env.HTTP_COOKIE || '');
  const locale = cookies.locale || 'en';

  module.exports = {
    locale
  };
</script>

Advanced Features

Pluralization

yaml
# translations/en.yaml
items:
  zero: "No items"
  one: ":count item"
  other: ":count items"
html
<script>
  function pluralize(key, count, translations) {
    const plural = count === 0 ? 'zero' : count === 1 ? 'one' : 'other';
    const translation = translations[key][plural] || translations[key].other;
    return translation.replace(':count', count);
  }

  module.exports = {
    itemCount: 5,
    pluralize
  };
</script>

<p>{{ pluralize('items', itemCount, __translations) }}</p>
<!-- Output: 5 items -->

Context-based Translations

yaml
# translations/en.yaml
button:
  submit:
    default: "Submit"
    loading: "Submitting..."
    success: "Submitted!"
html
<script>
  module.exports = {
    buttonState: 'loading'
  };
</script>

@translate(`button.submit.${buttonState}`)

Formatted Messages

yaml
# translations/en.yaml
date:
  full: ":day :month :year"
  short: ":month/:day/:year"
html
<script>
  module.exports = {
    day: 8,
    month: 10,
    year: 2025
  };
</script>

<p>@translate('date.full', {day: day, month: month, year: year})</p>
<!-- Output: 8 10 2025 -->

Best Practices

1. Organize Translations by Feature

yaml
# Good structure
auth:
  login:
    title: "Login"
    email: "Email"
    password: "Password"
  register:
    title: "Register"
    username: "Username"

# Avoid flat structure
login_title: "Login"
login_email: "Email"

2. Use Meaningful Keys

yaml
# Good
user.profile.edit.button: "Edit Profile"

# Bad
btn1: "Edit Profile"

3. Keep Translations DRY

yaml
# Use references where appropriate
common:
  save: "Save"
  cancel: "Cancel"

form:
  save: # Reference common.save
  cancel: # Reference common.cancel

4. Provide Context in Keys

yaml
# Good - Clear context
navigation.primary.home: "Home"
button.submit.loading: "Loading..."

# Bad - Ambiguous
home: "Home"
loading: "Loading..."

Examples

Multi-language Navigation

html
<script>
  module.exports = {
    locale: 'en' // or 'es', 'fr', etc.
  };
</script>

<nav>
  <a href="/?lang=en">English</a>
  <a href="/?lang=es">Español</a>
  <a href="/?lang=fr">Français</a>
</nav>

<ul>
  <li><a href="/">@translate('nav.home')</a></li>
  <li><a href="/about">@translate('nav.about')</a></li>
  <li><a href="/services">@translate('nav.services')</a></li>
  <li><a href="/contact">@translate('nav.contact')</a></li>
</ul>

translations/en.yaml:

yaml
nav:
  home: "Home"
  about: "About Us"
  services: "Services"
  contact: "Contact"

translations/es.yaml:

yaml
nav:
  home: "Inicio"
  about: "Sobre Nosotros"
  services: "Servicios"
  contact: "Contacto"

Form with Translations

html
<form>
  <h2>@translate('form.contact.title')</h2>

  <label>
    @translate('form.fields.name')
    <input type="text" name="name" placeholder="@translate('form.placeholders.name')">
  </label>

  <label>
    @translate('form.fields.email')
    <input type="email" name="email" placeholder="@translate('form.placeholders.email')">
  </label>

  <label>
    @translate('form.fields.message')
    <textarea name="message" placeholder="@translate('form.placeholders.message')"></textarea>
  </label>

  <button type="submit">
    @translate('form.buttons.submit')
  </button>
</form>

Dynamic User Messages

html
<script>
  module.exports = {
    user: {
      name: "Alice",
      lastLogin: "2025-10-08",
      unreadCount: 3
    }
  };
</script>

<div class="user-dashboard">
  <h1>@translate('dashboard.welcome', {name: user.name})</h1>
  <p>@translate('dashboard.lastLogin', {date: user.lastLogin})</p>

  @if(user.unreadCount > 0)
    <div class="notification">
      @translate('dashboard.unreadMessages', {count: user.unreadCount})
    </div>
  @endif
</div>

translations/en.yaml:

yaml
dashboard:
  welcome: "Welcome back, :name!"
  lastLogin: "Last login: :date"
  unreadMessages: "You have :count unread messages"

Cache Management

Translation Cache

typescript
import { translationsCache } from '@stacksjs/stx/i18n'

// Manually clear cache for a locale
delete translationsCache['en']

// Clear all cached translations
Object.keys(translationsCache).forEach(key => {
  delete translationsCache[key]
})

Cache Warming

Pre-load translations during application startup:

typescript
import { loadTranslation } from '@stacksjs/stx/i18n'

const locales = ['en', 'es', 'fr', 'de']

// Warm up cache
for (const locale of locales) {
  await loadTranslation(locale, {
    i18n: {
      translationsDir: 'translations',
      cache: true
    }
  })
}

Error Handling

Missing Translation Files

When a translation file is not found:

  • Falls back to default locale
  • Returns empty object if default locale also missing
  • Logs error in debug mode

Missing Translation Keys

When a translation key is not found:

  • Returns the key itself if fallbackToKey: true
  • Returns empty string if fallbackToKey: false
  • Does not throw errors

Debug Mode

Enable debug mode to see detailed i18n logs:

typescript
// stx.config.ts
export default {
  debug: true,
  i18n: {
    locale: 'en',
    translationsDir: 'translations'
  }
}

See Also

Released under the MIT License.