Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultFable support
Reified.Constraint compiles to JavaScript through Fable, including the rendering edge. Constraints, violations,
descriptors, the catalogue, and Renderer all work there. What differs is culture: the browser has no .NET
resource manager and no CultureInfo machinery, so the constructors that depend on them are absent rather than
present-and-useless.
The portable renderer
Renderer.ofLookup is the constructor to use. It takes any key-to-template function:
open Reified
let violation =
match "" |> Constraint.check Constraint.present with
| Error violation -> violation
| Ok () -> failwith "unreachable"
let translations =
Map [ "constraint.presence.present", "doit être renseigné"
"attribute.signup.name", "Le nom" ]
let renderer =
Renderer.ofLookup translations.TryFind
|> Renderer.context "signup"
violation |> Violation.fullMessage (renderer |> Renderer.attribute "name")
// "Le nom doit être renseigné"
Reifiedviolation: Violation(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
check: Constraint<'value> -> 'value -> Result<unit,Violation>Runs a constraint, returning why the value failed. let retryCount = Constraint.between 0 10 42 |> Constraint.check retryCount |> Result.mapError Violation.render
present: Constraint<^value>Requires a value to be inhabited according to its shape. Whitespace-only text is blank, as are null text, a null or empty collection or map, None, ValueNone, and an empty Nullable. Blankness means .NET's whitespace set plus U+FEFF, which is what lets the rule be exported; see nonBlankPattern. The shape is selected from the return type, so a reusable binding needs its annotation: let requiredName : Constraint<string> = Constraint.present. Applied where the type is already known — inside an annotated rule, or to a schema — no annotation is needed. let requiredName : Constraint<string> = Constraint.present
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
failwith: string -> 'TThrow a exception. The exception message. Never returns. let failingFunction() = failwith "Oh no" // Throws an exception true // Never reaches this failingFunction() // Throws a System.Exception
translations: Map<string,string>``.ctor``: (string * string) seq -> Map<string,string>Builds a map that contains the bindings of the given IEnumerable. The input sequence of key/value pairs. The resulting map. Map [ (1, "a"); (2, "b") ] // evaluates to map [(1, "a"); (2, "b")]
renderer: RendererReified.RendererModuleBuilds renderers and renders messages through them.
ofLookup: MessageLookup -> RendererA renderer backed by any key-to-template lookup. The portable constructor, and the one Fable applications use. let translations : Map<string, string> = Map.empty let renderer = Renderer.ofLookup translations.TryFind
TryFind: string -> string optionLookup an element in the map, returning a Some value if the element is in the domain of the map and None if not. The input key. The mapped value, or None if the key is not in the map. let sample = Map [ (1, "a"); (2, "b") ] sample.TryFind 1 // evaluates to Some "a" sample.TryFind 3 // evaluates to None
context: string -> Renderer -> RendererAppends a document, model, form, or component segment. let signup = renderer |> Renderer.context "signup"
Reified.ViolationModuleInspects, traverses, and renders violations.
fullMessage: Renderer -> Violation -> stringRenders a violation as a complete sentence fragment, with the attribute noun composed once. The noun wraps the finished message, never each leaf: a group of three failures still names the field once. With no attribute the contextual constraint.attribute.default supplies the noun, so an unattributed violation reads "Value must be present" rather than borrowing the document context. violation |> Violation.fullMessage (signup |> Renderer.attribute "name") // "Name must be present"
attribute: string -> Renderer -> RendererReplaces the attribute with one segment. Replacement, not append: a form-scoped renderer stays reusable for sibling fields. Schema supplies its typed path through Renderer.Advanced.attributePath instead. signup |> Renderer.attribute "name"
Requires & types
Requires: open Reified (packages: Reified.Constraint)val violation: Violationval translations: Map<string,string>val renderer: RendererA Map, an object fetched as JSON, or a function reaching into an existing i18n library all satisfy
MessageLookup. Reified owns the candidate order, so a lookup only ever answers "do you have this exact key".
Everything the Localization page describes about candidate order, contextual fallback,
.one/.other selection, named interpolation, attribute nouns, humanization, segment encoding, and group and list
joining behaves identically under Fable. So does Renderer.english, Renderer.Advanced.ofResolver, and the whole
inspection surface.
What is absent
These three constructors do not exist in a Fable build:
Renderer.ofResourceManager
Renderer.ofResourceManagerWithCultures
Renderer.ofCurrentCultureCalling one is a compile error under Fable, which is the point. A .NET resource constructor that compiled to a silent no-op would produce untranslated English in production with nothing to catch it.
If a codebase is shared between a .NET host and a Fable client, put the constructor behind the one conditional you already have:
let renderer =
#if FABLE_COMPILER
Renderer.ofLookup translations.TryFind
#else
Renderer.ofCurrentCulture resources
#endifNothing else in the localization path needs conditioning. Violation.message, Violation.fullMessage,
Renderer.context, Renderer.attribute, and SchemaErrors.fullMessages are the same calls on both targets.
Operand formatting
The built-in formatter under Fable renders operands with the portable invariant rendering and ignores placeholder
format suffixes. {divisor:N0} renders as though it were {divisor}.
Placeholder parsing is identical on every target, the suffix is recognized and stripped, not left in the output, but there is no culture to format through, and inventing one would give the browser and the server two different numbers for the same constraint.
Supply your own formatter when the suffix matters. Intl.NumberFormat is usually what you want:
open Reified
let translations = Map [ "constraint.presence.present", "doit être renseigné" ]
/// Stands in for a real Intl.NumberFormat call under Fable.
let formatNumber (value: float) = value.ToString()
let renderer =
Renderer.ofLookup translations.TryFind
|> Renderer.Advanced.withValueFormatting (fun request ->
match request.Value, request.Format with
| ConstraintValue.Integer value, Some _ -> formatNumber (float value)
| value, _ -> ConstraintValue.render value)
Reifiedtranslations: Map<string,string>``.ctor``: (string * string) seq -> Map<string,string>Builds a map that contains the bindings of the given IEnumerable. The input sequence of key/value pairs. The resulting map. Map [ (1, "a"); (2, "b") ] // evaluates to map [(1, "a"); (2, "b")]
formatNumber: float -> stringvalue: floatfloatAn abbreviation for the CLI type . Basic Types
ToString: unit -> stringrenderer: RendererReified.RendererModuleBuilds renderers and renders messages through them.
ofLookup: MessageLookup -> RendererA renderer backed by any key-to-template lookup. The portable constructor, and the one Fable applications use. let translations : Map<string, string> = Map.empty let renderer = Renderer.ofLookup translations.TryFind
TryFind: string -> string optionLookup an element in the map, returning a Some value if the element is in the domain of the map and None if not. The input key. The mapped value, or None if the key is not in the map. let sample = Map [ (1, "a"); (2, "b") ] sample.TryFind 1 // evaluates to Some "a" sample.TryFind 3 // evaluates to None
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
withValueFormatting: (ValueFormatRequest -> string) -> Renderer -> RendererReplaces operand formatting with a callback that receives the placeholder's format suffix. renderer |> Renderer.Advanced.withValueFormatting (fun request -> myFormat request.Value request.Format)
Reified.RendererModule.AdvancedReplacing lookup, formatting, or inspecting the exact keys Reified will ask for.
request: ValueFormatRequestValue: ConstraintValueThe operand being formatted.
Format: string optionThe text after : in the placeholder, as in {divisor:N0}.
Reified.ConstraintValueThe closed set of operand and actual-value representations a constraint may carry across a description, diagnostic, or localization boundary. A value is admitted only when the representation is lossless in the semantics Reified's runtime diagnostics and exporters use. Semantic sorts keep their own case rather than being flattened into Text because their wire rendering happens to be textual: an instant and the string spelling it are different facts, and an interpreter that cannot tell them apart cannot decide whether wire equality substitutes for typed equality. Guid and TimeSpan are reached through typed dispatch rather than a runtime type test. Fable erases a Guid to a plain string and a TimeSpan to a number, so a boxed type test silently labels them Text and Integer there while .NET labels them correctly — the same constraint meaning two different things per platform. ConstraintValue.ofOperand resolves the overload at the call site, where the type is still known, so both platforms agree. Values outside this set are never boxed through the public surface. The constraint still executes against its private typed closure; the atom describes and fails as UnsupportedOperand instead. This is not a solver literal theory. A later proof phase declares its own numeric, date, and string sorts and adds a translation from the cases here; nothing may be added to this type for the solver alone.
IntegerAn integral value that fits a signed 64-bit integer.
value: int64SomeThe representation of "Value of type 'T" The input value. An option representing the value.
float: ^T -> floatConverts the argument to 64-bit float. This is a direct conversion for all primitive numeric types. For strings, the input is converted using Double.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted float float 'A' // evaluates to 65.0 float 0xff // evaluates to 255.0 float -10 // evaluates to -10.0
value: ConstraintValueReified.ConstraintValueModuleBuilds and renders portable constraint values.
render: ConstraintValue -> stringRenders a portable value for a default English message. Not a wire format.
Use Renderer.withValues instead when one uniform rendering is enough and suffixes are irrelevant.
Operand agreement across targets
A constraint must mean the same thing on both runtimes, and its message must too. Two behaviours matter here.
Fable erases a Guid to a string and a TimeSpan to a number, so a boxed type test labels them Text and
Integer there while .NET labels them correctly. Reified's constructors resolve the operand at the call site, where
the type is still concrete, so the same constraint describes itself identically, and therefore interpolates
identically, on both targets. This is checked by the shared Fable surface test, not assumed.
Blankness and text length are the other pair. Text sizes count Unicode code points rather than UTF-16 code units,
and whitespace is defined the same way on both runtimes, so the constraint.actual value clause the composed
message adds reports the same {actual} for the same string in the browser and on the server.
Descriptors and comparison
MessageDescriptor keeps structural equality under Fable, so violations carrying one compare equal across
independently constructed values:
open Reified
MessageDescriptor.Advanced.create "books.isbn.invalid" Map.empty
= MessageDescriptor.Advanced.ofSegments [ "books"; "isbn"; "invalid" ] Map.empty
// true, on both targets
ReifiedReified.MessageDescriptorModuleReads and builds message identities.
create: string -> Map<string,ConstraintValue> -> MessageDescriptorParses a relative key, raising for a malformed programmer-authored key. A key written in source is either right or a defect. Failing at construction is what keeps rendering free of a malformed-descriptor branch. MessageDescriptor.Advanced.create "books.isbn.invalid" Map.empty
Reified.MessageDescriptorModule.AdvancedBuilding descriptors from keys that were not written by the calling programmer.
Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
empty: Map<'Key,'T>The empty map. let emptyMap = Map.empty<int, string>
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
ofSegments: string list -> Map<string,ConstraintValue> -> MessageDescriptorBuilds a descriptor from already-parsed segments, skipping the parse. For a generated catalogue whose segments are known at build time, so a render does not reparse a key it has already validated. Segments are unencoded and may contain any character except that an empty segment, or no segments at all, is rejected. MessageDescriptor.Advanced.ofSegments [ "schema"; "omitted" ] Map.empty
Requires & types
Requires: open Reified (packages: Reified.Constraint)That is what lets a Fable client compare a violation it received against one it computed, and what lets a test assert on a whole violation value rather than on its rendered text.
Shipping the translations
The lookup is a plain function, so how the bundle arrives is entirely yours. Two shapes work well:
// Bundled at build time. One Map per language, selected once.
let renderer = Renderer.ofLookup (catalogue culture).TryFind
// Fetched at runtime. Build the renderer after the fetch resolves; it is an immutable value,
// so there is no registry to update.
let renderer = Renderer.ofLookup (fun key -> loaded |> Map.tryFind key)Generate the bundle from Catalogue.keys and SchemaMessages.keys rather than transcribing it, see
Adding a language. Since the same catalogue drives both targets, one generated bundle
serves the browser and the server.
AOT and trimming
The rendering path uses no runtime reflection. Message identities are generated data, plural selection is a value comparison, and interpolation is string parsing, so a NativeAOT or trimmed .NET build has nothing to preserve beyond the resources themselves. See AOT, trimming, and Fable for the wider picture across packages.