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
| Operation | Application value | Important rule |
|---|---|---|
number | exact int or finite Decimal | Preserves exact decimal digits |
percent | exact int or finite Decimal | The value is a ratio; 0.125 means 12.5% |
currency | exact number plus a currency code | The code is three uppercase ASCII letters such as EUR |
date | exact Python date | Uses the locale's selected calendar and profile length |
time | zone-free Python time | Represents wall-clock fields, not an instant |
datetime | aware Python datetime | Converts the instant into the context's explicit time zone |
relative_time | exact number plus unit="day" | The current checked profile supports relative days |
list | list or tuple of non-empty strings | Formats a conjunction or disjunction and isolates every item |
unit | exact number plus a unit identifier | The 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.