| name | lingo |
| description | Parse natural-language quantities, units, dates, and ranges ("5'11\"", "1.5 cups", "72 in to cm", "three days ago", "between 5 and 10 kg", "it's hot") into canonical, validated values with issue codes and spans — and humanize them back. Use when a form input, LLM tool schema, MCP handler, or import pipeline takes free-text measurements/units/dates. Covers Standard Schema fields (quantityField/dateField/lingoObject) for the AI SDK and MCP, the headless <lingo-input>, unit conversion, and format/humanize round-trips. Zero-dependency TypeScript, deterministic. |
| metadata | {"author":"pascalorg","version":"1.0.0"} |
lingo — natural-language quantities, units, dates & ranges
@pascal-app/lingo is a zero-dependency
TypeScript library that turns the strings people type and models emit —
180cm, 5ft 11, 1.5 cups, 90 min, next friday, 1,5 kg, "5'11\"",
"twenty-five kg", "3pm EST" — into canonical, validated values: a quantity,
range, conversion, date, date-range, or duration in one canonical unit. It
converts, range-checks, and humanizes the value back. Every successful result
carries a [start, end) span into the original input plus any issues with stable
codes. It's two-way: default format()/humanize*() output re-parses to the
same value (display-only options like localizedUnits are outside the guarantee).
Tagline: Make forms easier, LLM tools safer.
Source of truth: for anything past the patterns below — the full API, every
unit and kind, all issue codes, and per-input examples — open
node_modules/@pascal-app/lingo/llms.txt or
lingo.pascal.app/llms.txt.
When to use this skill
Reach for lingo whenever a value arrives as free text and needs to become a
trustworthy stored value:
- Web forms — you're about to build a number box + unit dropdown. Use one
text field instead:
180cm, 2 lb 3 oz, an hour and a half, typos with
did-you-mean, fuzzy words (it's hot).
- LLM tool / MCP boundaries — a model returns
"5'11\"" or "1,234 kg" and
your handler needs a safe number. Models emit strings more reliably than
floats; lingo makes the string the safe path and rejects risky readings.
- Data pipelines / imports — normalizing messy measurement or date columns
into canonical units without
Number()/new Date() silently guessing wrong.
- Anything that converts (
72 in to cm), ranges (between 5 and 10 kg,
under 10 minutes, 10 ± 0.5 mm), or relative dates (three days ago).
Do not reach for it for already-canonical typed numbers, generic calendar
scheduling or timezone-database work, arbitrary NLP extraction, or currency
conversion needing live FX (lingo returns RATE_REQUIRED — you inject rates).
Install & entry points
npm i @pascal-app/lingo
Import only what you need; each subpath is tree-shakeable.
| Entry | Use for |
|---|
@pascal-app/lingo | core parse/convert: lingo, parseQuantity, parseRange, convert |
@pascal-app/lingo/date | dates & durations: parseDate, parseDuration, humanizeDate |
@pascal-app/lingo/ai | Standard Schema fields for LLM tools: quantityField, dateField, lingoObject |
@pascal-app/lingo/mcp | lingoTool() — MCP tool with validation before the handler |
@pascal-app/lingo/dom | headless lingoInput() controller for any <input> |
@pascal-app/lingo/react | useLingoInput() hook |
@pascal-app/lingo/react-native | DOM-free useLingoTextInput() hook for React Native TextInput |
@pascal-app/lingo/element | <lingo-input> form-associated custom element |
@pascal-app/lingo/complete | ranked autocomplete completions() |
@pascal-app/lingo/locales/{en,en-gb,es,fr,pt,zh,ja} | opt-in parsing language packs |
@pascal-app/lingo/catalog | query units/kinds/currencies |
@pascal-app/lingo/describe | rich human/agent-readable views of a value or result |
@pascal-app/lingo/schema | JSON Schema / OpenAPI generated from the v3 wire types |
Pattern 1 — parse, validate, convert (core)
import { lingo, parseQuantity, parseRange } from "@pascal-app/lingo"
const height = parseQuantity("5'11\"", { kind: "length" })
if (height.ok) height.quantity.to("m").value
lingo("72 in to cm")
parseRange("between 5 and 10 kg", { kind: "mass" })
lingo(text, opts?) returns a versioned union on .type
(quantity | range | conversion | number | failure), serialized as flat v3 JSON.
Branch on .ok first. Successes carry the value fields plus span and
confidence; a failure is { schemaVersion: 3, ok: false, type: "failure", text, issues, candidate? } — with no span/confidence. Surface .issues[] (each has
code, severity, message, and usually a span; field-level bound issues from
/ai may omit it) to the user.
Pass context to disambiguate: { kind, unit, currency, locale, system, strictness }.
strictness: "confirm" turns each assumption (typo fix, ambiguous number) into a
failure carrying a candidate, so you can render a one-click confirmation.
Pattern 2 — a natural-language form field
Headless controller — no styles shipped, canonicalizes on blur/Enter/submit,
never rewrites while the user is mid-type (2 f is incomplete, not invalid):
import { lingoInput } from "@pascal-app/lingo/dom"
const field = lingoInput(document.querySelector("#height"), {
kind: "length", unit: "m", name: "height_m",
})
field.set("6ft")
field.commit()
React: useLingoInput(opts) from @pascal-app/lingo/react. Framework-agnostic
element: defineLingoInput() then <lingo-input kind="length" unit="m" name="height_m">.
React Native: spread useLingoTextInput(opts).inputProps onto TextInput.
Pattern 3 — safe LLM tool / MCP boundary
The fields expose a string input JSON Schema (models are better at emitting
"5'11\"" than 1.8034) and hand your handler the canonical value. Risky
readings fail loudly: parser failures are [CODE]-prefixed and carry a
candidate when one exists (so a model can self-correct in one round trip);
schema-shape errors surface as plain Standard Schema messages.
import { tool, generateText } from "ai"
import { lingoObject, quantityField, dateField } from "@pascal-app/lingo/ai"
const now = new Date("2026-07-08T12:00:00Z")
const shipment = lingoObject({
weight: quantityField({ kind: "mass", unit: "kg", min: 0, max: 500 }),
deliverBy: dateField({ now }),
})
await generateText({
model,
tools: { create_shipment: tool({ inputSchema: shipment, execute: run }) },
})
MCP: wrap the same fields with lingoTool({ name, description, input, handler })
from @pascal-app/lingo/mcp; validation runs before handler, and failures
return { isError: true } with [CODE]-prefixed text.
Rules that keep you out of trouble
- Keep measurements as strings in tool/form schemas. Let lingo convert,
validate, surface spans, and handle ambiguity — don't ask the model or user
for a float.
- Store the canonical value, display the humanized one. Default
format() /
humanizeDate() output round-trips back to the same value (1.9999 m → 6′7″,
never 5′12″); display-only options like localizedUnits don't round-trip.
- Always pass an explicit
now to parseDate/dateField for relative
dates — never rely on Date.now(), so a queued or retried call can't drift
across midnight. Reference-dependent input without now → NOW_REQUIRED.
Timezones are detected but kept as civil time unless you pass applyZone: true;
/ai dateField rejects an ignored zone (TZ_IGNORED) by default.
- No silent guesses. Ambiguous input returns a deterministic best reading
plus ranked
alternatives/candidate and a warning code — show it, or use
strictness: "confirm" / "strict" to force confirmation.
- Currency conversion needs injected rates —
5 EUR to USD returns
RATE_REQUIRED; call convertCurrency with your own rates.
- Locale packs are opt-in. English is built in; load others with
createLingo({ locales: [es, fr, …] }). Omitting locale auto-detects among
loaded packs plus English; requesting an explicit unloaded locale →
LOCALE_NOT_LOADED.
Common issue codes to handle: UNKNOWN_UNIT, KIND_MISMATCH, UNIT_REQUIRED,
AMBIGUOUS_NUMBER, AMBIGUOUS_UNIT, TYPO_CORRECTED, RANGE_MIN/RANGE_MAX,
NOW_REQUIRED, TZ_IGNORED, RATE_REQUIRED. Override any message via the
messages option.
Full reference
This skill is the on-ramp; the exhaustive API, the complete kind/unit list, all
issue codes, and per-input examples live in the agent docs: