Skip to content

Internationalization (i18n)

Castella provides a built-in internationalization system for multi-language support with runtime locale switching.

Overview

The i18n system consists of:

  • I18nManager - Singleton manager for translations and locale switching
  • LocaleString - Reactive string that auto-updates UI when locale changes
  • t() / tn() - Convenience functions for translation
  • Loaders - Load translations from YAML or JSON files

Basic Usage

from castella.i18n import I18nManager, load_yaml_catalog

# Get the singleton manager
manager = I18nManager()

# Load translation catalogs
manager.load_catalog("en", load_yaml_catalog("locales/en.yaml"))
manager.load_catalog("ja", load_yaml_catalog("locales/ja.yaml"))

# Set the active locale
manager.set_locale("en")

# Translate a key
text = manager.t("greeting")  # "Hello!"

# With string interpolation
text = manager.t("welcome", name="World")  # "Hello, World!"

I18nManager Methods

Method Description
load_catalog(locale, catalog) Load a translation catalog for a locale
set_locale(locale) Set the active locale
set_fallback_locale(locale) Set fallback locale for missing translations
t(key, **kwargs) Translate a key with optional interpolation
tn(key, count, **kwargs) Translate with pluralization
locale Property: current locale
available_locales Property: list of loaded locales

Loading Translations

From YAML Files

from castella.i18n import load_yaml_catalog

catalog = load_yaml_catalog("locales/en.yaml")
manager.load_catalog("en", catalog)

From JSON Files

from castella.i18n import load_json_catalog

catalog = load_json_catalog("locales/en.json")
manager.load_catalog("en", catalog)

From a Directory

from castella.i18n import load_catalogs_from_directory

# Load all .yaml and .json files from a directory
catalogs = load_catalogs_from_directory("locales/")
for locale, catalog in catalogs.items():
    manager.load_catalog(locale, catalog)

Programmatically

from castella.i18n import create_catalog_from_dict

catalog = create_catalog_from_dict("en", {
    "greeting": "Hello!",
    "button": {
        "save": "Save",
        "cancel": "Cancel",
    },
})
manager.load_catalog("en", catalog)

LocaleString (Reactive)

LocaleString automatically updates the UI when the locale changes. Use it with widgets for reactive translations.

from castella import Text
from castella.i18n import LocaleString

# UI automatically updates when locale changes
Text(LocaleString("dashboard.title"))

# With interpolation
Text(LocaleString("greeting", name="World"))

How It Works

  1. LocaleString observes the I18nManager for locale changes
  2. When locale changes, LocaleString notifies attached widgets
  3. Widgets re-render with the new translation

Comparison: t() vs LocaleString

Use Case Function
Static translation (one-time) t("key")
Reactive translation (auto-update UI) LocaleString("key")
# Static - won't update when locale changes
label = t("greeting")

# Reactive - updates automatically
Text(LocaleString("greeting"))

Pluralization

Handle pluralized strings that vary based on count.

Using tn()

from castella.i18n import tn

# Returns "1 item" or "5 items" based on count and locale
text = tn("items", count=1)  # "1 item"
text = tn("items", count=5)  # "5 items"

Using LocalePluralString

from castella import Text
from castella.i18n import LocalePluralString

# Reactive pluralized string
Text(LocalePluralString("items", count=5))

Defining Plural Forms in YAML

# locales/en.yaml
locale: en
items:
  one: "{count} item"
  other: "{count} items"
# locales/ja.yaml
locale: ja
items:
  other: "{count}個のアイテム"  # Japanese doesn't distinguish singular/plural

Supported Plural Categories

Castella follows CLDR plural rules:

Category Description Example Languages
zero Zero quantity Arabic
one Singular English, German, French
two Dual Arabic, Welsh
few Few Russian, Polish
many Many Russian, Polish
other Default/Plural All languages

Built-in rules for: English, Japanese, Chinese, French, German, Russian.

Translation File Format

YAML Format

# locales/en.yaml
locale: en

# Simple strings
greeting: "Hello!"
farewell: "Goodbye!"

# Nested keys (accessed as "button.save")
button:
  save: "Save"
  cancel: "Cancel"
  delete: "Delete"

# String interpolation
welcome: "Welcome, {name}!"

# Plural forms
items:
  one: "{count} item"
  other: "{count} items"

notifications:
  one: "You have {count} notification"
  other: "You have {count} notifications"

JSON Format

{
  "locale": "en",
  "greeting": "Hello!",
  "button": {
    "save": "Save",
    "cancel": "Cancel"
  },
  "welcome": "Welcome, {name}!",
  "items": {
    "one": "{count} item",
    "other": "{count} items"
  }
}

Accessing Nested Keys

Use dot notation to access nested translations:

manager.t("button.save")        # "Save"
manager.t("button.cancel")      # "Cancel"

System Locale Detection

I18nManager can auto-detect the system locale:

  1. CASTELLA_LOCALE environment variable (highest priority)
  2. System locale (LC_ALL, LANG)
  3. Default: "en"
# Force Japanese locale
export CASTELLA_LOCALE=ja

Complete Example

from castella import App, Button, Column, Component, Row, Text
from castella.frame import Frame
from castella.i18n import I18nManager, LocaleString, load_yaml_catalog


class I18nDemo(Component):
    def __init__(self):
        super().__init__()
        self._manager = I18nManager()

        # Load translations
        self._manager.load_catalog("en", load_yaml_catalog("locales/en.yaml"))
        self._manager.load_catalog("ja", load_yaml_catalog("locales/ja.yaml"))
        self._manager.set_locale("en")

    def view(self):
        return Column(
            # Reactive translation - auto-updates on locale change
            Text(LocaleString("greeting")),
            Text(LocaleString("welcome", name="Castella")),

            Row(
                Button(LocaleString("button.save")).on_click(self._save),
                Button(LocaleString("button.cancel")),
            ),

            # Language switcher
            Row(
                Button("English").on_click(lambda _: self._manager.set_locale("en")),
                Button("日本語").on_click(lambda _: self._manager.set_locale("ja")),
            ),
        )

    def _save(self, _):
        print("Saved!")


App(Frame("I18n Demo", 500, 300), I18nDemo()).run()

locales/en.yaml:

locale: en
greeting: "Hello!"
welcome: "Welcome, {name}!"
button:
  save: "Save"
  cancel: "Cancel"

locales/ja.yaml:

locale: ja
greeting: "こんにちは!"
welcome: "ようこそ、{name}さん!"
button:
  save: "保存"
  cancel: "キャンセル"