Internationalisation
Gebweb's i18n module covers four concerns: deciding which locale a request belongs to, looking up translated messages, pluralising counts, and formatting numbers and dates. Validation error messages can flow through the same catalog automatically.
Wiring
import gebweb;
let app = gebweb.app([HomeController()]);
gebweb.useI18n(app, {
"locales": ["en", "de", "fr"],
"default": "en",
"messages": "messages"
});
opts.locales is the supported list. opts.default is the
fallback when negotiation yields nothing. opts.messages is the
directory of catalog files (one YAML per locale tag, see below);
omit it to wire negotiation without translation.
Locale negotiation
The middleware tries the configured sources in order. The
default order is ["url", "cookie", "accept-language"].
| Source | How it picks |
|---|---|
"url" |
URL prefix /<tag>/.... When opts.urlStrip is true (the default), the matching prefix is removed from request["path"] so routes match against /.... |
"cookie" |
Reads opts.cookieName (default gebweb_locale) from the request Cookie header. The value must be in opts.locales. |
"accept-language" |
Parses the Accept-Language header with q-value ranking against opts.locales. |
The resolved Locale is published on request["_locale"]. Reorder
or shorten sources to match the app's identity model (cookie
before URL when a sticky preference should override path).
Locale injection
A handler that declares gebweb.Locale as a parameter receives
the resolved locale directly:
class HomeController {
@Get("/")
func home(gebweb.Locale locale): dict<string, any> {
return {"tag": locale.tag, "language": locale.language};
}
}
| Field | Type | Description |
|---|---|---|
tag |
string |
Canonical BCP-47 tag ("en-GB", "de"). |
language |
string |
Primary language subtag ("en"). |
region |
string |
Region subtag ("GB") or "". |
zone |
string |
IANA time zone name when supplied via opts.defaultZone. |
Translation catalogs
Catalogs live under the directory passed as opts.messages. One
YAML file per locale tag (messages/en.yaml,
messages/de-AT.yaml). Nested keys flatten to dotted strings:
# messages/en.yaml
auth:
login:
title: "Sign in"
button: "Log in"
home:
greeting: "Hello, {name}!"
Look up by the flat key. Placeholders use {name} syntax and are
substituted from args:
class HomeController {
@Get("/")
func home(gebweb.Locale locale, gebweb.Translator tr): dict<string, any> {
return {"greeting": tr.translate(locale.tag, "home.greeting", {"name": "Ada"})};
}
}
gebweb.Translator is auto-injected once a catalog is loaded. The
top-level surface is gebweb.t(app, locale, key, args) for code
holding the app reference; views see t(key, args) automatically
bound to the request locale.
Missing-key behaviour falls back from the requested locale to the default locale to the raw key itself, so untranslated strings remain visible without breaking the page.
Pluralisation
A catalog entry whose value is a sub-dict with CLDR variant keys becomes plural-aware:
# messages/en.yaml
cart:
items:
one: "1 item"
other: "{count} items"
# messages/ru.yaml
cart:
items:
one: "{count} товар"
few: "{count} товара"
many: "{count} товаров"
Pass a count arg to translate:
tr.translate("ru", "cart.items", {"count": 3});
tr.translate("ru", "cart.items", {"count": 5});
The CLDR rule for the resolved language picks the variant
(one/other/few/many/zero/two). Hand-coded rules ship
for en, de, fr, es, it, pt, ru, uk, pl, ar, zh, ja, ko, nl, tr;
other languages fall back to the English rule.
The helper gebweb.i18n.pluralForm(language, count) returns the
variant name directly when you want to drive other catalogs.
Validation messages
Translation flows into the @Assert.* pipeline automatically.
When a Translator is bound to the request, validation failures
look up validation.<code> in the catalog with placeholders
filled from the failure's params:
# messages/de.yaml
validation:
minLength: "Feld {field} muss mindestens {min} Zeichen lang sein"
email: "Feld {field} muss eine gültige E-Mail-Adresse sein"
A POST /signup to a German request will return the localised
messages in the 422 Problem Details body. Codes without a catalog
entry preserve the raw English message.
The set of codes is: email, url, uuid, regex, minLength,
maxLength, range, in, notBlank (plus any custom assertions
registered via gebweb.registerAssertion). Each rule supplies the
relevant params; minLength supplies min, range supplies
min and max, regex supplies pattern.
Number and date formatting
Import the Gebweb module by its qualified name. Bare import i18n; loads
Geblang's standard-library message catalog module instead.
import gebweb.i18n as i18n;
i18n.formatNumber(1234567, "de");
i18n.formatNumber("1234.56", "fr");
let parts = {"year": 2026, "month": 6, "day": 3};
i18n.formatDate(parts, "en-US");
i18n.formatDate(parts, "en", "long");
i18n.formatDate(parts, "de", "long");
i18n.numberSeparators(locale) returns the
{"thousands": ..., "decimal": ...} pair for callers that need to
format values in a custom way.
Reference
| Helper | Purpose |
|---|---|
gebweb.useI18n(app, opts) |
Mount the negotiator and optionally load catalogs. |
gebweb.Locale |
Typed parameter; injected from request["_locale"]. |
gebweb.Translator |
Typed parameter; injected from request["_translator"]. |
gebweb.parseLocale(tag) |
Parse a BCP-47 tag; null on malformed input. |
gebweb.t(app, locale, key, args) |
Top-level translate. |
gebweb.i18n.loadCatalogs(dir) |
Read every <dir>/<tag>.yaml. |
gebweb.i18n.pluralForm(language, count) |
CLDR variant name. |
gebweb.i18n.formatNumber(value, locale) |
Apply locale separators. |
gebweb.i18n.formatDate(parts, locale, style) |
"short" or "long" style. |