i18n API
Piko's internationalisation surface has four parts. Configuration via piko.WithWebsiteConfig, accessors on *piko.RequestData, the *Translation fluent builder, and the DateTime wrapper for date/time variables. Piko derives the search-engine optimisation (SEO) head (canonical URL and hreflang alternates) automatically, as described below. Translation sources are JSON files in locales/<locale>.json and per-page <i18n> blocks. For task recipes see the basic setup how-to, the routing strategy how-to, and the pluralisation how-to. For the design rationale see about i18n.
Configuration
piko.I18nConfig
type I18nConfig struct {
DefaultLocale string `json:"defaultLocale"`
Strategy string `json:"strategy"`
Locales []string `json:"locales"`
}
| Field | Type | Purpose |
|---|---|---|
DefaultLocale | string | Locale served when no other selection applies. Example "en". |
Strategy | string | URL-encoding strategy; one of the values below. |
Locales | []string | Every supported locale code. Each code needs a matching locales/<code>.json file. An empty slice disables i18n. |
Pass it via WithWebsiteConfig in your func main:
ssr := piko.New(
piko.WithWebsiteConfig(piko.WebsiteConfig{
I18n: piko.I18nConfig{
DefaultLocale: "en",
Strategy: "prefix_except_default",
Locales: []string{"en", "fr", "de"},
},
}),
)
Strategy values
Strategy is a string. The runtime recognises exactly three values:
| Value | Behaviour |
|---|---|
"query-only" | Default. Single path per page. The locale comes from a query parameter or detection (/about?lang=fr). |
"prefix" | Every locale carries a prefix, including the default (/en/about, /fr/about). |
"prefix_except_default" | The default locale is bare. Non-default locales carry a prefix (/about, /fr/about). |
To turn i18n routing off entirely, leave Locales empty (or omit it).
Note: No
piko.StrategyPrefix*constant exists. Pass the string literal directly in code ("prefix_except_default","query-only", etc.).
Per-page opt-in
A page enables multi-locale routing by declaring a SupportedLocales function in its <script type="application/x-go"> block:
func SupportedLocales() []string {
return []string{"en", "fr", "de"}
}
Without SupportedLocales, the page produces a single route under the default locale even when other pages opt in. The router reads the list at build time.
Request accessors
Methods on *piko.RequestData. Available in template expressions (the r. prefix is implicit) and in Render().
| Method | Returns | Purpose |
|---|---|---|
r.Locale() | string | Active locale for the request (for example "en-GB"). |
r.DefaultLocale() | string | Configured default locale. |
r.T(key, fallbacks...) | *Translation | Look up key in the global store first, then the per-page <i18n> block. Variadic fallbacks tried in order. |
r.LT(key, fallbacks...) | *Translation | Look up key only in the per-page <i18n> block. Variadic fallbacks tried in order. |
r.LF(value) | *FormatBuilder | Wrap a value for locale-aware formatting outside a translation. |
Inside templates, the T and LT shortcuts call r.T and r.LT:
<h1>{{ T("nav.home") }}</h1>
<p>{{ LT("page.heading") }}</p>
Fallback arguments
T and LT have signature T(keyAndFallbacks ...string) *Translation. The first argument is the key. Every subsequent argument is a fallback string the runtime tries in order if the lookup chain fails. You may supply multiple fallbacks:
r.T("button.save", "Save")
r.T("page.subtitle", "page.heading", "Welcome")
The first non-empty resolution wins. The literal key is the final fallback if every preceding fallback also resolves empty.
T versus LT
r.T(key) walks the global store first and falls back to the page's local store. Use it for translations that come from i18n/*.json and may also be locally overridden.
r.LT(key) only ever consults the local store. Use it when a string lives in this PK file's <i18n> block and you do not want a same-named global key to shadow it accidentally.
The translation builder
r.T(...) and r.LT(...) return a *Translation (alias piko.Translation). The builder is mutable, fluent, and pooled. Every setter returns the same *Translation so calls chain.
Terminate the chain with String() to render. The *Translation also implements fmt.Stringer, which means template expressions render it implicitly:
{{ T("greeting").StringVar("name", state.User) }}
Variable setters
| Method | Signature | Locale-aware? | Purpose |
|---|---|---|---|
StringVar(name, value string) | (string, string) *Translation | No | Bind a string. |
IntVar(name string, value int) | (string, int) *Translation | No | Bind a Go int. |
FloatVar(name string, value float64) | (string, float64) *Translation | Yes | Bind a float64; locale picks the decimal separator and thousands grouping. |
DecimalVar(name string, value maths.Decimal) | (string, maths.Decimal) *Translation | Yes | Bind an arbitrary-precision decimal from piko.sh/piko/wdk/maths. |
MoneyVar(name string, value maths.Money) | (string, maths.Money) *Translation | Yes | Bind a money value; locale picks the currency-symbol position, separators, and digit grouping. |
BigIntVar(name string, value maths.BigInt) | (string, maths.BigInt) *Translation | Yes | Bind an arbitrary-precision integer with locale grouping. |
TimeVar(name string, value time.Time) | (string, time.Time) *Translation | Yes | Bind a time.Time with the medium date-time style for the locale. |
DateTimeVar(name string, value DateTime) | (string, DateTime) *Translation | Yes | Bind a date/time with an explicit style; see The DateTime wrapper. |
Var(name string, value any) | (string, any) *Translation | No | Generic fallback; uses fmt.Stringer if implemented, else %v. |
Pluralisation
| Method | Signature | Purpose |
|---|---|---|
Count(n int) | (int) *Translation | Set the count used for Common Locale Data Repository (CLDR) plural form selection. Auto-binds ${count}. |
r.T("cart.items").Count(5).String()
// "5 items" with "one item|${count} items" in en
See How to pluralise translations for the pipe-separated form syntax and per-language rule sets.
Terminus
| Method | Signature | Purpose |
|---|---|---|
String() | () string | Resolve key, select plural form, render template parts, return the string. Releases the builder back to the pool. |
The *Translation is also fmt.Stringer, so passing it directly into a template expression invokes String() implicitly.
Lifecycle
Piko pools the builder. Calling String() (or invoking it through fmt.Stringer) releases the builder. Do not retain a *Translation past the render that produced it.
Date and time formatting
Two builders cover locale-aware temporal formatting from user code: TimeVar on a *Translation and r.LF(value) for standalone formatting.
TimeVar
Embeds a time.Time inside a translation with the medium style for the active locale:
r.T("event.starts").TimeVar("at", record.StartsAt)
For a different style, format outside the translation with LF and bind the result with StringVar:
formatted := r.LF(record.StartsAt).Long().DateOnly().String()
r.T("event.starts").StringVar("at", formatted)
LF (format builder)
r.LF(value) returns a *FormatBuilder for inline locale-aware formatting. The bare LF and F helpers are also available in templates. The builder accepts time.Time, int*, uint*, float32/float64, string, bool, maths.Decimal, maths.BigInt, maths.Money, and time.Duration.
Style methods (each returns the same builder for chaining):
| Method | Style | Date example (en-GB) | Time example |
|---|---|---|---|
Short() | Compact | 02/01/2006 | 15:04 |
Medium() | Default | 2 Jan 2026 | 15:04:05 |
Long() | Explicit | 2 January 2026 | 15:04:05 GMT |
Full() | Most verbose | Monday, 2 January 2026 | 15:04:05 GMT |
DateOnly() | Render only the date portion. | ||
TimeOnly() | Render only the time portion. | ||
UTC() | Convert to UTC before formatting. | ||
Precision(n) | Set decimal precision for numeric values. | ||
Locale(code) | Override the active locale for this builder only. |
Terminate with String() (or rely on fmt.Stringer in templates).
Note:
DateTimeVarexists on*Translationand accepts an internalDateTimevalue type. The type lives underpiko.sh/piko/internal/..., which Go's internal-package rule prevents user code from importing. The same outcomes are reachable throughTimeVarplusLF; use those from user code.
Translation sources
Global JSON files
Place one file per locale at locales/<locale>.json. The runtime ignores files outside this directory. The file's basename (so en.json becomes en) is the locale code.
{
"nav": {
"home": "Home",
"about": "About"
},
"items": "no items|one item|${count} items"
}
Nested objects flatten to dot-separated keys. nav.home is the lookup key for "Home".
Per-page <i18n> blocks
Declare page-scoped translations inside a .pk file:
<i18n lang="json">
{
"en": { "heading": "Welcome" },
"fr": { "heading": "Bienvenue" }
}
</i18n>
A file may contain multiple <i18n> blocks. The compiler merges them.
Note: The compiler honours only
lang="json". It silently skips blocks declared with any otherlangattribute.
The block's top-level keys are locale codes. Each locale's value is the same nested-key shape as a global file.
Template-string syntax
Translation values support three constructs.
Variable interpolation
${expression} evaluates a Piko expression at render time. The expression sees variables bound through the *Var setters and the implicit count from Count(n).
{
"greeting": "Hello, ${name}!",
"summary": "${user} has ${count} items"
}
Expressions accept operators and property access (${user.firstName}, ${count + 1}).
Linked-message references
@key.path embeds another translation. The runtime resolves the reference against the same locale and store, passing through all bound variables:
{
"common": {
"appName": "MyApp"
},
"welcome": "Welcome to @common.appName, ${name}!"
}
The runtime caps recursion at depth 10 to break cycles.
Backslash escaping
\$ and \@ produce literal $ and @ characters:
{
"email": "support\\@example.com",
"price": "\\${amount}"
}
(The double backslash is JSON's escape. The parser sees \@ and \$.)
Pluralisation
Translation values may carry pipe-separated plural forms:
{
"items": "no items|one item|${count} items"
}
Count(n) selects the form for the active locale. CLDR rules cover six categories (zero, one, two, few, many, other) and the form ordering follows the rule set per locale family. See How to pluralise translations for ordering tables.
To represent a literal pipe inside a form, escape it as ||.
Locale fallback chain
When r.T(key) or r.LT(key) resolves, the lookup walks:
- The exact requested locale (for example
en-GB). - The base language (for example
en). DefaultLocalefromI18nConfig.- Each subsequent argument passed to
T/LT(variadic fallbacks), tried in order. - The literal key.
r.T(key) consults the global store first and the local store second. r.LT(key) only consults the local store.
SEO metadata
Piko derives the locale SEO head automatically. When a page declares SupportedLocales(), the framework fills Metadata.Language, Metadata.CanonicalURL, and Metadata.AlternateLinks from the page's registered per-locale route patterns, so it emits the <html lang>, <link rel="canonical">, and <link rel="alternate" hreflang="..."> tags correctly with no per-page code.
func SupportedLocales() []string { return []string{"en", "fr", "de"} }
func Render(r *piko.RequestData, props Props) (Response, piko.Metadata, error) {
// Language, CanonicalURL and AlternateLinks are derived automatically from
// the page's registered locale routes.
return Response{}, piko.Metadata{Title: props.PageTitle}, nil
}
The canonical origin comes from the configured site URL (the SEO sitemap hostname) when set, otherwise the request host. A page may still set Metadata.CanonicalURL or Metadata.AlternateLinks explicitly to override the derived values.
Locale-aware links
The <piko:a> element rewrites its href for the active locale:
<piko:a href="/about">{{ T("nav.about") }}</piko:a>
A request on /fr/about clicking that link lands on /fr/about. With data-locale="en" the link points at the English variant regardless of the active locale, useful for language switchers.
See also
- About i18n for the design rationale.
- How to add translations to a site.
- How to choose an i18n routing strategy.
- How to interpolate variables and reference other keys in translations.
- How to pluralise translations.
- How to bind typed variables to translations.
- How to format dates and times for a locale.
- Metadata reference for
Metadata.AlternateLinks.
Integration tests: tests/integration/i18n/.