Version: 1.0.0-draft.1 Date: 2026-03-20 Editors: Formspec Working Group Companion to: Formspec v1.0 — A JSON-Native Declarative Form Standard
This document is a Draft companion specification to the Formspec v1.0 Core Specification. It defines the Formspec Locale Document format — a sidecar JSON document that provides internationalized strings for a Formspec Definition.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in ALL CAPITALS, as shown here.
JSON syntax and data types are as defined in [RFC 8259]. URI syntax is as defined in [RFC 3986].
Terms defined in the Formspec v1.0 core specification — including Definition, Item, Response, Bind, FEL, and conformant processor — retain their core-specification meanings throughout this document unless explicitly redefined.
$formspecLocale,
version, locale,
targetDefinition, and a strings object.{{expression}}
syntax.schemas/locale.schema.json;
generated schema references are the canonical structural contract.
Formspec v1.0 defines form structure, behavior, and validation in a
single Definition document. Every Item has a label,
optional description and hint properties, and
choice options with display text. These inline strings serve as the
default presentation language.
Real-world forms must be presented in multiple languages. A federal grant application may need English, Spanish, and French versions. A multinational survey may require dozens of locales with regional variants. Without a standard localization mechanism, implementors must either embed all translations inside the Definition (bloating it and coupling translation to structural authoring) or build bespoke translation infrastructure outside the spec.
This specification defines a Locale Document — a standalone JSON artifact that provides localized strings for a Formspec Definition. A Locale Document:
Authors who do not need internationalization change nothing. The Definition’s inline strings serve as the default locale.
This specification defines:
locale() FEL function that exposes
the active locale code to FEL expressions in the Definition.pluralCategory() (Core
§3.5) for expressing pluralization patterns using CLDR plural
categories.This specification does NOT define:
Accept-Language parsing — this is
a host application concern.PageLayout.title,
PageLayout.description) are addressable via the
$page.<pageId> key prefix (§3.1.7). Component-tier
text props (Heading.text, Alert.text,
Card.title, etc.) are addressable via the
$component.<nodeId>.<prop> key prefix (§3.1.8),
where <nodeId> is the optional id
property on the component node. OptionSet option labels shared across
fields are addressable via the $optionSet.<setName>
key prefix (§3.1.3). Locale Documents MUST NOT alter non-string
properties (layout, styling, widget configuration, behavioral
expressions) — those remain Theme/Component concerns.The Formspec architecture defines concerns as composable sidecar artifacts:
| Concern | Inline (Tier 1) | Sidecar artifact |
|---|---|---|
| Structure & behavior | Items, Binds, Shapes | Core Definition |
| Presentation | presentation hints |
Theme Document |
| Interaction | widgetHint |
Component Document |
| Data transform | fieldMap |
Mapping Document |
| Localization | Inline string properties | Locale Document (this spec) |
The Locale Document follows the same sidecar pattern: the Definition provides sensible defaults inline; the Locale Document overrides them for a specific language. Multiple Locale Documents MAY target the same Definition.
| Term | Definition |
|---|---|
| Definition | A Formspec Definition document (core spec §4). |
| Locale Document | A JSON document conforming to this specification. |
| Locale code | A BCP 47 language tag (e.g., en, fr-CA,
zh-Hans). |
| String key | A dot-delimited path identifying a localizable string (§3.1). |
| Cascade | The fallback chain that determines the resolved string for a given key (§4). |
| Interpolation | Embedding FEL expressions in string values via
{{expression}} syntax (§3.3). |
JSON examples use // comments for annotation; comments
are not valid JSON. Property names in monospace (locale)
refer to JSON keys. Section references (§N) refer to this document
unless prefixed with “core” (e.g., “core §4.2.5”).
A Formspec Locale Document is a JSON object. Conforming implementations MUST recognize the following top-level properties and MUST reject any Locale Document that omits a REQUIRED property.
{
"$formspecLocale": "1.0",
"url": "https://agency.gov/forms/budget/locales/fr-CA",
"version": "1.0.0",
"name": "budget-fr-CA",
"title": "Budget Form — Canadian French",
"description": "French-Canadian localization for the annual budget form.",
"locale": "fr-CA",
"fallback": "fr",
"targetDefinition": {
"url": "https://agency.gov/forms/budget",
"compatibleVersions": ">=1.0.0 <2.0.0"
},
"strings": {
"projectName.label": "Nom du projet",
"projectName.hint": "Entrez le nom officiel du projet",
"budget.label": "Budget",
"budget.description": "Section des informations budgétaires"
}
}| Pointer | Field | Type | Required | Notes | Description |
|---|---|---|---|---|---|
#/properties/$formspecLocale |
$formspecLocale |
string |
yes | const: “1.0”; critical |
Locale specification version. MUST be ‘1.0’. |
#/properties/description |
description |
string |
no | — | Human-readable description of the locale’s purpose and target audience. |
#/properties/extensions |
extensions |
object |
no | — | Extension namespace for vendor-specific or tooling-specific metadata. All keys MUST be x- prefixed. Processors MUST ignore unrecognized extensions. Extensions MUST NOT alter locale resolution semantics. |
#/properties/fallback |
fallback |
string |
no | pattern: 1{2,3}(-[a-zA-Z0-9]{2,8})*$ |
BCP 47 language tag of the locale to consult when a key is not found in this document’s strings. Enables explicit fallback chains (e.g., fr-CA → fr). If absent, the cascade proceeds to implicit language fallback (strip region subtag) or inline defaults. Processors MUST detect circular fallback chains and terminate the cascade with a warning. |
#/properties/locale |
locale |
string |
yes | pattern: 2{2,3}(-[a-zA-Z0-9]{2,8})*$;
critical |
BCP 47 language tag identifying the locale this document provides strings for. Processors MUST perform case-insensitive comparison and SHOULD normalize to lowercase language with title-case region (e.g., ‘fr-CA’). |
#/properties/name |
name |
string |
no | — | Machine-friendly short identifier for programmatic use. |
#/properties/strings |
strings |
object |
yes | critical | Map of string keys to localized values. Keys follow the dot-delimited path format defined in the Locale Specification §3.1. Values are strings, optionally containing FEL interpolation via {{expression}} syntax. Keys address item properties (key.label, key.description, key.hint), context labels (key.label@context, key.hint@context), choice options (key.options.value.label), shared option sets (optionSet.setName.value.label), validationmessages(key.errors.CODE, key.constraintMessage, key.requiredMessage), form − levelstrings(form.title, form.description), shapemessages(shape.id.message), theme page strings ($page.pageId.title, page.pageId.description), andcomponentnodestrings(component.nodeId.property). |
#/properties/targetDefinition |
targetDefinition |
$ref |
yes | $ref:
https://formspec.org/schemas/component/1.0#/$defs/TargetDefinition;
critical |
Binding to the target Formspec Definition and compatible version range. The locale will only be applied to Definitions matching this target. If compatibleVersions is present and the Definition version falls outside the range, the processor SHOULD warn and MAY fall back to inline strings only. The processor MUST NOT fail on a version mismatch. |
#/properties/title |
title |
string |
no | — | Human-readable display name for the Locale Document. |
#/properties/url |
url |
string |
no | — | Canonical identifier for this Locale Document. Stable across versions — the tuple (url, version) SHOULD be globally unique. |
#/properties/version |
version |
string |
yes | critical | Version of this Locale Document. SemVer is RECOMMENDED. The tuple (url, version) SHOULD be unique across all published locale versions. |
The targetDefinition object binds this Locale Document
to a specific Definition.
| Property | Type | Cardinality | Description |
|---|---|---|---|
url |
string (URI) | 1..1 (REQUIRED) | Canonical URL of the target Definition (url property
from the Definition). |
compatibleVersions |
string | 0..1 (OPTIONAL) | Semver range expression (e.g., ">=1.0.0 <2.0.0")
describing which Definition versions this locale supports. When absent,
the locale is assumed compatible with any version. |
When compatibleVersions is present, a processor SHOULD
verify that the Definition’s version satisfies the range
before applying the Locale Document. A processor MUST NOT fail if the
range is unsatisfied; it SHOULD warn and MAY fall back to inline
strings.
The locale property MUST be a syntactically valid BCP 47
language tag. Processors SHOULD validate subtags against the IANA
Language Subtag Registry when available, but MUST NOT fail on
unrecognized subtags.
Well-known examples:
| Code | Language |
|---|---|
en |
English |
en-US |
American English |
fr |
French |
fr-CA |
Canadian French |
es |
Spanish |
zh-Hans |
Simplified Chinese |
ar |
Arabic |
Processors MUST perform case-insensitive comparison of locale codes
(BCP 47 tags are case-insensitive). Processors SHOULD normalize locale
codes to lowercase language with title-case region (e.g.,
fr-CA, not FR-CA or fr-ca).
String keys use dot-delimited paths that address localizable properties of Items in the target Definition. The general format is:
<itemKey>.<property>
Where <itemKey> is the key of an Item
in the Definition, and <property> identifies which
string property to localize.
When a Definition uses modular composition via $ref with
keyPrefix (core spec §6.6), string keys MUST use the
post-assembly key (i.e., after the prefix has been
prepended). For example, if a Definition imports items with
keyPrefix: "section1_", an imported item with key
name becomes section1_name, and the Locale
Document must use section1_name.label.
The following Item properties are localizable:
| Key pattern | Target property | Description |
|---|---|---|
<key>.label |
Item.label |
Primary display label. |
<key>.description |
Item.description |
Help text / tooltip. |
<key>.hint |
Item.hint |
Instructional text alongside input. |
Examples:
{
"projectName.label": "Nom du projet",
"projectName.hint": "Entrez le nom officiel",
"budgetSection.label": "Section budgétaire",
"budgetSection.description": "Détails du budget annuel"
}The Definition’s labels object provides alternative
display labels keyed by context name (e.g., short,
pdf, accessibility). Locale Documents override
these with a @context suffix:
<itemKey>.label@<context>
Examples:
{
"budgetSection.label": "Section budgétaire",
"budgetSection.label@short": "Budget",
"budgetSection.label@pdf": "Section III : Informations budgétaires",
"budgetSection.label@accessibility": "Section du budget annuel détaillé"
}When resolving a context label, the cascade is:
<key>.label@<context>
(if present)<key>.label (general
label)labels[context] (inline context label)label (inline default)The @context suffix MAY be used with any localizable
property, not only label. For properties without a
Definition-side context equivalent (i.e., properties other than
label), the cascade omits the inline context step:
| Step | label@context |
hint@context / description@context |
|---|---|---|
| 1 | Locale key.label@context |
Locale key.hint@context |
| 2 | Locale key.label |
Locale key.hint |
| 3 | Definition labels[context] |
(no equivalent — skip) |
| 4 | Definition label |
Definition hint |
Example: providing a screen-reader-specific hint:
{
"email.hint": "Courriel professionnel",
"email.hint@accessibility": "Saisissez votre adresse courriel professionnelle. Ce champ est obligatoire."
}Fields with choices have option display text that must
be localized. Options are addressed by their value:
<fieldKey>.options.<optionValue>.label
Only the label property of choice options is
localizable. The core Definition schema defines option objects with
value and label only; value is a
data key and is not subject to localization.
Examples:
{
"fundingStatus.options.yes.label": "Oui",
"fundingStatus.options.no.label": "Non",
"fundingStatus.options.na.label": "Sans objet"
}When an option value contains characters that are not
valid in a dot-delimited key (., \), those
characters MUST be escaped with a backslash: \. for a
literal dot, \\ for a literal backslash.
When multiple fields share an OptionSet (core §4.6), translators MAY
provide a single set of option translations using the
$optionSet prefix:
$optionSet.<setName>.<optionValue>.label
The resolution cascade for option labels is:
<fieldKey>.options.<value>.label$optionSet.<setName>.<value>.labellabel from the DefinitionField-level keys override OptionSet-level keys, enabling
context-specific translations when the same value set needs different
display text in different fields (e.g., “Yes/No” vs. “Approved/Rejected”
for the same underlying yesNoNA set).
Examples:
{
"$optionSet.yesNoNA.yes.label": "Oui",
"$optionSet.yesNoNA.no.label": "Non",
"$optionSet.yesNoNA.na.label": "Sans objet",
"approvalStatus.options.yes.label": "Approuvé"
}The $optionSet prefix is reserved and cannot collide
with item keys (item keys exclude the $ character).
Escaping rules for option values containing dots or backslashes (§3.1.3)
apply identically to OptionSet-level keys.
Validation messages are addressable at two granularities: per constraint code (coarse) and per Bind (fine-grained).
<itemKey>.errors.<code>
Where <code> matches the code
property of the ValidationResult. The code property
provides machine-readable identifiers designed for localization key
lookups. Seven codes are reserved for built-in constraints:
REQUIRED, TYPE_MISMATCH,
MIN_REPEAT, MAX_REPEAT,
CONSTRAINT_FAILED, SHAPE_FAILED,
EXTERNAL_FAILED. Shape rules MAY define custom codes (e.g.,
BUDGET_SUM_MISMATCH). This replaces the message for all
validation results with that code targeting the item.
constraintMessage and requiredMessage)Individual Binds may define a constraintMessage (core
spec §4.3.1) or use the item-level required message. To localize a
specific Bind’s constraint message, use:
<itemKey>.constraintMessage
When a field has a single Bind with constraint, this key
localizes that Bind’s constraintMessage. When a field is
targeted by multiple Binds, the key applies to the first Bind whose
constraint fires.
To localize the required-field message for an item:
<itemKey>.requiredMessage
When resolving a validation message, the cascade is:
<key>.errors.<code>) —
if present, wins.<key>.constraintMessage or
<key>.requiredMessage) — if present.constraintMessage on the Bind (Definition).Examples:
{
"email.errors.REQUIRED": "L'adresse courriel est obligatoire",
"email.errors.CONSTRAINT_FAILED": "Veuillez entrer une adresse courriel valide",
"ssn.constraintMessage": "Le NAS doit être au format 000-000-000",
"budget.errors.TYPE_MISMATCH": "Le budget doit être un nombre"
}The code property is optional on
ValidationResult. When a result lacks an explicit
code, processors MUST synthesize it from the
constraintKind property using the reserved code
mapping:
constraintKind |
Synthesized code |
|---|---|
required |
REQUIRED |
type |
TYPE_MISMATCH |
cardinality |
MIN_REPEAT or MAX_REPEAT (based on
violation) |
constraint |
CONSTRAINT_FAILED |
shape |
SHAPE_FAILED |
external |
EXTERNAL_FAILED |
This ensures locale keys are always resolvable regardless of whether
the processor explicitly sets the code property.
Top-level Definition properties (title,
description) use the reserved key prefix
$form:
{
"$form.title": "Rapport annuel sur les subventions",
"$form.description": "Formulaire de rapport pour les bénéficiaires"
}The $form and $shape prefixes are reserved
for form-level and shape-level keys respectively. These prefixes cannot
collide with item keys because the core Definition schema restricts item
keys to the pattern [a-zA-Z][a-zA-Z0-9_]*, which excludes
the $ character.
Shape rules (cross-field validations) are addressed by the shape’s
id:
$shape.<shapeId>.message
Example:
{
"$shape.budget-balance.message": "Le total du budget doit correspondre au financement demandé"
}Theme Documents define pages via PageLayout objects with
id, title, and description
properties. These user-visible strings are addressable via the
$page prefix:
$page.<pageId>.title
$page.<pageId>.description
Where <pageId> is the id property of
a PageLayout in the Theme Document (theme spec §6.1).
Examples:
{
"$page.info.title": "Informations du projet",
"$page.info.description": "Entrez les détails de base du projet",
"$page.review.title": "Révision et soumission"
}Page IDs are unique within a Theme Document and follow the pattern
^[a-zA-Z][a-zA-Z0-9_\-]*$.
Note:
$page.keys address Theme-tier constructs. A Locale Document using$page.keys depends on both the target Definition and the associated Theme Document. Validators SHOULD warn when a$page.key references a page ID not present in any loaded Theme Document (§7.2).
Component tree nodes with an id property (component spec
§3.1) are addressable via the $component prefix:
$component.<nodeId>.<property>
$component.<nodeId>.<property>[<index>]
$component.<nodeId>.<arrayProp>[<index>].<subProp>
Where <nodeId> is the id property of
a component node in the Component Document. Only string-typed props (and
string elements of array props) are addressable. Bracket indexing with
numeric indices is used for array-valued properties.
Examples:
{
"$component.budgetHeading.text": "Détails du budget",
"$component.contactCard.title": "Coordonnées",
"$component.contactCard.subtitle": "Adresse courriel et téléphone",
"$component.submitBtn.label": "Soumettre la demande",
"$component.submitBtn.pendingLabel": "Soumission en cours...",
"$component.mainTabs.tabLabels[0]": "Personnel",
"$component.mainTabs.tabLabels[1]": "Emploi",
"$component.lineItemTable.columns[0].header": "Description",
"$component.lineItemTable.columns[1].header": "Montant"
}The following component properties are localizable:
| Component | Localizable Props |
|---|---|
| Page | title, description |
| Heading | text |
| Text | text |
| Alert | text |
| Divider | label |
| Card | title, subtitle |
| Collapsible | title |
| ConditionalGroup | fallback |
| Tabs | tabLabels[N] |
| Accordion | labels[N] |
| SubmitButton | label, pendingLabel |
| DataTable | columns[N].header |
| Panel | title |
| Modal | title, triggerLabel |
| Popover | triggerLabel |
| Badge | text |
| ProgressBar | label |
| Summary | items[N].label |
| Select | placeholder |
| TextInput | placeholder, prefix,
suffix |
When a component node with id appears inside a repeat
template (e.g., as a child of a DataTable or Accordion bound to a
repeatable group), the id identifies the template
node, not individual rendered instances. All instances share
the same locale resolution — the key
$component.<id>.<prop> resolves to the same
string template, but {{expression}} sequences within that
string are evaluated in each repeat instance’s binding scope, giving
access to @index and @count.
Note:
$component.keys address Component-tier constructs. A Locale Document using$component.keys depends on both the target Definition and the associated Component Document. Validators SHOULD warn when a$component.key references a node ID not present in any loaded Component Document (§7.2).
Processors MUST apply the following rules when resolving string keys:
projectName.label and ProjectName.label are
different keys.strings object are
governed by JSON parsing rules (last value wins per RFC 8259 §4).
Authoring tools SHOULD warn on duplicates.String values MAY contain FEL expressions delimited by double curly braces:
{{<FEL expression>}}
The expression is evaluated in the binding context of the Item
identified by the string key’s <itemKey> prefix. This
gives the expression access to:
$ references (e.g.,
$budget, $projectName)locale() function (§5.1)pluralCategory() function (core spec §3.5)Examples:
{
"itemCount.label": "Nombre d'articles : {{$itemCount}}",
"budget.hint": "Maximum autorisé : {{formatNumber($maxBudget)}} $",
"lineItems.label": "{{$count}} {{if(pluralCategory($count) = 'one', 'poste', 'postes')}}"
}Processors MUST apply the following rules:
{{ in a string value without
triggering interpolation, authors MUST double the opening braces:
{{{{. Processors MUST treat {{{{ as a literal
{{ in the output. (In JSON source, this is simply
"{{{{".){{<original expression>}} and SHOULD emit a
warning.null becomes
the empty string "". Booleans become "true" or
"false". Numbers use their default string
representation.calculate expressions.{{...}}
sequences.The FEL evaluation context for {{expression}} sequences
depends on the string key’s prefix:
| Key prefix | Binding context | @index/@count |
Available references |
|---|---|---|---|
<itemKey>.* |
Item’s binding scope | Yes, if item is inside a repeat group | $fieldRef relative to scope |
$form.* |
Global form context | No | All top-level $fieldRef |
$shape.<id>.* |
Shape’s target scope | Depends on shape target | Per shape definition |
$page.<id>.* |
Global form context | No | All top-level $fieldRef |
$optionSet.* |
Global form context | No | All top-level $fieldRef |
$component.<id>.* (outside repeat) |
Global form context | No | All top-level $fieldRef |
$component.<id>.* (inside repeat template) |
Repeat instance scope | Yes | $fieldRef within repeat scope + parent scopes |
For item-level keys inside repeat groups, the locale key uses the
template path (indices stripped), but
{{expression}} is evaluated in the instance
context — @index resolves to the actual instance
index. This enables per-instance labels:
{
"lineItems.label": "Poste budgétaire {{@index + 1}}"
}When the engine resolves a localized string, it walks a fallback chain from most-specific to least-specific:
For a requested locale code (e.g., fr-CA) and string key
(e.g., projectName.label):
locale matches
fr-CA.fallback (e.g., "fr"),
look up the key in the Locale Document whose locale matches
the fallback code. If the fallback Locale Document itself declares a
fallback, continue walking the explicit chain (subject to
circular detection, §4.3).fr from
fr-CA). This step is skipped if any step in the explicit
fallback chain already consulted a Locale Document with that base
language code.label, description,
hint, etc.).A processor MUST walk the cascade in this order and MUST return the
first non-null result. If all steps produce no result, the processor
MUST return the empty string "".
Example of explicit fallback to a different language: If
fr-CAdeclaresfallback: "pt", the cascade is: (1)fr-CA, (2)pt(explicit), (3)fr(implicit — strip region from originalfr-CA), (4) inline. Both explicit and implicit fallback steps are consulted becauseptis a different language from the basefr.
Given these documents:
Definition (inline defaults):
{
"items": [
{ "key": "name", "type": "field", "label": "Name", "hint": "Enter your full name" }
]
}Locale Document (fr):
{
"locale": "fr",
"strings": {
"name.label": "Nom",
"name.hint": "Entrez votre nom complet"
}
}Locale Document (fr-CA):
{
"locale": "fr-CA",
"fallback": "fr",
"strings": {
"name.hint": "Entrez votre nom au complet"
}
}Resolution for locale fr-CA:
| Key | fr-CA |
fr |
Inline | Resolved |
|---|---|---|---|---|
name.label |
— | "Nom" |
"Name" |
“Nom” |
name.hint |
"Entrez votre nom au complet" |
"Entrez votre nom complet" |
"Enter your full name" |
“Entrez votre nom au complet” |
A processor MUST detect circular fallback chains (e.g.,
fr-CA → fr → fr-CA) and MUST
terminate the cascade, falling through to inline defaults. Processors
SHOULD emit a warning when a circular fallback is detected.
An engine MAY have multiple Locale Documents loaded simultaneously.
The engine maintains a locale cascade — an ordered list of Locale
Documents consulted during string resolution. The
setLocale() call (§6.2) determines which cascade is
active.
This specification introduces three FEL functions.
locale() is part of the Locale Core
conformance level (§10) and MUST be implemented by all conformant locale
processors. formatNumber() and formatDate()
are part of the Locale Extended conformance level and
are OPTIONAL.
The core FEL function pluralCategory() (core spec §3.5)
returns the CLDR plural category (zero, one,
two, few, many,
other) for a given number and is available in all FEL
evaluation contexts including locale string interpolation. It replaces
the need for a locale-specific pluralization function.
These functions are registered as locale-tier extensions to the FEL stdlib. They MUST NOT collide with core FEL built-in function names. Processors that do not support locale functionality MUST NOT register these functions.
locale()Returns the active locale code as a string.
Signature: locale() → string
Returns the BCP 47 language tag of the currently active locale. If no
locale is active (no Locale Document loaded), returns the empty string
"".
Like now() (core spec §3.1), locale() is
non-deterministic — its return value changes when
setLocale() is called. Processors SHOULD document their
locale() resolution behavior, consistent with the core
spec’s treatment of now().
This function is available in all FEL evaluation contexts —
calculate, relevant, constraint,
and readonly expressions. This enables locale-aware logic
in the Definition itself:
{
"key": "instructions",
"type": "display",
"label": "Instructions",
"bind": {
"relevant": "locale() = 'en' or locale() = ''"
}
}pluralCategory()Pluralization in locale strings uses the core FEL function
pluralCategory(count) (core spec §3.5), which returns the
CLDR plural category for the active locale. The six possible return
values are: zero, one, two,
few, many, other.
Authors combine pluralCategory() with if()
to select the appropriate word form:
{
"lineItems.label": "{{$count}} {{if(pluralCategory($count) = 'one', 'ligne', 'lignes')}}"
}For languages with more than two plural forms (e.g., Arabic with six forms, or Polish with three), authors chain conditions:
{
"items.label": "{{$count}} {{if(pluralCategory($count) = 'one', 'element', if(pluralCategory($count) = 'few', 'elementy', 'elementów'))}}"
}Because pluralCategory() uses CLDR data, it correctly
handles all languages — including those where the one
category does not correspond to the number 1 (e.g., French treats 0 as
one).
formatNumber(value, locale?)Formats a number according to locale conventions.
Signature:
formatNumber(value: number, locale?: string) → string
value is null, returns
null.locale is omitted, uses the active locale.1234.5 →
"1 234,5" for fr).Intl.NumberFormat in JavaScript,
locale.format_string in Python)."en" format.formatDate(value, pattern?, locale?)Formats a date string according to locale conventions.
Signature:
formatDate(value: string, pattern?: string, locale?: string) → string
value is an ISO 8601 date string. If null,
returns null.pattern is one of: "short",
"medium", "long", "full".
Defaults to "medium".locale is omitted, uses the active locale.A conformant locale processor MUST provide the following capabilities. The method names below are illustrative; implementations MAY use different API shapes provided the semantics are equivalent.
Register a Locale Document in the engine’s locale store.
$formspecLocale version
and targetDefinition binding before accepting the
document.locale code is
already loaded, the new document MUST replace it.Activate a locale, triggering reactive string resolution.
Resolve a single localized string for a given item, property, and optional context.
path — the item path (e.g., "projectName",
"budget[0].amount").property — the string property (e.g.,
"label", "hint",
"description").context — optional context name for alternative labels
(e.g., "short", "pdf")."" if no string is found at
any cascade level.Return the currently active BCP 47 locale code, or the empty string
"" if no locale is active.
Locale Documents MUST validate against
schemas/locale.schema.json. The schema enforces:
$formspecLocale,
version, locale,
targetDefinition, strings.strings MUST be an object with string values.locale MUST be a syntactically valid BCP 47 language
tag.targetDefinition.url MUST be a URI.A validator that has access to both a Locale Document and its target Definition SHOULD perform the following cross-reference checks:
| Check | Severity | Description |
|---|---|---|
| Orphaned key | Warning | String key references an item key not present in the Definition. |
| Missing translation | Info | A localizable property in the Definition has no corresponding key in the Locale Document. |
| Invalid option reference | Warning | An options.<value> key references a choice value
not present in the field’s choices. |
| Invalid shape reference | Warning | A $shape.<id> key references a shape ID not
present in the Definition. |
| Invalid property | Error | The property segment of a key is not a recognized localizable property. |
| Interpolation parse error | Warning | A {{...}} expression fails to parse as valid FEL. |
| Version mismatch | Warning | The Definition’s version does not satisfy
compatibleVersions. |
Orphaned $page key |
Warning | $page.<id> references a page ID not present in
the Theme Document. |
Orphaned $component key |
Warning | $component.<id> references a node ID not present
in the Component Document. |
Orphaned $optionSet key |
Warning | $optionSet.<setName> references an OptionSet name
not declared in the Definition. |
| Brackets in item key | Warning | A non-$component key contains [index]
bracket notation. Item-level keys MUST use template paths. |
The Python validator (src/formspec/validator/) SHOULD
implement the following locale-specific lint rules:
| Code | Description |
|---|---|
| L100 | Missing required top-level property. |
| L101 | Invalid BCP 47 locale code. |
| L200 | Orphaned string key — item not found in Definition. |
| L201 | Missing translation — localizable property has no key. |
| L202 | Invalid option value reference. |
| L203 | Invalid shape ID reference. |
| L300 | FEL interpolation parse error. |
| L301 | FEL interpolation references undefined variable. |
| L400 | Circular fallback chain detected. |
| L401 | Fallback locale not loaded. |
Locale string resolution is NOT part of the core four-phase processing cycle (Rebuild → Recalculate → Revalidate → Notify). String resolution is a presentation concern.
Conceptually, the processing layers are:
String resolution and theme cascade are orthogonal presentation concerns. In practice, both can run in parallel or in either order; the numbered list above represents conceptual layering, not a mandatory execution sequence.
Localized validation messages are resolved at render
time, not during the Revalidate phase. The core Revalidate
phase produces ValidationResult objects with
constraintKind and the inline (or processor-default)
message. The renderer (or a locale-aware presentation
layer) resolves the localized message by:
<itemKey>.errors.<code> in the
active locale cascade (synthesizing code from
constraintKind if absent — see §3.1.4).<itemKey>.constraintMessage or
<itemKey>.requiredMessage as appropriate.ValidationResult.message
as-is.This design means ValidationResult.message always
contains the inline/default-locale message. Localized messages are a
presentation overlay, not a mutation of the validation result.
String resolution is reactive. When any of the following change, all affected resolved strings MUST be re-evaluated:
String resolution changes are propagated through the implementation’s reactive notification mechanism (e.g., signals). These notifications are separate from the core Phase 4 Notify set — locale changes are presentation-layer events, not core data events.
Implementations using signals SHOULD create a computed signal for each resolved string that depends on the active locale signal and any field value signals referenced by interpolation expressions.
For items inside repeat groups, the string key uses the template path (without instance indices):
{
"lineItems.amount.label": "Montant",
"lineItems.description.label": "Description du poste"
}The same localized string applies to all instances of the repeated
item. Per-instance string customization is not supported — use FEL
interpolation with the @index repeat context variable (core
spec §3.2.2) if instance-specific text is needed:
{
"lineItems.label": "Poste {{@index}}"
}The @index variable is 1-based, so the above produces
“Poste 1”, “Poste 2”, etc.
Localized strings are rendered as text content. Renderers MUST sanitize string values before inserting them into HTML or other markup contexts. FEL interpolation results MUST be treated as untrusted text, not markup.
FEL expressions in interpolated strings are evaluated in a read-only
context with the same security model as calculate
expressions. They MUST NOT have side effects and MUST NOT access host
platform APIs beyond those exposed by the FEL stdlib.
When loading Locale Documents from external sources, the host application SHOULD verify document integrity and provenance using the same mechanisms applied to other sidecar artifacts (Theme, Mapping, Component Documents).
This specification defines two conformance levels:
| Level | Name | Description |
|---|---|---|
| 1 | Locale Core | Minimum viable locale support: cascade resolution, interpolation,
locale(). |
| 2 | Locale Extended | Full locale support: adds formatNumber(),
formatDate(), cross-reference validation, reactive
resolution. |
A Locale Core conformant processor MUST:
locale() FEL function (§5.1).A Locale Extended conformant processor MUST satisfy all Locale Core requirements and additionally MUST:
formatNumber() (§5.3) and
formatDate() (§5.4).A conformant Locale Document MUST:
The following is a complete Locale Document for a grant report form, demonstrating all key patterns defined in this specification.
{
"$formspecLocale": "1.0",
"url": "https://agency.gov/forms/grant-report/locales/fr-CA",
"version": "1.0.0",
"name": "grant-report-fr-CA",
"title": "Rapport de subvention — Français canadien",
"description": "Localisation française canadienne du formulaire de rapport de subvention.",
"locale": "fr-CA",
"fallback": "fr",
"targetDefinition": {
"url": "https://agency.gov/forms/grant-report",
"compatibleVersions": ">=1.0.0 <2.0.0"
},
"strings": {
// Form-level strings (§3.1.5)
"$form.title": "Rapport annuel sur les subventions",
"$form.description": "Formulaire de rapport pour les organismes bénéficiaires",
// Item labels, descriptions, hints (§3.1.1)
"projectName.label": "Nom du projet",
"projectName.hint": "Entrez le nom officiel tel qu'il apparaît dans l'entente",
"projectName.description": "Le nom complet du projet subventionné",
// Context labels (§3.1.2)
"budgetSection.label": "Section budgétaire",
"budgetSection.label@short": "Budget",
"budgetSection.label@pdf": "Section III : Informations budgétaires détaillées",
// Choice option labels (§3.1.3)
"fundingStatus.options.yes.label": "Oui",
"fundingStatus.options.no.label": "Non",
"fundingStatus.options.na.label": "Sans objet",
// Validation messages — per constraint code (§3.1.4)
"email.errors.REQUIRED": "L'adresse courriel est obligatoire",
"email.errors.CONSTRAINT_FAILED": "Veuillez entrer une adresse courriel valide",
// Validation messages — per Bind (§3.1.4)
"ssn.constraintMessage": "Le NAS doit être au format 000-000-000",
// Shape rule messages (§3.1.6)
"$shape.budget-balance.message": "Le total du budget doit correspondre au financement demandé",
// FEL interpolation (§3.3)
"totalItems.label": "Total : {{$itemCount}} {{if(pluralCategory($itemCount) = 'one', 'article', 'articles')}}",
"budgetRemaining.hint": "Il vous reste {{formatNumber($remaining)}} $",
// Repeat group with @index (§8.4)
"lineItems.label": "Poste budgétaire {{@index}}",
"lineItems.amount.label": "Montant",
"lineItems.description.label": "Description du poste",
// Page titles (§3.1.7)
"$page.info.title": "Informations du projet",
"$page.review.title": "Révision et soumission",
// OptionSet labels (§3.1.3)
"$optionSet.yesNoNA.yes.label": "Oui",
"$optionSet.yesNoNA.no.label": "Non",
// Component node strings (§3.1.8)
"$component.submitBtn.label": "Soumettre la demande",
"$component.mainTabs.tabLabels[0]": "Personnel"
}
}