# lingo > Make forms easier, LLM tools safer. Zero-dependency TypeScript library that parses > natural-language quantities, units, dates and ranges ("2 ft", "5'11\"", "72 in to cm", > "between 5 and 10 kg", "three days ago", "it's hot") into canonical SI-anchored > values; converts, validates, formats, and humanizes. Entries: > `@pascal-app/lingo` (core+units+fuzzy), `@pascal-app/lingo/date`, `@pascal-app/lingo/dom`, > `@pascal-app/lingo/element` (``), `@pascal-app/lingo/react`, > `@pascal-app/lingo/ai` (LLM tool fields), `@pascal-app/lingo/mcp` (MCP tool helper), > `@pascal-app/lingo/describe` (rich value/result descriptions), > `@pascal-app/lingo/catalog` (query units/kinds/currencies + ISO country codes), > `@pascal-app/lingo/schema` (JSON Schema + OpenAPI + enum reference), `@pascal-app/lingo/core`. Agent fetch order: read this file first, then fetch `https://lingo.pascal.app/llms.md` for the complete docs page or `https://lingo.pascal.app/docs#forms-range-slider` for the two-way field demo. Keep user measurements as strings in tool schemas; call lingo to convert, validate, surface spans, and handle ambiguity. **Core API (lingo).** - `lingo(text, opts?) → LingoResult`: versioned union on `.type`: 'quantity' | 'range' | 'conversion' | 'number' | 'failure'. Serialized as FLAT v3 JSON: `{ schemaVersion: 3, ok, type, ...value fields at top..., text, span, issues, confidence }` — a quantity inlines `kind, value, unit, base, baseUnit` (no `quantity` wrapper); a conversion has `source` + `converted:{value,unit}` (no `targetUnit`, no `converted.base`); failures add `candidate?`. Every `span` is `{ start, end, text }` where `text = input.slice(start, end)`. - `parseQuantity(text, opts?) → { ok, quantity, issues, confidence, span, alternatives? }`: conversion requests resolve to their target unit. - `parseRange(text, opts?)`: single values become [v,v]. - `partialState(text, opts?) → 'empty'|'incomplete'|'valid'|'invalid'`: as-you-type; "2 f" is incomplete, never invalid. - `findQuantities(text, opts?) → { result, span: { start, end } }[]`: free-text scan. - `quantity(value, unitRef, kind?) → Quantity` · `convert(v, from, to) → number` · `tryConvert(v, from, to) → { ok:true, type:'conversion', value, unit, kind } | { ok:false, type:'failure', issues }` · `convertDelta(v, from, to)` (temperature-safe differences: 5°C rise = 9°F rise). - `fromJSON(json)` rehydrates `toJSON()` output. - `describeResource(quantityOrRange)` from `@pascal-app/lingo/describe` returns `lingo.quantity` / `lingo.range` resource primitives — grouped value/canonical amounts with rich units `{id,symbol,name,plural?,system?}` and `formatted` — the same primitives used inside result resources; `describeResult(result)` accepts `lingo()` plus `parseDate()`/`parseDuration()` results and returns a resource-style view with `object`, `resourceSchemaVersion`, grouped value/canonical amounts, `canonicalUnit` for range roots, source text spans, rich units, issues, alternatives, candidates, conversion `{source,target:{unit},converted}` data, `lingo.date` `{value:{iso,epochMilliseconds},calendar,grain,known}`, and `lingo.duration` `{value,canonical,formatted,parts?}`. Keep `toJSON()` for compact wire storage. - `createLingo({ registry?, kinds?, messages?, fuzzy? }) → instance`: isolated parse/convert/format surface (own registry+messages+fuzzy; inputs snapshotted, no cross-instance leaks; global `lingo()` is itself a createLingo singleton). Built-in instances keep the same literal-unit checks as top-level `quantity`/`convert`/`tryConvert`; custom registry instances keep broad string refs. SSR/multi-tenant/test safe. - Result helpers: `firstError(r)`, type guards `isQuantity(r)`/`isRange(r)`/`isConversion(r)`, `candidateOf(r)`, `formatIssue(issue, messages?)`. - Extension: `registerKind(def)`, `registerUnits(kind, units)`, `defineFuzzyVocab(kind, vocab)`, `createRegistry(kinds)` for isolation. - `describeTemperature(q, { profile: 'weather'|'water'|'oven' })`: reverse of fuzzy parse. - Query data (`@pascal-app/lingo/catalog`): `listKinds()`, `listUnits(kind)`, `kindInfo(kind)`, `getUnit(ref, kind?)` (resolves any id/symbol/name/alias → `{kind,id,symbol,name,plural,system,aliases,isBase,toBase,intl?,minorUnit?}`), `kindOf(ref)`, `relatedUnits(ref)`, `listCurrencies()`, `getCurrency(ref)` (`{code,symbol,name,plural,minorUnit,countries,aliases}`), `currencyForCountry(iso)`. - Schema (`@pascal-app/lingo/schema`): `lingoJsonSchema` (Draft 2020-12 of the v3 wire types), `toOpenApi()` (OpenAPI 3.1), and `BUILTIN_KINDS`/`SEVERITIES`/`ISSUE_CODES`/`UNIT_SYSTEMS`. Docs also ship generated Zod/Valibot/TypeBox/ArkType/Effect schemas + a dictionary. Options `{ kind?, unit?, currency?, system?: 'us'|'imperial'|'metric', numberFormat?: 'auto'|'dot-decimal'|'comma-decimal', strictness?: 'forgiving'|'confirm'|'strict', accept?, tolerance?, escalate?, messages?, profile?, registry? }`. `kind` biases unit resolution ("5m" = minutes under kind 'duration'); `unit` is assumed for bare numbers ("72" in a cm field → 72 cm + UNIT_ASSUMED info); `currency` disambiguates bare currency symbols and ambiguous minor-unit words (`$5` + `{currency:'CAD'}` → CAD, `50 cents` + `{currency:'EUR'}` → 0.5 EUR, no ambiguity warning). GBP pence forms (`50p`, `3 quid 50`) are explicit GBP. `strictness:'confirm'` escalates assumption-class warnings/infos to errors and attaches `candidate`; `strictness:'strict'` also disables typo fixing, number words, fuzzy vocab, approximations and bare-number assumptions. `accept` switches: ranges conversions compounds fuzzy numberWords approximations bareNumbers. `tolerance.typos`: fix|suggest|off; `tolerance.ambiguity`: assume|confirm. `escalate` maps any issue code to error|warning|info. `messages` customizes copy for any code. Fail results are `{ schemaVersion: 3, ok: false, type: 'failure', text, issues, candidate? }`. `candidate` is the full successful result the parser would have returned. Candidate examples: ranges-off returns SINGLE_VALUE_EXPECTED + candidate range; conversions-off returns CONVERSION_NOT_ALLOWED + candidate conversion; confirm typo returns TYPO_CORRECTED error + candidate quantity. Success `alternatives` are discriminated: quantity alternatives use `{ type:'quantity', quantity, reason, confidence }`; date alternatives use `{ type:'date', date, reason, confidence }`. Quantity: `.value` (in `.unit`) · `.base` (canonical SI or self-canonical currency) · `.kind` · `.to(unitRef)` · `.valueIn(u)` · `.convertDelta(u)` · `.toMinor()` for currency · `.format({ unit?, compound?, style?: 'symbol'|'long'|'narrow', precision?, significant?, locale?, grouping?, localizedUnits?, notation?: 'standard'|'scientific'|'engineering', exponentStyle?: 'e'|'times'|'superscript' })` · `.toBest({ system?, cutOff?, exclude? })` · `.toJSON()`; default formatting is parseable; `localizedUnits:true` is display-only. Use `@pascal-app/lingo/describe` for symbol/name/formatted metadata and readable parse-result resources. QuantityRange: `.minBase/.maxBase/.min()/.max()/.plusMinus/.fuzzy/.contains(q)/.widthIn(u)/.to(u)/.format()`. Kinds: length mass temperature duration volume area speed data data_rate flow_rate acceleration pressure energy force torque power frequency angle percent luminous_intensity luminous_flux illuminance luminance voltage current resistance charge substance concentration radiation_absorbed_dose radiation_equivalent_dose radioactivity currency. Bases: m, kg, K, s, m³, m², m/s, byte, bit/s, m³/s, m/s², Pa, J, N, N⋅m, W, Hz, rad, %, cd, lm, lx, cd/m², V, A, Ω, C, mol, mol/m³, Gy, Sv, Bq, and self-canonical ISO currency codes. **Dates (@pascal-app/lingo/date).** - `parseDate(text, { now?, dayFirst?, weekStart?=1, forwardDates?=true, applyZone?, locale?, strictness?, escalate?, messages? }) → { ok, date, grain, known, zone?, issues, confidence, alternatives? }`. Inputs that depend on a reference time ("in 2d", "tomorrow", "next tues", "March 5", "at 3pm") fail with NOW_REQUIRED unless explicit `now` is passed; fully absolute dates never require it, and no wall-clock candidate is attached. Serializes as flat v3 JSON like `lingo()` results: `{ schemaVersion: 3, ok, type: 'date', date: '', grain, known, text, span: { start, end, text }, issues, confidence }` — `parseDateRange`/`parseDuration` results serialize the same way (ISO endpoint dates; durations inline `kind, value, unit, base, baseUnit`). - Coverage: today/tomorrow/tmr/tmrw/tmrw./yesterday/yday/y'day/tonight/tonite/day after tomorrow/just now; "in 3 days", "in 2d", "in 5h", "3min from tmrw", "three days ago", "2w ago", "3mo from now", "a week from friday"; weekdays bare/this/next/last with shorthand ("next tues", "this thurs", "on fri") and deterministic chrono semantics; next/last week/month/year; beginning/end/mid of month/year; ISO; 5/3 with dayFirst + AMBIGUOUS_DATE alternative; March 5th 2026; month-end clamping; DST-safe civil arithmetic. - Time of day: "at 3pm", "@ 3pm", "5.30pm", "17:30", "17h"/"17h30", "5 o'clock", "quarter past 5", "half past 3", British "half 5" (=5:30), "twenty to 6", "ten past four", military "0900 hours"/"1730 hrs", French "midi"/"minuit", "noon"/"midnight"/"midday", "in the morning/afternoon/evening". - Timezones: a trailing zone is DETECTED and EXPOSED on `.zone` (`{ source: 'offset'|'abbrev'|'iana'|'named', text, offsetMinutes, iana?, abbreviation?, ambiguous?, applied }`) while the civil wall-clock is kept by default; pass `applyZone:true` to resolve the real UTC instant. Explicit offsets ("+05:30", "UTC+2", "GMT-5", "Z"), abbreviations (EST/PST/CET/IST… → ambiguous, `AMBIGUOUS_TIMEZONE` warning), IANA names ("Europe/Paris", DST-correct via `Intl`), and named zones ("Eastern", "Pacific Time"). Detected-not-applied rides a `TZ_IGNORED` signal (softened from the old blanket-ignore; escalates to error at the `/ai` boundary). - `parseDateRange(text, opts?) → { ok, type:'date-range', start?, end?, issues, confidence, span, text }`: time slots. "2pm to 4pm", "between 9am and 5pm", "9-5"/"9 to 5" (workday shift → 9am–5pm), "2 to 4pm" (am/pm inferred across the pair), "from 3pm"/"until 5" (open-ended), "10pm to 2am" (cross-midnight → end next day). Each endpoint is `{ date, grain, known, zone? }`; a trailing zone applies to the whole slot; reference-dependent, so `now` is required (else NOW_REQUIRED). - `parseDuration(text) → { duration: Quantity(kind duration, base seconds) }`: "1h30", "90 min", "an hour and a half", ISO "PT1H30M", "1:30" (h:mm). - `humanizeDate(date, { now, maxUnit?, minUnit?, justNowUnder?, numeric?: 'auto'|'always', rounding? }) → "3 days ago"`: output always re-parses within one grain. - `humanizeDateRange(range, { hour12?=true }) → "2:00 PM to 4:00 PM"`: inverse of `parseDateRange` (round-trips to the same civil times); "from 9:00 AM"/"until 5:00 PM" for open ends; `hour12:false` for 24h "14:00". - `humanizeDuration(secondsOrQuantity, { largest?=2, style?: 'narrow'|'short'|'long'|'natural' }) → "1 h 30 min"`. **DOM (@pascal-app/lingo/dom).** `lingoInput(el, { kind, unit, displayUnit?, display?: 'canonical'|'echo'|'preserve', strictness?, accept?, tolerance?, escalate?, min?, max?, required?, name?, errorElement?, hintElement?, formatCandidate?, validationBehavior?: 'native'|'aria', messages?, debounce? }) → field`. `field.value` (canonical number) · `.quantity` · `.result` · `.state` ('idle'|'incomplete'|'valid'|'invalid') · `.set(numberOrString)` · `.commit()` · `.update(opts)` · `.destroy()`. Static `lingoInput.get(el)`. Behavior: parses on input (debounced, IME-safe, untrusted events honored), never rewrites while typing, canonicalizes on blur/Enter/submit, hidden input `name` submits the canonical value, ARIA error surfacing (aria-invalid, describedby only-while-error, visual hints / assertive commit errors), did-you-mean hint when invalid result has candidate, data attributes: data-lingo, data-state, data-touched, data-dirty, data-invalid, data-valid, data-approx, data-canonical, data-unit; bubbling CustomEvent 'lingo:change'. React: `useLingoInput(opts) → { ref, state, value, quantity, set, commit }` from @pascal-app/lingo/react. **Forms UX examples.** The `/docs#forms-ux` website section and `/llms.md?section=forms-ux` markdown mirror show the same before/after matrix: - Finance: Without lingo, budget value plus currency picker, rate value plus %/bps mode, settlement value plus duration unit. With lingo: `1250 usd`, `25 bps`, `2 days`. The app stores USD, percent, and days. Currency is built in; no live FX implied. - Recipes: Without lingo, ingredient amount plus cup/mL/L picker, oil amount plus tsp/tbsp/mL picker, cook-time picker. With lingo: `1.5 cups`, `2 tbsp`, `45 min`. The app stores mL and minutes while accepting prep-sheet wording. - Engineering: Without lingo, clearance, pressure, axial load, and torque each require their own unit selector. With lingo: `3/4 in`, `32 psi`, `120 lbf`, `35 Nm`. The app stores mm, kPa, N, and N⋅m. Force and torque are built in. - Fitness: Without lingo, body weight, distance, duration, and energy each carry a separate mode choice. With lingo: `165 lb`, `5 km`, `42 min`, `450 Calories`. The app stores kg, meters, minutes, and kcal for analytics. - Medical: Without lingo, dose volume, interval, height, and cuff pressure selectors create a noisy intake layout. With lingo: `1.5 tsp`, `6 hr`, `5'11"`, `120 mmHg`. The app stores field-local canonical values; medication units remain domain-owned. **AI (@pascal-app/lingo/ai).** For LLM structured output + agent form filling. Fields implement BOTH Standard Schema halves (StandardSchemaV1 validate + StandardJSONSchemaV1 jsonSchema) and drop into AI SDK v6/v7 `tool({inputSchema})`/`Output.object({schema})` directly, no Zod, no wrapper (v5's `asSchema()` has no Standard Schema branch; wrap with the SDK's own `jsonSchema()` first; `generateObject`/`experimental_repairText` are deprecated on v6/v7). The same fields drop into LangChain's `createAgent`/`withStructuredOutput` too (shape-validate only; run `canonicalizeValues` yourself for lingo's ambiguity/bounds checks) and any Standard-Schema-aware form resolver (react-hook-form's `standardSchemaResolver`, TanStack Form, vee-validate v5, Angular Signal Forms). One `lingoObject` validates an LLM tool argument and canonicalizes a human form; `{passthrough:true}` if the form submits fields lingo doesn't declare. Input JSON Schema is `type:"string"` + description (emit natural language; lingo canonicalizes); output is the canonical shape. Most schema adapters (AI SDK included) only read `{success,value}`/`{success,error}` from `validate()`. Warnings from lingo are dropped on that path; call `field.safeParse()` directly to see warnings and structured failure issues. Provider portability: the emitted schema is always `{type:'string', description}` (a closed object for `lingoObject`). It is strict-safe on OpenAI (wrap in `lingoObject`, set `strict:true`) and Anthropic (cast the emitted schema to `Tool.InputSchema`) with zero changes. Target Gemini's `parametersJsonSchema`/`responseJsonSchema`, never classic `parameters`/`responseSchema` (the classic path silently drops or rejects `additionalProperties`). Grok, Mistral, Cohere v2, Groq, Ollama and Hugging Face all accept the identical OpenAI-shaped JSON Schema. Tool-boundary defaults (plan 020, each has a one-line escape hatch): AMBIGUOUS_NUMBER escalates to error with a did-you-mean candidate ("1,234 kg" fails; the model re-emits "1234 kg"); dateField escalates TZ_IGNORED to error and REQUIRES an explicit `now` for reference-dependent inputs ("tomorrow", "March 5", and time-only "at 3pm" fail with NOW_REQUIRED and no candidate; `requireNow:false` opts out by resolving from the wall clock at field validation; fully absolute dates unaffected); `min`/`max` bounds reject absurd magnitudes with RANGE_MIN/RANGE_MAX, advertised in the input description on every field (numeric outputs also carry JSON Schema minimum/maximum; date-only bounds are LOCAL calendar days, min = start, max = inclusive end); numeric rangeField output rejects open-ended inputs with RANGE_OPEN_BOUND_NOT_ALLOWED, while `output:'range'` returns full QuantityRange JSON with open bounds/exclusivity/fuzzy/approximate/baseUnit context; lingoObject is CLOSED (additionalProperties:false, unknown keys fail, OpenAI-strict compatible; `{passthrough:true}` restores open); benign forgiveness (typos, qualifiers, UNIT_ASSUMED, AMBIGUOUS_DATE under dayFirst) still succeeds and is surfaced as `warnings: [{code,severity,message,span,data?}]` on `safeParse` success; failure issues include Standard Schema `message`/`path` plus optional lingo `code`, `severity`, field-input `span`, `data`, `suggestions`, and `candidate`; canonical numbers are float-safe. - `quantityField({ kind, unit, min?, max?, description?, ...LingoOptions }) → LingoField`: "2 lbs" → 0.907 (number in `unit`; `output:'quantity'` for Quantity JSON). Also `.parse(v)` (throws) / `.safeParse(v)`. - `rangeField(opts) → { min, max }` by default; `output:'range'` returns QuantityRange JSON. `dateField({ now?, requireNow?, applyZone?, min?, max?, ... }) → ISO string` (canonicalizes "tomorrow"/"March 5th"/"2026-07-03"; `applyZone:true` resolves a trailing zone to the real instant). - `dateRangeField({ now?, requireNow?, applyZone?, ... }) → { start?, end?: ISO }`: canonicalizes a natural-language time slot ("2pm to 4pm", "between 9am and 5pm", "9-5", "from 3pm") with the same reference-time + timezone guards as `dateField`. - `lingoObject(shape, { passthrough? })`: zero-dep object combinator: lingo fields, nested objects, `[field]` arrays, `'string'|'number'|'boolean'` passthrough. - `optional(field) → LingoField`: `null`/`undefined` validate to `null`, else delegates; JSON Schema turns nullable (`type:[T,'null']` for scalar fields, `anyOf` with `{type:'null'}` otherwise); the object key stays in `required`, which is the null-union idiom OpenAI/Anthropic strict modes both expect. - `toJSONSchema(field, { io?: 'input'|'output', target? }) → object`: named wrapper over `field['~standard'].jsonSchema[io]({target})` (defaults `io:'input'`, `target:'draft-2020-12'`), for raw provider SDKs (OpenAI `parameters`, Anthropic `input_schema`, MCP `inputSchema`) that want the fragment, not the field. - `repairTextWith(spec)`: `experimental_repairText`-compatible (AI SDK, deprecated with `generateObject` but still valid on v5); client-side, no extra model call; warnings don't block repair, errors do. - `repairToolCallWith(specsByTool)`: `experimental_repairToolCall`-compatible (AI SDK v6/v7 `generateText`/`streamText`/`ToolLoopAgent`): keyed by tool name, `JSON.parse`s the tool-call input, canonicalizes via the matching spec/field, returns `{...toolCall, input}` or `null` if no matching spec, invalid JSON, or an error-severity issue remains. - `quantityMatch(actual, expected, { kind, unit, tolerance?, ...LingoOptions }) → {pass,score,reason}` / `dateMatch(actual, expected, { grain?, timeZone?, ...DateFieldOptions }) → {pass,score,reason}`: eval graders canonicalize both sides through the same field before comparing (relative-error tolerance for quantities, default 1e-9; grain-truncated ISO-instant equality for dates, default grain 'day'), duck-typed to promptfoo's GradingResult. Adapts to autoevals/LangSmith/Vitest. - `canonicalizeValues(value, spec)`: walk any payload; spec maps dot-paths + `items[]` wildcards to field defs; returns { value, issues } with full paths + `severity` ('error' blocked the path; 'warning'/'info' rode along on an applied value) + structured lingo fields (`code`, field-input `span`, `data`, `suggestions`, `candidate`) when known. - MCP tools: prefer `lingoTool()` (`@pascal-app/lingo/mcp`, below); by hand, `field['~standard'].jsonSchema.input({target:'draft-2020-12'})` is the tool inputSchema. Run `safeParse` in the handler and return the `[CODE]`-prefixed issue messages as the tool error; the model self-corrects on the next call. - Tree-shakes: quantityField-only costs ~1.9 kB over the core lib; dateField brings the date engine. - Computer use: fields wired with lingoInput (see DOM) canonicalize agent-typed values through plain synthetic events. "5'11\"" commits 1.8034 to the hidden input. **Element (@pascal-app/lingo/element).** `defineLingoInput(tag = 'lingo-input')` registers a form-associated custom element wrapping `lingoInput` (see DOM). `` holds a light-DOM ``, submits its canonical value via `ElementInternals.setFormValue`/`setValidity` instead of a hidden input, and implements `formResetCallback`/`formDisabledCallback`. `.field` (LingoField|null) and `.value` (number|null) are readable on the element instance. Framework-agnostic: Vue, Svelte, Angular, plain HTML. **MCP (@pascal-app/lingo/mcp).** `lingoTool({ name, description, input: shape, passthrough?, handler }) → McpTool` builds a complete MCP tool descriptor from a `lingoObject` shape: `inputSchema` is the closed JSON Schema (`additionalProperties:false` unless `passthrough`), and `callback(raw)` runs `safeParse` before `handler`, returning `{isError:true, content:[{type:'text', text}]}` with `[CODE]`-prefixed messages on failure so the model self-corrects. Zero-dep; bring your own MCP SDK. Shaped for `server.registerTool(t.name, {description:t.description, inputSchema:t.inputSchema}, t.callback)`. **Issues.** `{ code, severity: 'error'|'warning'|'info', message, span: { start, end, text }, suggestions?, data? }`. `span.text` is `input.slice(start,end)` — the matched substring — so the span reads for itself. `data` is typed per code via `IssueDataMap` (`LingoIssue<'UNKNOWN_UNIT'>` → `data: { unit, suggestions? }`). `ok:false`/`type:'failure'` means at least one error. Codes: EMPTY NO_VALUE UNKNOWN_UNIT KIND_MISMATCH RANGE_KIND_MISMATCH CONVERSION_KIND_MISMATCH RATE_REQUIRED TRAILING_INPUT SINGLE_VALUE_EXPECTED APPROX_NOT_ALLOWED UNIT_REQUIRED CONVERSION_NOT_ALLOWED NUMBER_FORMAT NONFINITE RANGE_MIN RANGE_MAX RANGE_OPEN_BOUND_NOT_ALLOWED REQUIRED UNSUPPORTED_DATE NOW_REQUIRED TYPO_CORRECTED AMBIGUOUS_NUMBER AMBIGUOUS_UNIT AMBIGUOUS_DATE RANGE_REVERSED COMPOUND_OVERFLOW CIVIL_AVERAGE UNIT_ASSUMED WEEKDAY_ASSUMED_NEXT SLANG_UNIT TZ_IGNORED AMBIGUOUS_TIMEZONE. Messages overridable via `messages` option map. **Canonical examples (input → essence).** "2 ft" → quantity length base 0.6096 m · "5'11\"" → 1.8034 m (parts ft+in) · "72 in to cm" → conversion, converted 182.88 cm · "60 miles an hour" → speed in m/s · "5 cubic feet" → volume in m³ · "approx. 5 kg" → approximate mass · "1m80" → 1.8 m · "1h30" → 5400 s · "2 lb 3 oz" → 0.9922 kg · "$5" → currency USD value/base 5 baseUnit USD + AMBIGUOUS_UNIT; pass `{currency:'CAD'}` to read bare "$" as CAD · "50 cents" → 0.5 USD + AMBIGUOUS_UNIT; pass `{currency:'EUR'}` to read as EUR · "five dollars and fifty cents" → 5.5 USD · "50p" → 0.5 GBP · "3 quid 50" → 3.5 GBP · "€5-€10" → currency range baseUnit EUR · "5 EUR to USD" → ok:false RATE_REQUIRED (use convertCurrency with injected rates) · "between 5 and 10 kg" → range 5..10 kg · "under 10 minutes" → range max 600 s exclusive · "no greater than 5 kg" → range max 5 kg inclusive · "10 ± 0.5 mm" → plusMinus center 10 mm/base 0.01 and delta 0.5 mm/base 0.0005 · "a few minutes" → range 120..240 s approximate · "it's hot" (kind temperature) → range 300.15..308.15 K fuzzy 'hot' · "1,5 kg" → 1.5 kg · "1,234" → 1234 + AMBIGUOUS_NUMBER (alt 1.234) · "5 meterz" (kind length) → 5 m + TYPO_CORRECTED; with strictness confirm → ok:false + candidate 5 m · "72" (kind length, unit cm, accept.bareNumbers false) → UNIT_REQUIRED + candidate 72 cm · "72 in to cm" with accept.conversions false → CONVERSION_NOT_ALLOWED + candidate conversion · "5m" (kind duration) → 300 s + SLANG_UNIT · "in 2d" → date two days from now · "3min from tmrw" → tomorrow, same time-of-day +3 min · "17h30" → 17:30 · "quarter past 5" → 05:15 · "3pm EST" → 15:00 civil + zone {abbrev, -300, ambiguous} + TZ_IGNORED/AMBIGUOUS_TIMEZONE; `{applyZone:true}` → the 20:00Z instant · "2pm to 4pm" → date-range 14:00..16:00 · "9-5" → date-range 09:00..17:00 (workday shift) · "500 KB" → 500000 B · "5 Mb" → 625000 B (megabits) · "5 Mbps" → data_rate 5000000 bit/s; use "bit/s" for bits per second because bare "bps" stays basis points · "5 gpm" → flow_rate 0.000315451 m³/s · "250 mL/min" → flow_rate 0.000004167 m³/s · "10 inH₂O" → pressure 2490.8891 Pa · "1 kgf/cm²" → pressure 98.0665 kPa · "1 kg/cm²" → ok:false TRAILING_INPUT (kilogram-mass over area deferred; use kgf/cm²) · "5 psig" (kind pressure) → ok:false UNKNOWN_UNIT (gauge semantics deferred) · "9.8 m/s²" → acceleration 9.8 m/s² · "10 Nm" → torque 10 N⋅m (exact-case; lowercase "nm" remains nanometers) · "500 lux" → illuminance 500 lx · "100 nits" → luminance 100 cd/m² · "20 mSv" → radiation equivalent dose 0.02 Sv · "5 MBq" → radioactivity 5000000 Bq · "5 uM" → concentration 0.005 mol/m³ · "1 mol/L" and "1 mol per L" → concentration 1000 mol/m³; untyped glued "1M" fails, use "1 M" or `kind:'concentration'` · "-40°F" → 233.15 K · "3×10⁵ m" → 300000 m · "½ cup" → 118.29 mL · "15%" → percent 15 · "25 bps" → 0.25% (basis points; bare bps stays percent) · "500 mAh" → charge 1800 C · "4.7 kohm" → resistance 4700 Ω · "250 mmol" → substance 0.25 mol. ## Docs - [Full docs markdown](https://lingo.pascal.app/llms.md): Complete `/docs` page as clean Markdown, including installation, parse, forms, coverage, integrations, API, and agent recipes. - [Human docs](https://lingo.pascal.app/docs): Interactive docs with live parser demos, keyboard-accessible fields, and copyable code. - [Two-way slider demo](https://lingo.pascal.app/docs#forms-range-slider): Text parses to range thumbs; pointer and keyboard movement humanizes back to text. - [README](https://github.com/pascalorg/lingo/tree/main/packages/lingo#readme): Package overview and recipes.