Aurelia 1 Docs
Website
  • Introduction
  • Introduction
    • Quick Start
    • Aurelia for new developers
    • Contact manager tutorial
  • Components
    • Component basics
    • Component lifecycles
    • Styling components
    • Component options
    • Bindable properties
    • Slotted content
    • Template compilation using processContent
  • Templates
    • Template syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Element content
      • Function references
      • Element references
      • Contextual properties
      • Expression syntax
      • Binding
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form inputs
    • CSS classes and styling
    • List rendering (repeaters)
    • Conditional rendering
    • HTML & SVG attributes
    • The as-element (element aliasing)
    • View Resource pipeline
    • View and Compilation Spies
  • Getting to know Aurelia
    • App configuration and startup
    • Routing
      • Getting started
      • Router configuration
      • Configuring routes
      • Viewports
      • Layouts
      • Redirecting routes
      • View Swapping and Animation
      • Internationalizing Titles
      • Fallback Routes
      • Navigating
      • Lifecycle hooks
      • Router pipelines (hooks)
      • Reusing an Existing View Model
      • Router events
    • Enhance
    • Dynamic composition
    • Observation
    • Dependency injection (DI)
      • Overview
      • Injection
      • Resolvers
    • Task queue
    • Event aggregator
    • Server side rendering
      • Caveats
      • How it works
      • Isomorphic code
      • Memory management
      • Different entry points
      • 404 pages
    • CLI
      • Machine setup
      • Creating a new project
      • Running your app
      • Environments
      • Building your app
      • Generators
      • Configuration with aurelia.json
      • Webpack vs built-in bundler
      • Gulp tasks
      • Webpack
      • Built-in bundler
      • Updating & migrating
      • Recipes & known issues
      • Getting help
  • Developer guides
    • Animation
    • Testing
      • Unit Testing
        • Testing Custom Elements
        • Testing Custom Attributes
        • Testing Routing and Navigation
        • Advanced Testing
        • Helpful Properties, Functions, and Advanced Querying
        • Dependency Injection Testing
        • Testing Custom Value Converters and Binding Behaviors
        • Internationalization (i18n) Testing
      • End-to-end testing
        • Getting started with Playwright
        • Writing E2E tests
        • Advanced Techniques
    • Logging
    • Building plugins
      • Plugin Development
      • Building and Publishing
      • Advanced Plugin Techniques
    • UI Virtualization
    • Integrations
    • Cheat sheet
  • Aurelia packages
    • Validation
    • i18n Internationalization
    • Dialogs
    • Fetch client
    • HTTP client
    • Store
      • Configuring your app
      • What is the State?
      • Reasons for state management
      • Subscribing to the stream of states
      • Subscribing with the connectTo decorator
      • What are actions?
      • Async actions
      • Dispatching actions
      • Resetting the store to a specific state
      • Two-way binding and state management
      • Recording a navigable history of the stream of states
      • Making our app history-aware
      • Navigating through history
      • Handling side-effects with middleware
      • Accessing the original (unmodified) state in a middleware
      • Defining settings for middlewares
      • Error Propagation with Middlewares
      • Default middlewares
      • Tracking overall performance
      • Defining custom devToolsOptions
      • Defining custom loglevels
      • Debugging with the Redux devtools extension
      • Comparison to other state management libraries
Powered by GitBook
On this page
  • Overview
  • Features
  • Requirements
  • Installation
  • Using Aurelia CLI
  • Using Webpack
  • TypeScript Support
  • Setup and Configuration
  • Basic Setup
  • Configuration Options
  • Multiple Namespaces
  • Using Translations
  • Setting and Getting Locales
  • Translation Methods
  • Formatting
  • Number Formatting
  • Date Formatting
  • Relative Time Formatting
  • Error Handling and Debugging
  • Missing Translations
  • Common Issues and Solutions
  • Debugging Tools
  • Build and Bundle Configuration
  • Aurelia CLI Configuration
  • Webpack Configuration
  • API Reference
  • Core Classes
  • Configuration Interfaces
  • Events and Signals
  • Value Converters
  • Backend Options
  • Integrations
  • Using with Aurelia Validation
  • Integration with Aurelia Dialog
  • Custom Backend Integration
  • Real-World Examples
  • Dynamic Form with Translations
  • Dynamic Menu with Language Switching
  • Localized Data Grid
  • Best Practices and Common Patterns
  • Project Organization
  • Translation Tips
  • Performance Considerations
  • Common Patterns
  • Testing

Was this helpful?

Export as PDF
  1. Aurelia packages

i18n Internationalization

Overview

Aurelia's internationalization (i18n) features are provided through the aurelia-i18n plugin, which is built on top of the widely-used i18next library. This combination offers a powerful and flexible solution for adding multilingual support to your Aurelia applications, including translations, number formatting, date formatting, and relative time displays.

Features

  • Text translations with variable substitution

  • Number and date formatting based on locale

  • Relative time formatting

  • Multiple translation namespaces

  • HTML content support in translations

  • Flexible backend options for loading translations

  • TypeScript support

  • Value converters and binding behaviors for declarative usage

Requirements

  • Aurelia application

  • Node.js and NPM

  • Basic understanding of JSON formatting

Installation

Using Aurelia CLI

Install the required packages using npm:

npm install aurelia-i18n i18next

For XHR backend support (optional):

npm install i18next-xhr-backend

Using Webpack

Install the same packages as above:

npm install aurelia-i18n i18next

And optionally:

npm install i18next-xhr-backend

TypeScript Support

If you're using TypeScript, install the necessary type definitions:

npm install -D @types/i18next @types/i18next-xhr-backend

Setup and Configuration

Basic Setup

1. Configure Manual Bootstrapping

Update your index.html to use manual bootstrapping:

<body aurelia-app="main">
  <!-- your content -->
</body>

2. Project Structure

Create the following folder structure in your project:

src/
  ├── locales/
  │   ├── en/
  │   │   └── translation.json
  │   └── de/
  │       └── translation.json

3. Translation File Format

Create your translation files using this structure:

{
  "greeting": "Hello!",
  "welcome": {
    "title": "Welcome to our app",
    "message": "Hello {{name}}, how are you?"
  },
  "items": {
    "one": "{{count}} item",
    "other": "{{count}} items"
  }
}

Configuration Options

Basic Configuration

Configure the plugin in your main.js/ts:

import {I18N, TCustomAttribute} from 'aurelia-i18n';
import Backend from 'i18next-xhr-backend';

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .plugin('aurelia-i18n', instance => {
      let aliases = ['t', 'i18n'];
      TCustomAttribute.configureAliases(aliases);

      return instance.setup({
        backend: {
          loadPath: './locales/{{lng}}/{{ns}}.json',
        },
        attributes: aliases,
        lng: 'en',
        fallbackLng: 'en',
        debug: false
      });
    });

  aurelia.start().then(a => a.setRoot());
}

Using Built-in Aurelia Loader

Alternative configuration using Aurelia's built-in loader:

import {I18N, Backend, TCustomAttribute} from 'aurelia-i18n';

export function configure(aurelia) {
  aurelia.use
    .plugin('aurelia-i18n', instance => {
      let aliases = ['t', 'i18n'];
      TCustomAttribute.configureAliases(aliases);

      instance.i18next.use(Backend.with(aurelia.loader));

      return instance.setup({
        backend: {
          loadPath: './locales/{{lng}}/{{ns}}.json',
        },
        attributes: aliases,
        lng: 'en',
        fallbackLng: 'en',
        debug: false
      });
    });
}

Multiple Namespaces

You can organize translations into multiple namespaces for better organization:

instance.setup({
  backend: {
    loadPath: './locales/{{lng}}/{{ns}}.json',
  },
  ns: ['translation', 'navigation', 'errors'],
  defaultNS: 'translation',
  fallbackLng: 'en',
  debug: false
});

This allows you to structure your translations like this:

locales/
  ├── en/
  │   ├── translation.json
  │   ├── navigation.json
  │   └── errors.json
  └── de/
      ├── translation.json
      ├── navigation.json
      └── errors.json

When using namespaces, reference strings outside the default namespace by prefixing them with the namespace, e.g., navigation:home.title.

Ensure all your translation files are properly formatted JSON and that all namespaces are present in each language folder to avoid runtime errors.

Using Translations

Setting and Getting Locales

Setting the Active Locale

Change the application's language using the setLocale method:

import {I18N} from 'aurelia-i18n';

export class MyViewModel {
  static inject = [I18N];
  
  constructor(i18n) {
    this.i18n = i18n;
    
    this.i18n.setLocale('de-DE').then(() => {
      // Locale has been loaded and set
    });
  }
}

Getting the Active Locale

Retrieve the current locale using getLocale:

constructor(i18n) {
  this.i18n = i18n;
  const currentLocale = this.i18n.getLocale();
}

Translation Methods

Translating via Code

Use the tr method for programmatic translations:

import {I18N} from 'aurelia-i18n';

export class MyViewModel {
  static inject = [I18N];
  
  constructor(i18n) {
    this.i18n = i18n;
    
    // Simple translation
    let translated = this.i18n.tr('myKey');
    
    // With parameters
    let withParams = this.i18n.tr('welcome.message', { name: 'John' });
  }
}

Translating via HTML Attributes

Use the t attribute in your templates:

<!-- Simple translation -->
<span t="title">Title</span>

<!-- With HTML content -->
<div t="[html]content">Content</div>

<!-- Multiple attributes -->
<input t="[placeholder,title]input.placeholder">

<!-- Different keys for different attributes -->
<span t="[text]title;[class]titleClass">Title</span>

Available special attributes:

  • [text]: Sets textContent (default)

  • [html]: Sets innerHTML

  • [append]: Appends translation to existing content

  • [prepend]: Prepends translation to existing content

Using Parameters in Templates

Pass parameters using the t-params attribute:

<span t="welcome.message" t-params.bind="{ name: userName }"></span>

Using Value Converters

The t value converter provides a declarative way to translate content:

<div class="greeting">
  <!-- Simple translation -->
  ${ 'greeting' | t }
  
  <!-- With parameters -->
  ${ 'welcome.message' | t: { name: userName } }
  
  <!-- With plural forms -->
  ${ 'items' | t: { count: itemCount } }
</div>

Using Binding Behaviors

For automatic updates when locale changes, use the t binding behavior:

<div class="greeting">
  ${ 'welcome.message' & t: { name: userName } }
</div>

Working with Images

Images can be translated when different images need to be displayed for different languages:

<!-- Basic image translation -->
<img t="header.logo">

<!-- With default fallback -->
<img data-src="images/default-logo.png" t="header.logo">

Translation file:

{
  "header": {
    "logo": "images/logo-en.png"
  }
}

Complex Parameter Handling

Object Parameters

// Translation file
{
  "validation": {
    "range": "Value must be between {{range.min}} and {{range.max}}"
  }
}

// Usage in code
this.i18n.tr('validation.range', {
  range: {
    min: 1,
    max: 100
  }
});

Context-Based Translations

// Translation file
{
  "greeting": "Hello!",
  "greeting_male": "Hello sir!",
  "greeting_female": "Hello madam!"
}

// Usage
this.i18n.tr('greeting', { context: 'male' });
this.i18n.tr('greeting', { context: 'female' });

Plural Forms

// Translation file
{
  "items": {
    "zero": "No items",
    "one": "One item",
    "few": "A few items",
    "many": "{{count}} items",
    "other": "{{count}} items"
  }
}

// Usage
this.i18n.tr('items', { count: 0 });  // "No items"
this.i18n.tr('items', { count: 1 });  // "One item"
this.i18n.tr('items', { count: 100 }); // "100 items"

Formatting

Number Formatting

Via Code

Use the nf method for programmatic number formatting:

import {I18N} from 'aurelia-i18n';

export class MyViewModel {
  static inject = [I18N];
  
  constructor(i18n) {
    this.i18n = i18n;
    
    // Basic formatting
    let nf = this.i18n.nf();
    let formatted = nf.format(123456.789);
    
    // Currency formatting
    let currencyFormat = this.i18n.nf({ 
      style: 'currency', 
      currency: 'EUR' 
    }, 'de');
    let price = currencyFormat.format(123456.789);
  }
}

Using the Number Value Converter

Format numbers declaratively in templates:

<!-- Default formatting -->
${ 1234567.89 | nf }

<!-- Currency formatting -->
${ 1234567.89 | nf: { style: 'currency', currency: 'EUR' } : 'de' }

<!-- With specific locale -->
${ 1234567.89 | nf: undefined : 'de' }

Date Formatting

Via Code

Use the df method for date formatting:

import {I18N} from 'aurelia-i18n';

export class MyViewModel {
  static inject = [I18N];
  
  constructor(i18n) {
    this.i18n = i18n;
    
    // Basic date formatting
    let df = this.i18n.df();
    let formatted = df.format(new Date());
    
    // Custom formatting
    let options = {
      year: 'numeric',
      month: 'long',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit'
    };
    
    let customDf = this.i18n.df(options, 'de');
    let customFormatted = customDf.format(new Date());
  }
}

Using the Date Value Converter

Format dates in templates:

<!-- Default formatting -->
${ myDate | df }

<!-- Custom formatting -->
${ myDate | df: { 
    year: 'numeric', 
    month: 'long', 
    day: '2-digit' 
  } : 'de' }

Relative Time Formatting

Via Code

Use the RelativeTime service:

import {RelativeTime} from 'aurelia-i18n';

export class MyViewModel {
  static inject = [RelativeTime];
  
  constructor(relativeTime) {
    this.rt = relativeTime;
    
    let pastDate = new Date();
    pastDate.setHours(pastDate.getHours() - 2);
    
    let relative = this.rt.getRelativeTime(pastDate);
    // Output: "2 hours ago"
  }
}

Using the Relative Time Value Converter

Format relative times in templates:

<!-- Simple relative time -->
${ myDate | rt }

Relative time formatting automatically updates when the locale changes.

When using number or date formatting with value converters, ensure your locale binding is properly set up to trigger updates when the locale changes.

Error Handling and Debugging

Missing Translations

// Configuration to handle missing translations
instance.setup({
  saveMissing: true,
  missingKeyHandler: (lng, ns, key, fallbackValue) => {
    console.warn(`Missing translation: ${key} for language: ${lng}`);
    // Optional: Report to error tracking service
  },
  fallbackLng: 'en',
  skipTranslationOnMissingKey: true
});

Common Issues and Solutions

  • Issue: Translations not updating when locale changes

    • Solution: Ensure you're using the binding behavior (&t) instead of value converter (|t)

  • Issue: HTML content displayed as escaped text

    • Solution: Use the [html] attribute: t="[html]myKey"

  • Issue: Parameters not being replaced

    • Solution: Check parameter naming matches exactly in translation files

Debugging Tools

// Enable debug mode
instance.setup({
  debug: true,
  debug: {
    skipOnMissingKey: false,
    sendMissing: true,
    keySeparator: '.',
    load: 'all'
  }
});

Build and Bundle Configuration

Aurelia CLI Configuration

The latest versions of Aurelia CLI include built-in support for loading locale files. If you're using the standard CLI setup, no additional configuration is required.

Webpack Configuration

Using i18next-resource-store-loader

For bundling all translations with your main application:

  1. Install the loader:

npm install i18next-resource-store-loader
  1. Configure your project structure:

src/
  ├── assets/
  │   └── i18n/
  │       ├── index.js
  │       ├── de/
  │       │   └── translation.json
  │       └── en/
  │           └── translation.json
  1. Configure your main.js/ts:

import {PLATFORM} from 'aurelia-pal';
import resBundle from 'i18next-resource-store-loader!./assets/i18n/index.js';

export function configure(aurelia) {
  aurelia.use
    .plugin(PLATFORM.moduleName('aurelia-i18n'), instance => {
      return instance.setup({
        resources: resBundle,
        lng: 'en',
        fallbackLng: 'en',
        debug: false
      });
    });
}

Copying Translation Files

Add to your webpack.config.js to copy translation files:

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  plugins: [
    new CopyWebpackPlugin([
      { from: 'src/locales/', to: 'locales/' }
    ])
  ]
};

API Reference

Core Classes

I18N Class

Properties

  • i18next: The underlying i18next instance

  • ea: EventAggregator instance

  • Intl: Reference to the Intl API

Methods

  • setup(options: I18NConfigOptions): Promise<i18next>

    • Initializes the plugin with the provided options

    • Returns a promise that resolves when initialization is complete

  • setLocale(locale: string): Promise<any>

    • Changes the active locale

    • Returns a promise that resolves when the locale is changed

  • getLocale(): string

    • Returns the current active locale

  • tr(key: string, options?: any): string

    • Translates the given key with optional parameters

    • Returns the translated string

  • nf(options?: Intl.NumberFormatOptions, locale?: string): Intl.NumberFormat

    • Creates a number formatter

    • Returns NumberFormat instance

  • df(options?: Intl.DateTimeFormatOptions, locale?: string): Intl.DateTimeFormat

    • Creates a date formatter

    • Returns DateTimeFormat instance

TCustomAttribute

Static Methods

  • configureAliases(aliases: string[]): void

    • Configures custom aliases for the translation attribute

Properties

  • service: I18N

  • element: Element

  • params: any

Configuration Interfaces

I18NConfigOptions

interface I18NConfigOptions {
  lng?: string;                 // Default language
  fallbackLng?: string;        // Fallback language
  debug?: boolean;             // Enable debug mode
  defaultNS?: string;          // Default namespace
  ns?: string[];              // Available namespaces
  attributes?: string[];      // Custom attribute aliases
  backend?: any;              // Backend configuration
  resources?: any;            // Bundled resources
  skipTranslationOnMissingKey?: boolean; // Keep original content on missing keys
}

Events and Signals

Event Aggregator Events

  • i18n:locale:changed

    • Triggered when locale changes

    • Payload: { oldValue: string, newValue: string }

Binding Signals

  • aurelia-translation-signal

    • Triggers update of all t binding behaviors

  • aurelia-relativetime-signal

    • Triggers update of relative time bindings

Value Converters

TValueConverter

  • Parameters:

    • value: string: Translation key

    • options?: any: Translation options

    • locale?: string: Specific locale (optional)

NfValueConverter

  • Parameters:

    • value: number: Number to format

    • options?: Intl.NumberFormatOptions: Formatting options

    • locale?: string: Specific locale (optional)

DfValueConverter

  • Parameters:

    • value: Date: Date to format

    • options?: Intl.DateTimeFormatOptions: Formatting options

    • locale?: string: Specific locale (optional)

RtValueConverter

  • Parameters:

    • value: Date: Date to format relative to now

Backend Options

XHR Backend

interface XHRBackendOptions {
  loadPath: string;           // Path template for loading translations
  addPath?: string;          // Path template for adding missing keys
  allowMultiLoading?: boolean; // Allow loading multiple namespaces
  parse?: (data: string) => any; // Custom parser function
  crossDomain?: boolean;     // Enable cross-domain requests
  withCredentials?: boolean; // Send cookies in cross-domain requests
}

Aurelia Loader Backend

interface AureliaLoaderOptions {
  loadPath: string;          // Path template for loading translations
  parse?: (data: string) => any; // Custom parser function
}

Some features may require additional configuration depending on your build setup and environment.

All configuration options from i18next are also supported. Refer to i18next documentation for additional options.

Integrations

Using with Aurelia Validation

import {I18N} from 'aurelia-i18n';
import {ValidationMessageProvider} from 'aurelia-validation';

// Configure validation messages to use i18n
ValidationMessageProvider.prototype.getMessage = function(key) {
  const i18n = Container.instance.get(I18N);
  const translation = i18n.tr(`validation.messages.${key}`);
  return this.parser.parse(translation);
};

// Translation file (locales/en/validation.json)
{
  "validation": {
    "messages": {
      "required": "${$displayName} is required",
      "email": "${$displayName} is not a valid email",
      "minLength": "${$displayName} must be at least ${$config.length} characters",
      "maxLength": "${$displayName} cannot be longer than ${$config.length} characters"
    }
  }
}

Integration with Aurelia Dialog

// dialog-service.js
import {I18N} from 'aurelia-i18n';
import {DialogService} from 'aurelia-dialog';

export class LocalizedDialogService {
  static inject = [DialogService, I18N];
  
  constructor(dialogService, i18n) {
    this.dialogService = dialogService;
    this.i18n = i18n;
  }
  
  open(viewModel, model) {
    return this.dialogService.open({
      viewModel,
      model: {
        ...model,
        title: this.i18n.tr(model.titleKey),
        message: this.i18n.tr(model.messageKey, model.messageParams)
      }
    });
  }
}

// Usage
this.localizedDialog.open(ConfirmDialog, {
  titleKey: 'dialogs.confirm.title',
  messageKey: 'dialogs.confirm.deleteUser',
  messageParams: { username: user.name }
});

Custom Backend Integration

// custom-backend.js
class CustomApiBackend {
  constructor(services, options = {}) {
    this.init(services, options);
  }

  init(services, options) {
    this.services = services;
    this.options = options;
  }

  read(language, namespace, callback) {
    fetch(`${this.options.apiUrl}/translations/${language}/${namespace}`)
      .then(response => response.json())
      .then(data => callback(null, data))
      .catch(error => callback(error, null));
  }
}

// main.js
instance.i18next.use(new CustomApiBackend());

Real-World Examples

Dynamic Form with Translations

<!-- form-builder.html -->
<template>
  <form submit.delegate="submit()">
    <div repeat.for="field of formFields">
      <label t.bind="field.labelKey"></label>
      
      <div if.bind="field.type === 'text'">
        <input type="text" 
               value.bind="field.value"
               placeholder.bind="field.placeholderKey | t"
               validation-errors.bind="field.errors">
      </div>
      
      <div if.bind="field.type === 'select'">
        <select value.bind="field.value">
          <option repeat.for="option of field.options"
                  value.bind="option.value">
            ${option.labelKey | t}
          </option>
        </select>
      </div>
      
      <div class="error-messages">
        <span repeat.for="error of field.errors"
              class="error-message">
          ${error.messageKey | t:error.params}
        </span>
      </div>
    </div>
    
    <button type="submit" t="forms.submit"></button>
  </form>
</template>
// form-builder.js
export class FormBuilder {
  static inject = [I18N, ValidationController];
  
  constructor(i18n, validationController) {
    this.i18n = i18n;
    this.validationController = validationController;
    
    this.formFields = [
      {
        type: 'text',
        labelKey: 'forms.user.name.label',
        placeholderKey: 'forms.user.name.placeholder',
        value: '',
        validation: {
          required: true,
          minLength: 3
        }
      },
      {
        type: 'select',
        labelKey: 'forms.user.role.label',
        value: '',
        options: [
          { value: 'admin', labelKey: 'forms.user.role.options.admin' },
          { value: 'user', labelKey: 'forms.user.role.options.user' }
        ]
      }
    ];
  }
}

Dynamic Menu with Language Switching

<!-- nav-menu.html -->
<template>
  <nav class="main-nav">
    <ul>
      <li repeat.for="item of menuItems">
        <a href.bind="item.route" t.bind="item.titleKey"></a>
        
        <ul if.bind="item.children">
          <li repeat.for="child of item.children">
            <a href.bind="child.route" t.bind="child.titleKey"></a>
          </li>
        </ul>
      </li>
    </ul>
    
    <div class="language-selector">
      <select value.bind="currentLanguage" 
              change.delegate="switchLanguage($event.target.value)">
        <option repeat.for="lang of availableLanguages"
                value.bind="lang.code">
          ${lang.name}
        </option>
      </select>
    </div>
  </nav>
</template>
// nav-menu.js
export class NavMenu {
  static inject = [I18N, EventAggregator];
  
  constructor(i18n, ea) {
    this.i18n = i18n;
    this.ea = ea;
    
    this.availableLanguages = [
      { code: 'en', name: 'English' },
      { code: 'de', name: 'Deutsch' },
      { code: 'fr', name: 'Français' }
    ];
    
    this.menuItems = [
      {
        titleKey: 'nav.home',
        route: '#/',
      },
      {
        titleKey: 'nav.admin',
        route: '#/admin',
        children: [
          {
            titleKey: 'nav.admin.users',
            route: '#/admin/users'
          },
          {
            titleKey: 'nav.admin.settings',
            route: '#/admin/settings'
          }
        ]
      }
    ];
    
    this.currentLanguage = this.i18n.getLocale();
    
    this.ea.subscribe('i18n:locale:changed', payload => {
      this.currentLanguage = payload.newValue;
    });
  }
  
  async switchLanguage(newLanguage) {
    await this.i18n.setLocale(newLanguage);
    // Optional: Save preference to user settings
    localStorage.setItem('preferredLanguage', newLanguage);
  }
}

Localized Data Grid

<!-- data-grid.html -->
<template>
  <div class="grid-controls">
    <div class="search">
      <input type="text" 
             value.bind="searchTerm" 
             placeholder.bind="'grid.search' | t">
    </div>
    
    <div class="per-page">
      <select value.bind="itemsPerPage">
        <option repeat.for="size of pageSizes" value.bind="size">
          ${'grid.itemsPerPage' | t: { count: size }}
        </option>
      </select>
    </div>
  </div>

  <table>
    <thead>
      <tr>
        <th repeat.for="column of columns"
            click.delegate="sort(column)">
          ${column.headerKey | t}
          <i if.bind="sortColumn === column.field" 
             class="sort-icon ${sortDirection}">
          </i>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr repeat.for="item of paginatedItems">
        <td repeat.for="column of columns">
          <!-- Handle different column types -->
          <span if.bind="column.type === 'text'">
            ${item[column.field]}
          </span>
          <span if.bind="column.type === 'date'">
            ${item[column.field] | df:column.format}
          </span>
          <span if.bind="column.type === 'number'">
            ${item[column.field] | nf:column.format}
          </span>
          <span if.bind="column.type === 'translated'">
            ${column.prefix + item[column.field] | t}
          </span>
        </td>
      </tr>
    </tbody>
  </table>

  <div class="pagination">
    <button click.delegate="previousPage()" 
            disabled.bind="currentPage === 1">
      ${'grid.previous' | t}
    </button>
    <span>
      ${'grid.pageInfo' | t: { 
        current: currentPage, 
        total: totalPages 
      }}
    </span>
    <button click.delegate="nextPage()" 
            disabled.bind="currentPage === totalPages">
      ${'grid.next' | t}
    </button>
  </div>
</template>

Best Practices and Common Patterns

Project Organization

Recommended Translation Structure

src/
  ├── locales/
  │   ├── en/
  │   │   ├── translation.json     # General translations
  │   │   ├── validation.json      # Form validation messages
  │   │   ├── navigation.json      # Navigation-related texts
  │   │   └── errors.json          # Error messages
  │   └── de/
  │       └── ...

Translation Key Naming Conventions

{
  // Group related translations
  "auth": {
    "login": {
      "title": "Login",
      "emailLabel": "Email Address",
      "passwordLabel": "Password",
      "submitButton": "Sign In",
      "errors": {
        "invalidCredentials": "Invalid email or password"
      }
    }
  },
  
  // Use dots for deeply nested structures
  "common.buttons.save": "Save",
  "common.buttons.cancel": "Cancel"
}

Translation Tips

Keep Translations Maintainable

// Bad - Hard to maintain and reuse
{
  "welcomeMessage": "Welcome to our app! Click here to get started."
}

// Good - Separate reusable components
{
  "welcome": {
    "greeting": "Welcome to our app!",
    "callToAction": "Click here to get started"
  }
}

Handle Pluralization Properly

{
  "items": {
    "zero": "No items",
    "one": "{{count}} item",
    "other": "{{count}} items"
  }
}

Use Parameters Effectively

// Bad - Hard-coded values
{
  "lastLogin": "You last logged in on Monday at 2:00 PM"
}

// Good - Flexible parameters
{
  "lastLogin": "You last logged in on {{day}} at {{time}}"
}

Performance Considerations

Lazy Loading

  • Load translations by namespace when needed

  • Consider splitting large translation files

  • Use route-based translation loading for large applications

// Configure lazy loading
instance.setup({
  ns: ['common'],
  partialBundledLanguages: true,
  load: 'languageOnly'
});

// Load additional namespace when needed
i18next.loadNamespaces(['specific-feature'])

Caching

  • Enable localStorage caching for faster subsequent loads

  • Configure cache expiration based on your update frequency

instance.setup({
  cache: {
    enabled: true,
    expirationTime: 7 * 24 * 60 * 60 * 1000 // 7 days
  }
});

Common Patterns

Handling Dynamic Content

<!-- Using interpolation -->
<div t="[html]content.description" t-params.bind="{ 
  link: '<a href=\'https://example.com\'>Click here</a>' 
}"></div>

<!-- In translation file -->
{
  "content": {
    "description": "Learn more about our service. {{link}}"
  }
}

Form Validation Messages

{
  "validation": {
    "required": "{{field}} is required",
    "minLength": "{{field}} must be at least {{min}} characters",
    "email": "Please enter a valid email address"
  }
}

Error Handling

// Handle missing translations gracefully
instance.setup({
  fallbackLng: 'en',
  saveMissing: true,
  missingKeyHandler: (lng, ns, key) => {
    console.warn(`Missing translation: ${key} in ${lng}/${ns}`);
    // Optional: Report to error tracking service
  }
});

Testing

Translation Key Coverage

// Test helper to verify translation key existence
function verifyTranslationKey(key, locale = 'en') {
  const translation = i18n.tr(key);
  expect(translation).not.toBe(key); // Should not return key itself
  expect(translation.includes('{{}')).toBe(false); // No unprocessed parameters
}

Automated Checks

  • Verify all locales have the same keys

  • Check for missing translations

  • Validate parameter usage

Consider implementing a CI check to ensure translation files are properly formatted and complete.

Always test your applications with different locales to catch any internationalization-related issues early.

PreviousValidationNextDialogs

Last updated 5 months ago

Was this helpful?