Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Localization

Localization is a rendering concern shared by Constraints and Schema. Constraints produce Violation values; Schema carries them alongside parse and structural failures in path-aware SchemaErrors. Both use the same Renderer, with Schema supplying the field path. Install Reified.Constraint for direct rules or Reified.Schema for structured input (it includes Constraint as a dependency). Open Reified; no separate localization package is required.

Start with the renderer examples below, then read Constraints and localization in schemas for a complete field example. Continue with context and fallback, custom rules, the key catalogue, and finally adding a language if you need translations.

A Violation carries no language. Built-in failures carry Reified-owned identities and operands; opaque failures carry the prose their author wrote. Turning either into a sentence happens at the rendering edge, through a Renderer.

If you never translate

Violation.render needs no renderer, no resources, and no setup:

Verified
open Reified

42
|> Constraint.check (Constraint.between 0 10)
|> Result.mapError Violation.render
// Error "expected a value between 0 and 10, but was 42"
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)

Renderer.english is the same idea with the renderer mechanics available, bare predicates, composed nouns, and group joining, still without a resource file:

Verified
let violation =
    match Constraint.check (Constraint.atLeast 13) 11 with
    | Error violation -> violation
    | Ok () -> failwith "unreachable"

violation |> Violation.fullMessage Renderer.english
// "value must be at least 13, but was 11"
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val violation: Violation

Translate four keys

A renderer is a key-to-template lookup. The smallest useful one is a map:

Verified
let french =
    Map.ofList
        [ "constraint.presence.present", "doit être renseigné"
          "constraint.cardinality.minimum", "doit contenir au moins {minimum} éléments"
          "constraint.format.email", "doit être une adresse e-mail"
          "attribute.name", "Nom" ]

let renderer = Renderer.ofLookup french.TryFind
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val french: Map<string,string>
val renderer: Renderer

Keys missing from the map fall back to Reified's neutral English. You can translate only the keys your application uses.

Look keys up in the catalogue.

The two messages

Violation.message renders the failure with no attribute noun; when the violation carries an actual value, it is appended through the constraint.actual composition entry. Violation.fullMessage composes the attribute noun around that once. Using the violation and renderer from above:

Verified
let field = renderer |> Renderer.context "signup" |> Renderer.attribute "name"

violation |> Violation.message field      // "must be at least 13, but was 11"
violation |> Violation.fullMessage field  // "Nom must be at least 13, but was 11"
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val field: Renderer

The French map has no constraint.relation.atLeast entry, so the predicate falls back to Reified's neutral English; the attribute.name entry does resolve, which is why the noun is Nom.

Use message where a label already names the field, a form row, or a Schema result whose returned path identifies it. Use fullMessage for API payloads, logs, and anywhere the message stands alone.

The noun is composed once, never per leaf, so a group of three failures still names the field once:

Verified
let name : Constraint<string> =
    Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]

"" |> Constraint.check name |> Result.mapError (Violation.fullMessage field)
// Error "Name must be present and must have a size between 2 and 40, but was 0"
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)
val name: Constraint<string>

Building a renderer

Fragment
// Any key-to-template lookup. The portable constructor, and the one Fable uses.
Renderer.ofLookup translations.TryFind

// A .NET resource manager and one culture for lookup, plurals, and operand formatting.
Renderer.ofResourceManager resources culture

// Interface language and operand conventions separately: English text, German decimal separators.
Renderer.ofResourceManagerWithCultures resources uiCulture valueCulture

// CurrentUICulture and CurrentCulture, read on every render rather than captured.
Renderer.ofCurrentCulture resources

Renderer.english needs no resources at all: every entry falls back to Reified's neutral English. It is the default for tests, tools, and applications that never translate.

ofCurrentCulture is the one place ambient culture enters Reified. It reads the thread's cultures per render, so one renderer registered as a singleton follows a per-request culture. Constraint execution itself stays free of ambient effects.

Register a renderer as an ordinary immutable value. Reified introduces no renderer interface, no ambient registry, and no global configuration:

Fragment
services.AddSingleton(Renderer.ofCurrentCulture resources) |> ignore

Recipes

Do not show the rejected value

By default a failure that carries an actual value reads "must be at least 13, but was 11". The value clause is its own entry, so removing it is one override:

Verified
let quiet = Map.ofList [ "constraint.actual", "{message}" ]

violation |> Violation.message (Renderer.ofLookup quiet.TryFind)
// "must be at least 13"
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val quiet: Map<string,string>

Translate only the rules you actually use

Translate the identities your application produces, leave the rest, and check what is missing with Adding a language.

Translate one of your own rules

Give the rule a key and it joins the same lookup, see Custom rules.

Do not translate the English sentence

Rendering an English message before translation loses the rule's identity and operands. Look up the identity to cache translations, use consistent terminology, and format 13 for the target language.

Where next

Schema renders its own parse and structural failures through these same mechanics, with the field path supplied for you: see Redisplay and field errors.