Internationalization (i18n) Migration Guide

This guide explains how to add internationalization to ICN components across Rust, React Native, and web platforms.

Overview

ICN uses different i18n libraries optimized for each platform:

Platform Library Locale Format Location
Rust (CLI, Gateway) rust-i18n YAML <crate>/locales/
React Native react-i18next JSON src/i18n/locales/
Web (Vanilla JS) Custom module JSON locales/

Translation Key Naming Convention

Use dot-notation with consistent hierarchy:

{component}.{section}.{message}

Examples:

  • cli.status.connected - CLI status message
  • nav.overview - Navigation item
  • common.loading - Shared UI string
  • auth.signIn - Authentication action

Variable Interpolation

Platform Syntax Example
Rust {variable} t!("greeting", name = "Alice")
React/JS {{variable}} t('greeting', { name: 'Alice' })

Rust Components

Adding rust-i18n to a Crate

1. Add dependency to Cargo.toml:

[dependencies]
rust-i18n = "3"

2. Initialize in main.rs or lib.rs:

use rust_i18n::t;

// Load translations from ./locales directory, fallback to English
rust_i18n::i18n!("locales", fallback = "en");

3. Create locale file at locales/en.yaml:

# ICN Component Translations - English
# Format: {component}.{section}.{message}
# Variables: {var_name} for interpolation

cli:
  common:
    no_identity: "No identity found. Run 'icnctl id init' to create one."
    error: "Error"
    success: "Success"

  status:
    checking: "Checking daemon status..."
    connected: "Connected to ICN daemon"
    not_running: "Daemon is not running"

Using the t!() Macro

Basic translation:

println!("{}", t!("cli.status.connected"));

With variable interpolation:

// Locale file: greeting: "Welcome, {name}!"
println!("{}", t!("greeting", name = user_name));

Setting locale at runtime:

// Get locale from environment or config
let locale = std::env::var("ICN_LOCALE").unwrap_or_else(|_| "en".into());
rust_i18n::set_locale(&locale);

Example: icnctl CLI

See icn/bins/icnctl/ for a complete example:

// icn/bins/icnctl/src/main.rs
use rust_i18n::t;

rust_i18n::i18n!("locales", fallback = "en");

fn main() {
    // Set locale from environment
    if let Ok(locale) = std::env::var("ICN_LOCALE") {
        rust_i18n::set_locale(&locale);
    }

    println!("{}\n", t!("cli.id.init.starting"));
    // ... rest of implementation
}

React Native

Setting Up react-i18next

1. Install dependencies:

npm install i18next react-i18next expo-localization

2. Create src/i18n/index.ts:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';

// Import locale files
import en from './locales/en.json';

const resources = {
  en: { translation: en },
  // Add more locales here:
  // es: { translation: es },
};

// Get device locale
const getDeviceLocale = (): string => {
  try {
    const deviceLocale = Localization.locale?.split('-')[0] || 'en';
    return deviceLocale in resources ? deviceLocale : 'en';
  } catch {
    return 'en';
  }
};

i18n.use(initReactI18next).init({
  resources,
  lng: getDeviceLocale(),
  fallbackLng: 'en',
  interpolation: {
    escapeValue: false, // React already escapes
  },
  react: {
    useSuspense: false,
  },
});

export default i18n;

3. Create locale file at src/i18n/locales/en.json:

{
  "common": {
    "loading": "Loading...",
    "error": "An error occurred",
    "retry": "Retry",
    "cancel": "Cancel"
  },

  "nav": {
    "home": "Home",
    "payments": "Payments",
    "settings": "Settings"
  },

  "home": {
    "title": "Home",
    "welcome": "Welcome back",
    "balance": {
      "label": "Available Balance",
      "currency": "hours"
    }
  }
}

4. Initialize in app entry point:

// App.tsx or index.ts
import './i18n';

Using the useTranslation Hook

import { useTranslation } from 'react-i18next';

function HomeScreen() {
  const { t } = useTranslation();

  return (
    <View>
      <Text>{t('home.title')}</Text>
      <Text>{t('home.welcome')}</Text>
      <Text>{t('home.balance.label')}: 42 {t('home.balance.currency')}</Text>
    </View>
  );
}

With interpolation:

// Locale: "greeting": "Hello, {{name}}!"
<Text>{t('greeting', { name: 'Alice' })}</Text>

Changing language at runtime:

import i18n from './i18n';

const changeLanguage = async (lang: string) => {
  await i18n.changeLanguage(lang);
};

Web (Vanilla JavaScript)

Using the i18n Module

ICN includes a lightweight, framework-agnostic i18n module for vanilla JavaScript.

1. Import and initialize:

import { initI18n, t, setLocale } from './i18n.js';

// Initialize (call once at app startup)
await initI18n();

2. Create locale file at locales/en.json:

{
  "app": {
    "title": "ICN Timebank",
    "subtitle": "Cooperative Time Banking"
  },

  "nav": {
    "overview": "Overview",
    "network": "Network",
    "ledger": "Ledger"
  },

  "status": {
    "online": "Online",
    "offline": "Offline",
    "connecting": "Connecting..."
  }
}

Using Translations

Basic translation:

const title = t('app.title'); // "ICN Timebank"

With interpolation:

// Locale: "welcome": "Welcome, {{name}}!"
const greeting = t('common.welcome', { name: 'Alice' }); // "Welcome, Alice!"

Changing locale at runtime:

await setLocale('es');

XSS Prevention

When rendering translations to the DOM, always use safe methods:

// SAFE: Use textContent for plain text (recommended)
element.textContent = t('app.title');

// SAFE: Use DOM methods for structured content
const span = document.createElement('span');
span.textContent = t('greeting', { name: userName });
container.appendChild(span);

The i18n module returns raw strings. Locale files are trusted, but interpolated params could contain user input.

Listening for Locale Changes

window.addEventListener('localechange', (event) => {
  console.log(`Locale changed to: ${event.detail.locale}`);
  // Update UI components
});

Adding New Languages

Checklist

For each platform, create the corresponding locale file:

  • Rust: <crate>/locales/<lang>.yaml
  • React Native: src/i18n/locales/<lang>.json
  • Web: locales/<lang>.json

Then register the new locale:

  • Rust: No registration needed (auto-loaded)
  • React Native: Import and add to resources in src/i18n/index.ts
  • Web: Auto-loaded by loadLocale() function

Translation Process

  1. Copy the English locale file as a starting point
  2. Translate all strings while preserving:
    • Key structure (don't rename keys)
    • Variable placeholders ({var} or {{var}})
    • HTML entities if present
  3. Request review from native speakers in the community
  4. Test the translations in the actual UI

Testing Translations

Rust:

ICN_LOCALE=es cargo run -- status

React Native:

import i18n from './i18n';
i18n.changeLanguage('es');

Web:

await setLocale('es');

Language Codes

Use ISO 639-1 two-letter codes:

Language Code
English en
Spanish es
French fr
German de
Portuguese pt
Chinese zh

Best Practices

1. Avoid Concatenating Translated Strings

Word order varies by language.

// BAD: "Hello" + " " + name
t('hello') + ' ' + name;

// GOOD: Use interpolation
t('greeting', { name });

2. Keep Translations Context-Aware

Include context in key names:

# BAD: ambiguous
button: "Submit"

# GOOD: clear context
payment.submitButton: "Send Payment"
proposal.submitButton: "Submit Proposal"

3. Handle Pluralization

Some languages have complex plural rules.

// Use count-based keys
{
  "items": {
    "one": "{{count}} item",
    "other": "{{count}} items"
  }
}

4. Test with Long Strings

German and Finnish often produce longer strings than English. Test UI layout with these languages.

5. Keep Locale Files Organized

Group related strings:

# Group by feature/screen
payments:
  title: "Payments"
  send: "Send Payment"
  receive: "Receive"
  history: "Transaction History"

governance:
  title: "Governance"
  proposals: "Proposals"
  vote: "Vote"

Related Documentation