Theme
Version
GitHub PyPI Discord
On this page

Format values

A formatter turns a canonical application value into text for one locale. The same amount may use different digits, decimal separators, currency placement, or date order in different locales.

Citry keeps those choices in named profiles. Application code asks for a name such as account-balance; it does not repeat low-level formatter options at every call site.

Define a format registry

Pass one FormatRegistry in the i18n engine settings:

from citry import (
    Citry,
    CurrencyFormat,
    DateFormat,
    DateTimeFormat,
    FormatRegistry,
    ListFormat,
    NumberFormat,
    PercentFormat,
    RelativeTimeFormat,
    TimeFormat,
    UnitFormat,
)

formats = FormatRegistry(
    number={
        "measurement": NumberFormat(),
    },
    percent={
        "completion": PercentFormat(),
    },
    currency={
        "account-balance": CurrencyFormat(),
    },
    date={
        "invoice-date": DateFormat(length="long"),
    },
    time={
        "appointment-time": TimeFormat(length="short"),
    },
    datetime={
        "appointment": DateTimeFormat(
            length="medium",
            time_zone_name="short",
        ),
    },
    relative_time={
        "activity-age": RelativeTimeFormat(unit="day"),
    },
    list={
        "people": ListFormat(kind="and", length="wide"),
    },
    unit={
        "distance": UnitFormat(width="long"),
    },
)

app = Citry(
    extensions_defaults={
        "i18n": {
            "source_locale": "en-US",
            "locales": ("en-US", "cs-CZ", "ar-EG"),
            "formats": formats,
        },
    },
)

Profile names are application-defined. They must use ASCII letters, digits, -, or _. An unknown profile or a profile stored under the wrong category raises an error.

The registry accepts new names under the supported categories. It is not a plugin registry for arbitrary formatter implementations. The profile types are closed so the Rust server and browser can apply the same semantic rule.

Use profiles from a component

Use self.i18n.format in Python:

from decimal import Decimal


class AccountBalance(Component):
    citry = app

    def template_data(self, kwargs, slots):
        return {
            "balance": self.i18n.format.currency(
                Decimal("1234.50"),
                "EUR",
                format="account-balance",
            ),
        }

    template = """
      <data>{{ balance }}</data>
    """

Templates receive the shorter fmt facade:

<data>{{ fmt.number(total, format="measurement") }}</data>

Outside a component, use the service bound to an explicit locale context:

formatted = i18n.for_context(context).format.number(
    Decimal("1234.50"),
    format="measurement",
)

Choose the correct value type

OperationApplication valueImportant rule
numberexact int or finite DecimalPreserves exact decimal digits
percentexact int or finite DecimalThe value is a ratio; 0.125 means 12.5%
currencyexact number plus a currency codeThe code is three uppercase ASCII letters such as EUR
dateexact Python dateUses the locale's selected calendar and profile length
timezone-free Python timeRepresents wall-clock fields, not an instant
datetimeaware Python datetimeConverts the instant into the context's explicit time zone
relative_timeexact number plus unit="day"The current checked profile supports relative days
listlist or tuple of non-empty stringsFormats a conjunction or disjunction and isolates every item
unitexact number plus a unit identifierThe unit stays explicit application data

Citry rejects floats for exact numeric profiles. Convert application amounts to Decimal before formatting when decimal precision matters.

Keep percent values in one domain

Percent formatting uses ratio values:

from decimal import Decimal

label = self.i18n.format.percent(
    Decimal("0.125"),
    format="completion",
)

The same Decimal("0.125") means 12.5 percent in every locale. The formatter chooses the digits, decimal separator, spacing, and percent sign.

Parsing with the same profile returns the ratio again. See Parse localized input.

Keep date, time, and datetime distinct

A date has calendar fields but no clock. A time has wall-clock fields but no date or zone. A datetime formatter receives an aware instant and converts it to the time zone in the context:

context = i18n.make_context(
    locale="cs-CZ",
    time_zone="Europe/Prague",
)
formatter = i18n.for_context(context).format

text = formatter.datetime(
    aware_instant,
    format="appointment",
)

Calling datetime() without a context time zone is an error. Calling time() with a zone-aware Python time is also an error, because a zone offset can depend on the missing date.

Use the same names in the browser

A client-enabled provider exposes the registry through $i18n.format:

<output
  x-text="$i18n.format.currency(
    '1234.50',
    'EUR',
    { format: 'account-balance' },
  )"
></output>

Browser exact decimal values use strings or safe exact integers. Date formatting takes { year, month, day }; time formatting takes wall-clock fields; datetime formatting takes a JavaScript Date instant and the context's time zone.

The server uses ICU4X and the browser uses Intl. Both consume the same named profile and semantic input. Browser implementations may use different current locale data for presentational details, so do not compare localized output as an application identifier.