Internationalization
An app serves every locale from one set of templates, SQL, and YAML: message catalogs translate the UI and the error model, the request locale negotiates per user, and date/number inputs parse the way that user writes them.
Message catalogs
Section titled “Message catalogs”One YAML file per BCP-47 language tag under the app home’s messages/ directory; nested
maps flatten to dotted keys:
messages/ en.yml ja.ymlusers: list: title: ユーザー一覧 provision: unknown-user: 指定されたユーザーは存在しません。Lookup walks the exact tag, then the bare language (ja-JP → ja), then the app default
locale. App catalogs layer over the framework’s built-in texts (tql.*, shipped in English
and Japanese — the error and input texts, the list and form chrome, and every string of the
bundled system apps: the account, inbox, task-queue, sign-in, invitation, password-recovery,
consent and portal pages, and the IAM Admin and Operations consoles), so any framework
message can be overridden per app. Texts may carry {name}
placeholders — the same syntax the Hypermedia Components client catalog interpolates — filled
from the error entry they describe (constraint params, violation row columns).
Configuration and locale resolution
Section titled “Configuration and locale resolution”tesseraql: i18n: defaultLocale: en # the app's authoring locale (default en) locales: [en, ja] # served locales; defaults to every catalog found preference: # user-preference sources, highest priority first - query.lang # ?lang=ja — a language toggle without sign-in - preference.ui.locale # the account surface's stored choice - principal.claim.locale # the signed-in user's locale claimWith no locales: the served set is every catalog the app can answer in: its own
messages/ files and the framework’s built-in English and Japanese. So an app that never
wrote a catalog still serves ja — a browser asking for Japanese gets the framework’s
chrome, error texts and input messages in it, the language picker offers both, and the app’s
own texts fall back to the default locale. Declare locales: to pin the set. A declared
defaultLocale: or locales: entry is a BCP-47 tag the JDK can format (ja-JP, never
ja_JP; Locale.forLanguageTag folds the underscore spelling to und), judged by the same
rule as an export’s locale:; a value that fails it is a lint error and a boot refusal,
TQL-YAML-1065, naming the key.
Every route resolves its locale once, right after authentication: the preference sources in
order, then Accept-Language negotiation (RFC 4647 lookup, so ja-JP matches a supported
ja), then the default. Three source kinds exist: preference.<key> reads the signed-in
user’s stored preference (the full key after the prefix — the language picker in the
account surface writes ui.locale), principal.* reads the authenticated
principal, and
query.<name> reads a request parameter. When no list is declared the default is
[preference.ui.locale, principal.claim.locale] — the language a user picks in
/_tesseraql/account takes effect with zero configuration, and an IdP claim still covers
users who never chose. The result publishes as the request.locale source expression —
usable anywhere format sources are (export: { locale: request.locale }) — and drives
everything below. An unsupported preference falls through to the next source instead of
serving an untranslated locale.
Templates
Section titled “Templates”#{key} message expressions resolve from the catalogs with the request locale, and
#locale follows it (the framework shell sets lang from it):
<h2 th:text="#{users.list.title}">Users</h2><input class="hc-input" type="search" th:placeholder="#{users.list.searchPlaceholder}">A missing key renders as the standard ??key_locale?? marker so gaps stay visible (lint
reports them at build time, below). Locale-less renders — mail bodies and subjects, inbox
titles and bodies, generated file responses, a print template whose export declares no
locale: — read the English/default texts, never the server’s own locale; a print template
follows its export’s locale: when one is declared.
Localized errors
Section titled “Localized errors”Error responses localize at render time with the request locale. A field error’s declared
key keeps riding as messageKey while message carries the resolved text; the top-level
message is the localized status phrase (tql.http.<status>):
{"error": {"code": "TQL-FIELD-4220", "message": "入力内容を確認してください", "details": {"fields": [ {"rule": "userExists", "field": "userName", "code": "unknown-user", "messageKey": "users.provision.unknown-user", "message": "指定されたユーザーは存在しません。"}]}}}- Validation rules declare keys as before (
message: users.provision.unknown-user). - Input-constraint rejections (required/min/max/maxLength/enum/type) are field-scoped errors
with built-in keys (
tql.input.required,tql.input.min, …), translated by the framework catalog and overridable per app. - Mapped constraint violations fall back to
tql.constraint.<code>texts (duplicate,required, …); anerrors.constraintsmapping may declare its ownmessage:key. - The optimistic-locking conflict keys are
tql.conflict.*—staleis the hint every refusal carries, andtitle/keep/reload/overwriteare the declared lock’s dialog and page. Resolved per locale (hintKeykeeps the key,hintthe text).
htmx fragments carry the localized text as the item body and the key as data-message-key,
so the kit’s client catalog can re-resolve it after a swap.
Locale-aware input parsing
Section titled “Locale-aware input parsing”date, datetime, and number inputs parse with the request locale and an optional
format pattern — the same format-pattern rules as export columns
(file transfers):
input: orderDate: type: date format: yyyy/MM/dd # 2026/06/12 → a LocalDate bind parameter amount: type: number format: "#,##0.##" # 1.234,56 under de-DE → BigDecimal 1234.56Without format, number keeps ISO parsing and date/datetime accept the ISO defaults.
A bad value answers 400 with the matching tql.input.<type> field error.
The client catalog (Hypermedia Components)
Section titled “The client catalog (Hypermedia Components)”The shell loads /assets/_tesseraql/messages.js?locale=<tag> before the framework
bootstrap: an ES module that imports the kit’s official locale pack when the component kit
ships one for the language, and then merges the app’s catalog entries via
setMessages() — later merges win, so app wording layers over the pack. Module scripts
execute in document order, so the catalog lands before the behaviors install at
DOMContentLoaded. English needs no pack — the kit’s own defaults apply. Field-error items
also carry data-message-params, so a client-side catalog override with {name}
placeholders interpolates the violation’s values after a swap.
Testing and lint
Section titled “Testing and lint”Declarative suites (testing.md) assert on catalogs directly — one row per key
with key, locale, and text columns:
- name: the japanese catalog translates the provisioning error messages: locale: ja keys: [users.provision.unknown-user] expect: rows: - text: 指定されたユーザーは存在しません。A message coverage kind declares every shipped catalog by its language tag and counts it
covered when a messages case reads it, gated via coverage.thresholds.message.
Lint checks the declared locales, then the catalogs statically when messages/ exists:
| Code | Severity | Finding |
|---|---|---|
TQL-YAML-1065 |
error | a defaultLocale or locales entry is not a language tag the JDK can format (ja_JP); the boot refuses it too |
TQL-YAML-1007 |
error | a catalog file is malformed or its name is not a BCP-47 tag |
TQL-YAML-1103 |
warning | a declared tesseraql.i18n.locales entry has no catalog |
TQL-YAML-1008 |
warning | a catalog misses keys present in the default locale |
TQL-FIELD-2005 |
warning | a declared message key has no default-locale text |
- account.md — where users pick their own language.
- declarative-validation.md — the error messages being translated.