{"_id":"@pascal-app/lingo","_rev":"6-865bd506d67d86c6ba278c182fc95fcd","name":"@pascal-app/lingo","dist-tags":{"latest":"0.5.0"},"versions":{"0.1.0":{"name":"@pascal-app/lingo","version":"0.1.0","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","_id":"@pascal-app/lingo@0.1.0","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"dist":{"shasum":"c710aa29d3cc2b5b0fef4ac8eb644261c5969f8e","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.1.0.tgz","fileCount":76,"integrity":"sha512-j4cN9DSc3QD4LGG003TCIO5+jMYySFpJO6467V0Q3IPumVkwq7/BpEAw8JGY6zTZFmFETIeupuPEi4drnA9m+g==","signatures":[{"sig":"MEUCIEfypJBXmNTlV/GgQyj2yirh+AIw5LwXdDaqKEf9E1ILAiEAtMlGOOs71p/8K7kBqWoMNoCza7Gg3DA1JzlM9cJaWSw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@pascal-app%2flingo@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3197647},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./package.json":"./package.json"},"gitHead":"5b2b61e722faefea9714a9c496056a1354b9da01","scripts":{"dev":"tsup --watch","demo":"node scripts/demo.mjs","size":"node scripts/size.mjs","test":"vitest run","bench":"node scripts/bench.mjs","build":"tsup","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","ai-eval":"node scripts/ai-eval.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","prepublishOnly":"bun run check"},"_npmUser":{"name":"aymericr","email":"aymeric@pascal.app"},"repository":{"url":"git+https://github.com/pascalorg/lingo.git","type":"git","directory":"packages/lingo"},"_npmVersion":"10.9.8","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^29.1.1","vitest":"^4.1.9","esbuild":"^0.28.1","react-dom":"^19","typescript":"^6.0.3","@types/react":"^19.2.17","@types/react-dom":"^19","@standard-schema/spec":"^1.1.0"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lingo_0.1.0_1783495025012_0.3099156527598528","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@pascal-app/lingo","version":"0.2.0","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","_id":"@pascal-app/lingo@0.2.0","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"dist":{"shasum":"e73334414f5586b9600b4a4061b7081e53f57dd2","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.2.0.tgz","fileCount":115,"integrity":"sha512-Ws+Utm23SmDbMB0Jc7joIp4KPkXhs4Z1boUcW9dl8nC3ATJz+a50F7/MyVTbE3QaL5Fr+YwNfsiOMxrKdhwPhA==","signatures":[{"sig":"MEUCIQCj3ErHefYUTs5yQ9ua+5i+ZaumF6fFOCyj+RHNe3HQcAIgUgDx1EGzDoJF4UM7Fn0bUq8CJi6i3dVsdwyRfOdUuK4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@pascal-app%2flingo@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3709838},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./complete":{"import":{"types":"./dist/complete/index.d.ts","default":"./dist/complete/index.js"},"require":{"types":"./dist/complete/index.d.cts","default":"./dist/complete/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./locales/en":{"import":{"types":"./dist/locales/en.d.ts","default":"./dist/locales/en.js"},"require":{"types":"./dist/locales/en.d.cts","default":"./dist/locales/en.cjs"}},"./locales/es":{"import":{"types":"./dist/locales/es.d.ts","default":"./dist/locales/es.js"},"require":{"types":"./dist/locales/es.d.cts","default":"./dist/locales/es.cjs"}},"./locales/fr":{"import":{"types":"./dist/locales/fr.d.ts","default":"./dist/locales/fr.js"},"require":{"types":"./dist/locales/fr.d.cts","default":"./dist/locales/fr.cjs"}},"./locales/ja":{"import":{"types":"./dist/locales/ja.d.ts","default":"./dist/locales/ja.js"},"require":{"types":"./dist/locales/ja.d.cts","default":"./dist/locales/ja.cjs"}},"./locales/pt":{"import":{"types":"./dist/locales/pt.d.ts","default":"./dist/locales/pt.js"},"require":{"types":"./dist/locales/pt.d.cts","default":"./dist/locales/pt.cjs"}},"./locales/zh":{"import":{"types":"./dist/locales/zh.d.ts","default":"./dist/locales/zh.js"},"require":{"types":"./dist/locales/zh.d.cts","default":"./dist/locales/zh.cjs"}},"./package.json":"./package.json","./locales/en-gb":{"import":{"types":"./dist/locales/en-gb.d.ts","default":"./dist/locales/en-gb.js"},"require":{"types":"./dist/locales/en-gb.d.cts","default":"./dist/locales/en-gb.cjs"}}},"gitHead":"65cb2995a7dc0f3b00125a1613be8f5f60ba4c0b","scripts":{"dev":"tsup --watch","demo":"node scripts/demo.mjs","size":"node scripts/size.mjs","test":"vitest run","bench":"node scripts/bench.mjs","build":"tsup","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && bun scripts/check-llms-index.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","ai-eval":"node scripts/ai-eval.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","prepublishOnly":"bun run check"},"_npmUser":{"name":"aymericr","email":"aymeric@pascal.app"},"repository":{"url":"git+https://github.com/pascalorg/lingo.git","type":"git","directory":"packages/lingo"},"_npmVersion":"10.9.8","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^29.1.1","vitest":"^4.1.9","esbuild":"^0.28.1","react-dom":"^19","typescript":"^6.0.3","@types/react":"^19.2.17","@types/react-dom":"^19","@standard-schema/spec":"^1.1.0"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lingo_0.2.0_1783521362617_0.8602795501485812","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@pascal-app/lingo","version":"0.2.1","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","_id":"@pascal-app/lingo@0.2.1","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"dist":{"shasum":"7a446b0de595b7456ebd238c7cd93d6b70fff61e","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.2.1.tgz","fileCount":115,"integrity":"sha512-Y6Nmb4vLSMp/dIdKABc7GjdTdB6yvUyBg95ySlThMz6zKrJjpVG0utexY4cnmWVPkGNSKOFotiHw8UJiQMUVnw==","signatures":[{"sig":"MEUCIQC/ZB09XOGhhbPvD+aY2o4lceU5Yyr9pOL8mHwTf/mzmAIgS4SnTIGSuGDddnzrjcyYYhSp6+pP9LZQQviDRBP7g+w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@pascal-app%2flingo@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3853150},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./complete":{"import":{"types":"./dist/complete/index.d.ts","default":"./dist/complete/index.js"},"require":{"types":"./dist/complete/index.d.cts","default":"./dist/complete/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./locales/en":{"import":{"types":"./dist/locales/en.d.ts","default":"./dist/locales/en.js"},"require":{"types":"./dist/locales/en.d.cts","default":"./dist/locales/en.cjs"}},"./locales/es":{"import":{"types":"./dist/locales/es.d.ts","default":"./dist/locales/es.js"},"require":{"types":"./dist/locales/es.d.cts","default":"./dist/locales/es.cjs"}},"./locales/fr":{"import":{"types":"./dist/locales/fr.d.ts","default":"./dist/locales/fr.js"},"require":{"types":"./dist/locales/fr.d.cts","default":"./dist/locales/fr.cjs"}},"./locales/ja":{"import":{"types":"./dist/locales/ja.d.ts","default":"./dist/locales/ja.js"},"require":{"types":"./dist/locales/ja.d.cts","default":"./dist/locales/ja.cjs"}},"./locales/pt":{"import":{"types":"./dist/locales/pt.d.ts","default":"./dist/locales/pt.js"},"require":{"types":"./dist/locales/pt.d.cts","default":"./dist/locales/pt.cjs"}},"./locales/zh":{"import":{"types":"./dist/locales/zh.d.ts","default":"./dist/locales/zh.js"},"require":{"types":"./dist/locales/zh.d.cts","default":"./dist/locales/zh.cjs"}},"./package.json":"./package.json","./locales/en-gb":{"import":{"types":"./dist/locales/en-gb.d.ts","default":"./dist/locales/en-gb.js"},"require":{"types":"./dist/locales/en-gb.d.cts","default":"./dist/locales/en-gb.cjs"}}},"gitHead":"735fd854d469742614872080a1c35e8ba2131177","scripts":{"dev":"tsup --watch","demo":"node scripts/demo.mjs","size":"node scripts/size.mjs","test":"vitest run","bench":"node scripts/bench.mjs","build":"tsup","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && bun scripts/check-llms-index.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","ai-eval":"node scripts/ai-eval.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","prepublishOnly":"bun run check"},"_npmUser":{"name":"aymericr","email":"aymeric@pascal.app"},"repository":{"url":"git+https://github.com/pascalorg/lingo.git","type":"git","directory":"packages/lingo"},"_npmVersion":"10.9.8","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^29.1.1","vitest":"^4.1.9","esbuild":"^0.28.1","react-dom":"^19","typescript":"^6.0.3","@types/react":"^19.2.17","@types/react-dom":"^19","@standard-schema/spec":"^1.1.0"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lingo_0.2.1_1783638981433_0.45549149689009516","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@pascal-app/lingo","version":"0.3.0","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","_id":"@pascal-app/lingo@0.3.0","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"dist":{"shasum":"18b185f7153f612fdd685fc231d6a96df8fd079a","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.3.0.tgz","fileCount":119,"integrity":"sha512-AG/EyKsiMffCkCc2mOuuTLvTmHHo9CYiLQ17PCmlOkWEx9mUnjouFkcJ6AdRE1W5daT3C1JhS0TJnC/mYSA8+w==","signatures":[{"sig":"MEYCIQCDhK8zghQmlrj0zI6TX/6fxjvM5kw/OAl6kg6242UMHgIhAK40O5zae3igaeqfYzBg637x3s7lysOK9NgcArZWP1oc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4876907},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./complete":{"import":{"types":"./dist/complete/index.d.ts","default":"./dist/complete/index.js"},"require":{"types":"./dist/complete/index.d.cts","default":"./dist/complete/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./locales/en":{"import":{"types":"./dist/locales/en.d.ts","default":"./dist/locales/en.js"},"require":{"types":"./dist/locales/en.d.cts","default":"./dist/locales/en.cjs"}},"./locales/es":{"import":{"types":"./dist/locales/es.d.ts","default":"./dist/locales/es.js"},"require":{"types":"./dist/locales/es.d.cts","default":"./dist/locales/es.cjs"}},"./locales/fr":{"import":{"types":"./dist/locales/fr.d.ts","default":"./dist/locales/fr.js"},"require":{"types":"./dist/locales/fr.d.cts","default":"./dist/locales/fr.cjs"}},"./locales/ja":{"import":{"types":"./dist/locales/ja.d.ts","default":"./dist/locales/ja.js"},"require":{"types":"./dist/locales/ja.d.cts","default":"./dist/locales/ja.cjs"}},"./locales/pt":{"import":{"types":"./dist/locales/pt.d.ts","default":"./dist/locales/pt.js"},"require":{"types":"./dist/locales/pt.d.cts","default":"./dist/locales/pt.cjs"}},"./locales/zh":{"import":{"types":"./dist/locales/zh.d.ts","default":"./dist/locales/zh.js"},"require":{"types":"./dist/locales/zh.d.cts","default":"./dist/locales/zh.cjs"}},"./package.json":"./package.json","./react-native":{"import":{"types":"./dist/react-native/index.d.ts","default":"./dist/react-native/index.js"},"require":{"types":"./dist/react-native/index.d.cts","default":"./dist/react-native/index.cjs"}},"./locales/en-gb":{"import":{"types":"./dist/locales/en-gb.d.ts","default":"./dist/locales/en-gb.js"},"require":{"types":"./dist/locales/en-gb.d.cts","default":"./dist/locales/en-gb.cjs"}}},"gitHead":"b1cca21de5f543476b240b905c346d6d6219f390","scripts":{"dev":"tsup --watch","demo":"node scripts/demo.mjs","size":"node scripts/size.mjs","test":"vitest run","bench":"node scripts/bench.mjs","build":"tsup","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && bun scripts/check-llms-index.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","ai-eval":"node scripts/ai-eval.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","prepublishOnly":"bun run check"},"_npmUser":{"name":"aymericr","email":"aymeric@pascal.app"},"repository":{"url":"git+https://github.com/pascalorg/lingo.git","type":"git","directory":"packages/lingo"},"_npmVersion":"10.9.7","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^29.1.1","vitest":"^4.1.9","esbuild":"^0.28.1","react-dom":"^19","typescript":"^6.0.3","@types/react":"^19.2.17","@types/react-dom":"^19","@standard-schema/spec":"^1.1.0"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lingo_0.3.0_1784623471785_0.6580191641965929","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@pascal-app/lingo","version":"0.4.0","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","i18n","locale","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","_id":"@pascal-app/lingo@0.4.0","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"dist":{"shasum":"08c2d2959f0cb9f743c1824e0ae28b8e96759724","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.4.0.tgz","fileCount":117,"integrity":"sha512-xY939Jya70cKGhyDzbhx209YEo/ICw52S6ETomsljfrBLeIILJor7ceISadmhqjLpIDw/5WI+jlz7Vp4ZJAJbw==","signatures":[{"sig":"MEUCIQDcQ2Bch8cukR2PDetp1KfkAOlQR0ccQ9nWrwxuo6lh5QIgK4oqllxVIH7iL02FMSY8S/8WgwOC3EMguFgB2EHY/3w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@pascal-app%2flingo@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4555575},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./complete":{"import":{"types":"./dist/complete/index.d.ts","default":"./dist/complete/index.js"},"require":{"types":"./dist/complete/index.d.cts","default":"./dist/complete/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./locales/en":{"import":{"types":"./dist/locales/en.d.ts","default":"./dist/locales/en.js"},"require":{"types":"./dist/locales/en.d.cts","default":"./dist/locales/en.cjs"}},"./locales/es":{"import":{"types":"./dist/locales/es.d.ts","default":"./dist/locales/es.js"},"require":{"types":"./dist/locales/es.d.cts","default":"./dist/locales/es.cjs"}},"./locales/fr":{"import":{"types":"./dist/locales/fr.d.ts","default":"./dist/locales/fr.js"},"require":{"types":"./dist/locales/fr.d.cts","default":"./dist/locales/fr.cjs"}},"./locales/ja":{"import":{"types":"./dist/locales/ja.d.ts","default":"./dist/locales/ja.js"},"require":{"types":"./dist/locales/ja.d.cts","default":"./dist/locales/ja.cjs"}},"./locales/pt":{"import":{"types":"./dist/locales/pt.d.ts","default":"./dist/locales/pt.js"},"require":{"types":"./dist/locales/pt.d.cts","default":"./dist/locales/pt.cjs"}},"./locales/zh":{"import":{"types":"./dist/locales/zh.d.ts","default":"./dist/locales/zh.js"},"require":{"types":"./dist/locales/zh.d.cts","default":"./dist/locales/zh.cjs"}},"./package.json":"./package.json","./react-native":{"import":{"types":"./dist/react-native/index.d.ts","default":"./dist/react-native/index.js"},"require":{"types":"./dist/react-native/index.d.cts","default":"./dist/react-native/index.cjs"}},"./locales/en-gb":{"import":{"types":"./dist/locales/en-gb.d.ts","default":"./dist/locales/en-gb.js"},"require":{"types":"./dist/locales/en-gb.d.cts","default":"./dist/locales/en-gb.cjs"}}},"gitHead":"888c40dc84f142fa9f032feef50a38a2bb6d09db","scripts":{"dev":"tsup --watch","demo":"node scripts/demo.mjs","size":"node scripts/size.mjs","test":"vitest run","bench":"node scripts/bench.mjs","build":"tsup","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && bun scripts/check-llms-index.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","ai-eval":"node scripts/ai-eval.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","prepublishOnly":"bun run check"},"_npmUser":{"name":"aymericr","email":"aymeric@pascal.app"},"repository":{"url":"git+https://github.com/pascalorg/lingo.git","type":"git","directory":"packages/lingo"},"_npmVersion":"10.9.8","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^29.1.1","vitest":"^4.1.9","esbuild":"^0.28.1","react-dom":"^19","typescript":"^6.0.3","@types/react":"^19.2.17","@types/react-dom":"^19","@standard-schema/spec":"^1.1.0"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lingo_0.4.0_1785639028299_0.9715533135289689","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@pascal-app/lingo","version":"0.5.0","description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","i18n","locale","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"license":"MIT","author":{"name":"Aymeric","email":"aymeric@pascal.app"},"type":"module","sideEffects":false,"engines":{"node":">=18"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./date":{"import":{"types":"./dist/date/index.d.ts","default":"./dist/date/index.js"},"require":{"types":"./dist/date/index.d.cts","default":"./dist/date/index.cjs"}},"./calc":{"import":{"types":"./dist/calc/index.d.ts","default":"./dist/calc/index.js"},"require":{"types":"./dist/calc/index.d.cts","default":"./dist/calc/index.cjs"}},"./dom":{"import":{"types":"./dist/dom/index.d.ts","default":"./dist/dom/index.js"},"require":{"types":"./dist/dom/index.d.cts","default":"./dist/dom/index.cjs"}},"./element":{"import":{"types":"./dist/element/index.d.ts","default":"./dist/element/index.js"},"require":{"types":"./dist/element/index.d.cts","default":"./dist/element/index.cjs"}},"./describe":{"import":{"types":"./dist/describe/index.d.ts","default":"./dist/describe/index.js"},"require":{"types":"./dist/describe/index.d.cts","default":"./dist/describe/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./schema":{"import":{"types":"./dist/schema/index.d.ts","default":"./dist/schema/index.js"},"require":{"types":"./dist/schema/index.d.cts","default":"./dist/schema/index.cjs"}},"./ai":{"import":{"types":"./dist/ai/index.d.ts","default":"./dist/ai/index.js"},"require":{"types":"./dist/ai/index.d.cts","default":"./dist/ai/index.cjs"}},"./mcp":{"import":{"types":"./dist/mcp/index.d.ts","default":"./dist/mcp/index.js"},"require":{"types":"./dist/mcp/index.d.cts","default":"./dist/mcp/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./react-native":{"import":{"types":"./dist/react-native/index.d.ts","default":"./dist/react-native/index.js"},"require":{"types":"./dist/react-native/index.d.cts","default":"./dist/react-native/index.cjs"}},"./complete":{"import":{"types":"./dist/complete/index.d.ts","default":"./dist/complete/index.js"},"require":{"types":"./dist/complete/index.d.cts","default":"./dist/complete/index.cjs"}},"./locales/en":{"import":{"types":"./dist/locales/en.d.ts","default":"./dist/locales/en.js"},"require":{"types":"./dist/locales/en.d.cts","default":"./dist/locales/en.cjs"}},"./locales/en-gb":{"import":{"types":"./dist/locales/en-gb.d.ts","default":"./dist/locales/en-gb.js"},"require":{"types":"./dist/locales/en-gb.d.cts","default":"./dist/locales/en-gb.cjs"}},"./locales/es":{"import":{"types":"./dist/locales/es.d.ts","default":"./dist/locales/es.js"},"require":{"types":"./dist/locales/es.d.cts","default":"./dist/locales/es.cjs"}},"./locales/fr":{"import":{"types":"./dist/locales/fr.d.ts","default":"./dist/locales/fr.js"},"require":{"types":"./dist/locales/fr.d.cts","default":"./dist/locales/fr.cjs"}},"./locales/pt":{"import":{"types":"./dist/locales/pt.d.ts","default":"./dist/locales/pt.js"},"require":{"types":"./dist/locales/pt.d.cts","default":"./dist/locales/pt.cjs"}},"./locales/zh":{"import":{"types":"./dist/locales/zh.d.ts","default":"./dist/locales/zh.js"},"require":{"types":"./dist/locales/zh.d.cts","default":"./dist/locales/zh.cjs"}},"./locales/ja":{"import":{"types":"./dist/locales/ja.d.ts","default":"./dist/locales/ja.js"},"require":{"types":"./dist/locales/ja.d.cts","default":"./dist/locales/ja.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","size":"node scripts/size.mjs","bench":"node scripts/bench.mjs","ai-eval":"node scripts/ai-eval.mjs","bench:backend":"node scripts/bench.mjs --backend","bench:browser":"node scripts/bench.mjs --browser --open","demo":"node scripts/demo.mjs","check":"bun run typecheck && bun run test && bun run build && bun run size && node scripts/corpus-diff.mjs && node scripts/check-docs-sync.mjs && bun scripts/check-llms-index.mjs && node scripts/check-zero-deps.mjs && node scripts/gen-schemas.mjs --check","prepublishOnly":"bun run check"},"devDependencies":{"@standard-schema/spec":"^1.1.0","@types/react":"^19.2.17","@types/react-dom":"^19","esbuild":"^0.28.1","jsdom":"^29.1.1","react-dom":"^19","tsup":"^8.5.0","typescript":"^6.0.3","vitest":"^4.1.9"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"repository":{"type":"git","url":"git+https://github.com/pascalorg/lingo.git","directory":"packages/lingo"},"homepage":"https://lingo.pascal.app","bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"publishConfig":{"access":"public","provenance":true},"gitHead":"d8692a903bb8b67e623d002a34c56e6164a12a3e","_id":"@pascal-app/lingo@0.5.0","_nodeVersion":"22.23.2","_npmVersion":"11.12.1","dist":{"integrity":"sha512-SrmSO4MApMUIxzK4MlMU74VcnB61h2XjoAs50dmdHgJ2R5LDw4H9n7vMp0rfqXCmK7RnfOS7o2ZzQfaXzd9YUA==","shasum":"098b72303a126baf7c06bfb700ff36540a9da2cc","tarball":"https://registry.npmjs.org/@pascal-app/lingo/-/lingo-0.5.0.tgz","fileCount":124,"unpackedSize":4868887,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@pascal-app%2flingo@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICqBGNnUfzb33VEpR6AUnR+H+w1NF7kkzErC9feSrRtEAiAVyADQCQo/3WR+biXDobCRUjmrd6XV9vRyYJTZwq7mRA=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:331e54c4-727a-411f-8d24-a357be3f2e7a"}},"directories":{},"maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lingo_0.5.0_1787495041806_0.6439450370673665"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-08T07:17:04.846Z","modified":"2026-08-23T14:24:02.388Z","0.1.0":"2026-07-08T07:17:05.148Z","0.2.0":"2026-07-08T14:36:02.890Z","0.2.1":"2026-07-09T23:16:21.664Z","0.3.0":"2026-07-21T08:44:31.999Z","0.4.0":"2026-08-02T02:50:28.467Z","0.5.0":"2026-08-23T14:24:02.049Z"},"bugs":{"url":"https://github.com/pascalorg/lingo/issues"},"author":{"name":"Aymeric","email":"aymeric@pascal.app"},"license":"MIT","homepage":"https://lingo.pascal.app","keywords":["parse","natural-language","units","measurement","convert","unit-conversion","input","mask","form","date","duration","i18n","locale","humanize","quantity","typescript","zero-dependencies","llm","ai","agents","structured-output","standard-schema","json-schema","tool-calling","mcp","validation"],"repository":{"type":"git","url":"git+https://github.com/pascalorg/lingo.git","directory":"packages/lingo"},"description":"Make forms easier, LLM tools safer. Parse what humans type and what models emit — natural-language quantities, units, dates and ranges — into canonical, validated values. Two-way, zero dependencies.","maintainers":[{"name":"aymericr","email":"aymeric@pascal.app"},{"name":"wass08","email":"wass08@gmail.com"}],"readme":"# lingo\n\n**Make forms easier, LLM tools safer.**\n\nDocs with live demos: [lingo.pascal.app](https://lingo.pascal.app) · every\nsection as agent-readable markdown:\n[lingo.pascal.app/llms.txt](https://lingo.pascal.app/llms.txt)\n\nlingo parses what humans type and what models emit: natural-language quantities,\nunits, dates, and ranges. It turns them into canonical, validated values and\nhumanizes them back. Zero runtime dependencies. The size gate lives in\n`scripts/size.mjs`, and the package includes thirty-three built-in kinds\n(chrono-node needs 35 kB for dates alone). English is the default; opt-in locale\npacks (`createLingo({ locales })`, `@pascal-app/lingo/locales/*`) add number words,\ngrammar, units, and relative-date idioms for Spanish, French, Portuguese,\nChinese, Japanese, and en-GB. Wave-1 packs cover Romance number composition,\nCJK number tokens, localized date grammar, and per-locale corpus gates. Number\n*formatting* is locale-aware via `Intl` in every locale; `humanizeDate()` output\nstays English for now.\n\n```ts\nimport { lingo, parseQuantity, convert, tryConvert } from '@pascal-app/lingo'\n\nparseQuantity(`5'11\"`).quantity.to('m').value   // 1.8034\nparseQuantity('72 in to cm').quantity.value     // 182.88  (honors the request)\nconvert(72, 'in', 'ft')                          // 6\nconst t = tryConvert(72, 'in', 'cm')\nt.ok && t.value                                   // 182.88\n\nlingo('between 5 and 10 kg')     // { type: 'range',      range:  5 to 10 kg }\nlingo('10 ± 0.5 mm')             // { type: 'range',      plusMinus ... }\nlingo('a few minutes')           // { type: 'range',      2 to 4 min, approximate }\nlingo(\"it's hot\", { kind: 'temperature' })\n                                 // { type: 'range',      27 to 35 °C, fuzzy: 'hot' }\nlingo('an hour and a half')      // { type: 'quantity',   5400 s }\nlingo('1,5 kg')                  // 1.5 kg. Separator policy, not locale sniffing.\nlingo('5 meterz', { kind: 'length' })\n                                 // 5 m + warning TYPO_CORRECTED (\"meterz\" → m)\n```\n\nPeople type it quick and abbreviated: `180cm`, `5ft 11`, `1m80`, `72 in`,\n`1.5 cups`, `500 KB`, `5 Mbps`, `5 gpm`, `500 mAh`, `$1,234.50`, `50p`,\n`about 20C`, `under 10 minutes`. Models and pasted text bring the fluent,\ntypographic forms: `5'11\"`, `1½ cups`, `twenty-five kg`, `3×10⁵ m`,\n`10 inH₂O`, `1 kgf/cm²`, `5 uM`, `9.8 m/s²`, `10 Nm`, `500 lux`, `20 mSv`,\n`fifty cents`. Your database wants one number in one unit. The library is the\nlayer in between: **forgiving in, canonical through, human out.**\n\n## Why lingo\n\n- **Actually parses language.** Number words (\"an hour and a half\"), unicode\n  (`½`, `μm`, `℃`, `′ ″`, curly quotes), typos with did-you-mean, compounds\n  (`5'11\"`, `2 lb 3 oz`, `1h30`), ranges, tolerances, qualifiers (\"about\",\n  \"at least\"), conversion requests (\"72 in to cm\"), and fuzzy words (\"hot\") when\n  you opt a field into them.\n- **Canonical underneath.** Every value normalizes to an SI-anchored base (meters,\n  kilograms, kelvin, seconds) or a self-canonical currency unit, with exact legal\n  conversion factors, never rounded anchors. Temperature *deltas* convert\n  correctly (a 5 °C rise is a 9 °F rise).\n- **Two-way.** Everything default `format()` emits, `parse()` reads back. Tests\n  enforce the invariant. `localizedUnits: true` is the explicit display-only\n  escape hatch for Intl unit words the English parser cannot read yet.\n  `1.9999 m` formats as `6′7″`, never `5′12″`.\n- **Honest about ambiguity.** `1,234` (dot-decimal? comma-decimal?), `5m` (meters?\n  minutes?), `ton` (which ton?) return a deterministic best reading, ranked\n  discriminated alternatives, and structured warnings. The parser does not guess silently.\n- **Errors are UX.** Every issue has a stable code, a human message you can\n  override, an input span for highlighting, and did-you-mean suggestions.\n- **Tiny and dependency-free.** No runtime deps. Tree-shakeable. `Intl` handles\n  locale number formatting, so the main entry ships no locale data — parser locale\n  packs are opt-in `@pascal-app/lingo/locales/*` subpaths you load explicitly.\n\n## Performance\n\nlingo is designed to be cheap enough to run while someone types, and fast enough\nto validate large imports without a worker queue.\n\nThe checked-in baseline uses a deterministic generated-English corpus built from\nthe library's own aliases, number forms, qualifiers, ranges, conversions, typos,\ndates, durations, and sentence templates. On the local Node v24.5.0 backend\nbaseline in this repo:\n\n- A normal single-value field parsed **492,739 values/second** across\n  **900 generated cases**, about **2.03 µs per input**.\n- Strict bulk validation without did-you-mean suggestions ran at\n  **550,270 checks/second** across **240 generated unknown-unit cases**,\n  about **1.82 µs per row**.\n- Scanning generated sentences for values ran at **53,533 scans/second** across\n  **260 cases**.\n- A no-match stress test over 50,000 characters finished in **0.341 ms**.\n\nThose numbers are a machine-local snapshot, not a universal promise. The point\nis the order of magnitude: ordinary parsing is microsecond-scale, so debounce is\na UX choice, not something the parser needs to survive. Reproduce with\n`bun run bench:backend`; the checked-in baseline lives in\n`bench/baseline-node.json`.\n\n## Install\n\n```sh\nnpm install @pascal-app/lingo\n```\n\nCoding agents: the package ships an inline reference at\n`node_modules/@pascal-app/lingo/llms.txt` (also at\n[lingo.pascal.app/llms-small.txt](https://lingo.pascal.app/llms-small.txt)).\nOnline, start at [lingo.pascal.app/llms.txt](https://lingo.pascal.app/llms.txt).\n\n## The 5-minute tour\n\n### Parse\n\n```ts\nimport { lingo, parseQuantity, parseRange } from '@pascal-app/lingo'\n\nconst r = parseQuantity('2 ft', { kind: 'length' })\nif (r.ok) {\n  r.quantity.value        // 2 (in ft)\n  r.quantity.base         // 0.6096 (meters, canonical)\n  r.quantity.to('cm').value  // 60.96\n  r.confidence            // 1\n  r.issues                // [] on success; warnings/infos still appear here\n}\n\n// lingo() returns a versioned discriminated union. Switch on .type.\nconst g = lingo('5-10 kg in lb')\n// g.type === 'conversion'; g.converted is a QuantityRange in pounds\n```\n\nSerialized parse results are **flat v3**: `{ schemaVersion: 3, ok, type, …value\nfields…, text, span, issues, confidence }`. A quantity is\n`{ …, type: 'quantity', kind, value, unit, base, baseUnit, … }` right at the top\n(no nesting); a conversion has `source` + `converted` (no redundant\n`targetUnit`); failures use `type: 'failure'` and may include a full `candidate`.\nEvery result and issue carries a `span: { start, end, text }` — half-open UTF-16\noffsets into the original input plus the matched substring, so `span` reads for\nitself (`input.slice(start, end) === span.text`).\n`ok: false` means at least one `error`-severity issue; warnings (`AMBIGUOUS_NUMBER`,\n`TYPO_CORRECTED`, `RANGE_REVERSED`, and others) accompany successful parses so you can surface\nthem or ignore them. A machine-readable JSON Schema, OpenAPI, and ready-made\nZod/Valibot/TypeBox/ArkType/Effect schemas live in `@pascal-app/lingo/schema`.\n\nNeed a richer view for docs, logs, or tool output? Keep `toJSON()` for compact\nwire storage and import `describeResource()` for direct `lingo.quantity` /\n`lingo.range` resources, or `describeResult()` for whole parse-result resources\nwith issues and source spans:\n\n```ts\nimport { lingo, parseQuantity } from '@pascal-app/lingo'\nimport { parseDate, parseDuration } from '@pascal-app/lingo/date'\nimport { describeResource, describeResult } from '@pascal-app/lingo/describe'\n\nconst parsed = parseQuantity('72 in')\nparsed.ok && describeResource(parsed.quantity)\n// {\n//   object: 'lingo.quantity',\n//   kind: 'length',\n//   value: { amount: 72, unit: { id: 'in', symbol: 'in', name: 'inch', ... } },\n//   canonical: { amount: 1.8288, unit: { id: 'm', symbol: 'm', name: 'meter', ... } },\n//   formatted: '72 in'\n// }\n\nconst result = describeResult(lingo('72 in to cm'))\nresult.data\n// {\n//   object: 'lingo.conversion',\n//   source: { object: 'lingo.quantity', value: { amount: 72, unit: { id: 'in', ... } }, ... },\n//   target: { unit: { id: 'cm', symbol: 'cm', name: 'centimeter' } },\n//   converted: { object: 'lingo.quantity', value: { amount: 182.88, unit: { id: 'cm', ... } }, ... },\n// }\n\ndescribeResult(parseDate('5/3/2026', { now })).data\n// { object: 'lingo.date', value: { iso: '...', epochMilliseconds: ... }, calendar: { year: 2026, month: 5, day: 3 }, grain: 'day', ... }\n\ndescribeResult(parseDuration('1h30')).data\n// { object: 'lingo.duration', value: { amount: 1.5, unit: { id: 'h', ... } }, canonical: { amount: 5400, unit: { id: 's', ... } }, parts: [...] }\n```\n\n### Strictness & error escalation\n\nUse one dial for field personality, then override the pieces that matter:\n\n| mode | behavior |\n|------|----------|\n| `forgiving` | Default. Typos, ambiguities, slang and assumptions can succeed with warnings/infos. |\n| `confirm` | Assumption-class issues escalate to `error`; the would-have-been result is attached as `candidate`. |\n| `strict` | `confirm` plus typo fixing off, number words/fuzzy/approximations/bare-number assumptions rejected. |\n\n```ts\nconst r = lingo('5 meterz', { kind: 'length', strictness: 'confirm' })\n\nif (!r.ok && r.candidate?.type === 'quantity') {\n  r.issues[0].code              // \"TYPO_CORRECTED\"\n  r.candidate.quantity.format() // \"5 m\"\n}\n```\n\nCandidate UX is built into the DOM layer:\n\n```ts\nlingoInput(input, {\n  kind: 'length',\n  strictness: 'confirm',\n  hintElement: '#hint',\n  formatCandidate: (candidate) =>\n    candidate.type === 'quantity'\n      ? `Use ${candidate.quantity.format()}`\n      : 'Use this value',\n})\n```\n\nAcceptance switches reject whole shapes while keeping a candidate:\n\n```ts\nlingo('5-10 kg', { accept: { ranges: false } })       // SINGLE_VALUE_EXPECTED + candidate range\nlingo('72 in to cm', { accept: { conversions: false } }) // CONVERSION_NOT_ALLOWED + candidate\nlingo('about 5 kg', { accept: { approximations: false } }) // APPROX_NOT_ALLOWED + candidate\n```\n\nTolerance switches control how hard the parser tries:\n\n```ts\nlingo('5 meterz', { kind: 'length', tolerance: { typos: 'suggest' } })\n// ok:false, UNKNOWN_UNIT suggestions [\"m\"], candidate parses as 5 m\n```\n\nEscalate surgically and customize copy independently:\n\n```ts\nparseQuantity('72', {\n  kind: 'length',\n  unit: 'cm',\n  escalate: { UNIT_ASSUMED: 'error' },\n  messages: { UNIT_ASSUMED: 'Confirm the unit before saving.' },\n})\n```\n\n### Convert & format\n\n```ts\nimport { quantity, convert, convertDelta, tryConvert } from '@pascal-app/lingo'\n\nconvert(100, 'C', 'F')          // 212        (absolute)\nconvertDelta(5, 'C', 'F')       // 9          (a difference; offsets don't apply)\nconvert(1, 'gal', 'L')          // 3.785411784 (exact; aliases work as refs)\nconvert(20, 'MB/s', 'Mbit/s')   // 160        (data rates are bits/s underneath)\nconvert(5, 'gpm', 'l/min')      // 18.92705892 (flow rates are m³/s underneath)\nconvert(1, 'M', 'μM')           // 1000000    (concentrations are mol/m³ underneath)\nconvert(1, 'g0', 'm/s2')        // 9.80665    (standard gravity; bare \"g\" is grams)\nconvert(10, 'N*m', 'lbf*ft')    // 7.37562149 (torque; declared units, not unit algebra)\nconvert(1, 'fc', 'lx')          // 10.7639104167 (illuminance)\nconvert(10, 'inH2O', 'Pa')      // 2490.8891  (water-column pressure is declared, not unit algebra)\nconvert(1, 'kgf/cm2', 'kPa')    // 98.0665    (older pressure gauges)\nconst targetUnit: string = 'cm'\ntryConvert(5, 'kg', targetUnit) // ok:false + structured issue instead of throwing\n\nquantity(1500, 'm').toBest().format()            // \"1.5 km\"\nquantity(1.8034, 'm').format({ compound: ['ft', 'in'] })  // \"5′11″\"\nquantity(2, 'ft').format({ style: 'long' })      // \"2 feet\"\nquantity(0.5, 'kg').format({ locale: 'de-DE' })  // \"0,5 kg\"\nquantity(1, 'm').format({ locale: 'fr-FR', style: 'long' }) // \"1 meter\" (parseable)\nquantity(1, 'm').format({ locale: 'fr-FR', style: 'long', localizedUnits: true }) // \"1 mètre\" (display-only)\n\nconvert(1, 'hp', 'W')                            // 745.699872 — force/power/frequency plus electrical + mole units\nlingo('500 mAh').quantity.to('C').value          // 1800 — battery charge is coulombs underneath\nlingo('5 uM').quantity.to('mol/m3').value        // 0.005 — concentration shorthands are exact-case\nlingo('20 mSv').quantity.to('Sv').value          // 0.02 — radiation equivalent dose is separate from gray\nlingo('5 MBq').quantity.to('Bq').value           // 5000000 — radioactivity is becquerels underneath\nlingo('1 kgf/cm²').quantity.to('kPa').value      // 98.0665 — use kgf/cm²; kg/cm² stays deferred\nquantity(3e5, 'm').format({ notation: 'scientific' })   // \"3e5 m\" (also 'engineering'; every style re-parses)\n```\n\nLiteral `Quantity.to()` / `QuantityRange.to()` targets are same-kind checked\ntoo; dynamic strings still validate at runtime.\n\nBare `bps` is finance shorthand for basis points. Use `bit/s`, `kbit/s`, or\nnetwork spellings such as `Mbps` when you mean bits per second.\nUntyped glued `1M` stays rejected because `M` is a future multiplier hazard;\nuse `1 M` or pass `kind: 'concentration'` for molar fields.\n\n### Currency\n\nCurrency is built in — parse symbols and codes, format via `Intl`, and convert\nwith **your own rates** (exchange rates aren't static, so lingo never bundles or\nfetches them — no hidden clock, no network):\n\n```ts\nimport { lingo, quantity, convertCurrency, fromMinor } from '@pascal-app/lingo'\n\nlingo('$5')                     // 5 USD + AMBIGUOUS_UNIT (bare \"$\" is ambiguous)\nlingo('$5', { currency: 'CAD' }) // 5 CAD        (explicit currency context)\nlingo('50 cents')               // 0.5 USD + AMBIGUOUS_UNIT (cent currency is ambiguous)\nlingo('50 cents', { currency: 'EUR' }) // 0.5 EUR\nlingo('five dollars and fifty cents').quantity.value // 5.5\nlingo('50p')                    // 0.5 GBP\nlingo('3 quid 50').quantity.value // 3.5\nlingo('between $5 and $10')     // 5–10 USD range + AMBIGUOUS_UNIT warnings\nlingo('5 bucks').quantity.unit  // 'USD'         (slang + ISO codes + names)\n\nquantity(5, 'USD').format()     // \"$5.00\"       (Intl; round-trips)\nquantity(5, 'USD').toMinor()    // 500           (Stripe integer minor units; JPY→0, KWD→3 decimals)\nfromMinor(500, 'USD').value     // 5\n\nconvertCurrency(100, 'USD', 'EUR', { rates: { base: 'USD', rates: { EUR: 0.92 } } }) // 92\n// cross-currency convert()/.to() throws WITHOUT rates — never a silent wrong answer\ntryConvert(100, 'USD', 'EUR') // ok:false, RATE_REQUIRED issue\n```\n\n### Type-safe by default\n\nUnit refs are literal-typed from the registry, so cross-kind and unknown-unit\nmistakes are **compile errors** — zero runtime cost. Dynamic strings still work\n(the escape hatch), so nothing breaks at the boundary:\n\n```ts\nconvert(5, 'in', 'cm')   // ✅ number\ntryConvert(5, 'in', 'cm') // ✅ { ok:true, value, unit, kind }\nquantity(5, 'kg')        // ✅ Quantity<'mass'> — kind inferred from the unit\nfromMinor(500, 'USD')    // ✅ Quantity<'currency'>\n\nconst tenant = createLingo()\ntenant.convert(5, 'in', 'cm') // ✅ built-in instances keep the same checks\ntenant.tryConvert(5, 'in', 'cm') // ✅ non-throwing instance equivalent\n\n// @ts-expect-error 'kg' is mass, 'cm' is length — caught before you run it\nconvert(5, 'kg', 'cm')\n// @ts-expect-error tryConvert keeps the same literal-unit check\ntryConvert(5, 'kg', 'cm')\n// @ts-expect-error 'nope' isn't a unit\nquantity(5, 'nope')\n\nconst u: string = userInput\nquantity(5, u)           // ✅ still compiles — validated at runtime\ntryConvert(5, u, 'cm')   // ✅ dynamic refs return ok:false issues instead of throwing\n```\n\n### Dates & durations: `@pascal-app/lingo/date`\n\n```ts\nimport { parseDate, parseDuration, humanizeDate, humanizeDuration } from '@pascal-app/lingo/date'\n\nparseDate('three days ago', { now })         // exact Date, grain 'day'\nparseDate('next tuesday', { now })           // + alternative interpretation attached\nparseDate('5/3', { dayFirst: true })         // March 5 vs May 3; AMBIGUOUS_DATE\nhumanizeDate(d, { now })                     // \"3 days ago\"; always re-parseable\nparseDuration('PT1H30M').duration.base       // 5400 (seconds)\nhumanizeDuration(5400, { style: 'natural' }) // \"an hour and a half\"\n```\n\nTimes of day, timezones, and slots parse too. Zones are **exposed** by default\n(the civil wall-clock is kept); pass `applyZone` to resolve the real UTC instant:\n\n```ts\nimport { parseDate, parseDateRange, humanizeDateRange } from '@pascal-app/lingo/date'\n\nparseDate('17h30', { now })                  // 17:30, grain 'minute'\nparseDate('quarter past 5', { now })         // 05:15\nparseDate('3pm EST', { now }).zone           // { source:'abbrev', offsetMinutes:-300, ... }\nparseDate('3pm EST', { now, applyZone: true }).date // the real 20:00Z instant\n\nconst slot = parseDateRange('2pm to 4pm', { now }) // { start, end } endpoints\nparseDateRange('9-5', { now })               // workday shift → 09:00–17:00\nparseDateRange('from 3pm', { now })          // open-ended (no end)\nhumanizeDateRange(slot)                      // \"2:00 PM to 4:00 PM\" — re-parseable\n```\n\n`parseDateRange` reads calendar ranges too, so one field can take a slot, a\ndated span, or a whole period without a mode toggle:\n\n```ts\nparseDateRange('Aug 3 - Aug 9', { now })     // dated span, day grain\nparseDateRange('July 1 to July 5', { now })  // the end resolves against the start\nparseDateRange('next week', { now })         // Monday through Sunday\nparseDateRange('August', { now })            // Aug 1 → Aug 31, the whole month\nparseDateRange('until August', { now })      // open start, ends Aug 31\nparseDateRange('this weekend', { now })      // Sat–Sun; on a Sat/Sun, the one in progress\nparseDateRange('2026-08-09 to 2026-08-03', { now })\n                                             // swapped + RANGE_REVERSED warning\n```\n\nA coarse endpoint widens on the *closing* side, which is why `July to August`\nends on August 31 while `from August` still opens on the 1st. The result carries\na `dated` flag saying which grammar matched, and `humanizeDateRange` renders\ndated ranges as dates (`\"2026-08-01 to 2026-08-31\"`) rather than clock times.\nQuarters (`Q3`), elliptical right sides (`Aug 3-9`), and dash-joined ISO dates\nwith no spaces (`2026-08-01-2026-08-05`) are not in the grammar and return\n`UNSUPPORTED_DATE` instead of a guess.\n\nThe humanizer's output is guaranteed re-parseable by the parser (round-trip tested):\n`parseDate(humanizeDate(d, { now }), { now })` lands within one display-grain of `d`.\n\n### Calculations: `@pascal-app/lingo/calc`\n\nA closed calculator over quantities and numbers. `lingo()` never does this —\n`5-10 kg` stays a range, `2 ft + 3 in` stays a compound, and `2+3 kg` is\ntrailing input rather than a silent 2–3 kg range.\n\n```ts\nimport { calc } from '@pascal-app/lingo/calc'\n\nconst million = calc('7m*2')\nmillion.ok && million.value                         // 14000000\nmillion.ok && million.format({ style: 'words' })    // \"14 million\"\nmillion.ok && million.format({ style: 'grouped' })  // \"14,000,000\"\nmillion.ok && million.format({ style: 'scientific' }) // \"14e6\"\nmillion.ok && million.format({ style: 'compact' })  // \"14m\"\nmillion.ok && million.expression                    // \"7e6 × 2\"\nmillion.ok && million.latex                         // \"7 \\\\times 10^{6} \\\\times 2\"\n\nconst duration = calc('9min x 4')\nduration.ok && duration.format()                    // \"36 min\"\nduration.ok && duration.format({ unit: 'h' })       // \"0.6 h\"\n\ncalc('half of 56kg+1700g')                          // 28.85 kg\n```\n\nGlued `m` at an operator boundary is million unless `kind` is `length` or\n`duration`; spaced `7 m` is meters; `1m80` stays 1.80 m. Compact `\"14m\"`\nround-trips through `calc()`, not `lingo()`. Same-kind `q / q` cancels to a\nratio; cross-currency arithmetic is `RATE_REQUIRED`; `q * q` is\n`SCALAR_EXPECTED`. Completions need a leading `=` so\n`5-10 kg` stays a range. `quantityField({ calc })` evaluates `12 * 0.75 kg`\nand `10 kg / 2` without a prefix (`5/10 kg` stays a fraction):\n\n```ts\nimport { calc } from '@pascal-app/lingo/calc'\nimport { quantityField } from '@pascal-app/lingo/ai'\nimport { completions } from '@pascal-app/lingo/complete'\n\nquantityField({ kind: 'mass', unit: 'kg', calc }).parse('12 * 0.75 kg') // 9\ncompletions('=2+3 kg', { calc: (text) => calc(text, { trigger: '=' }) })\n```\n\nThe grammar cannot express a call, a scope, or a side effect.\n\n### Forms: `@pascal-app/lingo/dom`\n\nTurn any `<input>` into a natural-language field. Headless: no styles shipped,\nstate exposed as data-attributes.\n\n```ts\nimport { lingoInput } from '@pascal-app/lingo/dom'\n\nconst field = lingoInput(document.querySelector('#height'), {\n  kind: 'length',\n  unit: 'm',              // canonical unit received by your backend\n  name: 'height_m',       // hidden input carries \"1.8034\" on submit\n  errorElement: '#height-error',\n  hintElement: '#height-hint',\n})\n\n// user types: 5'11     -> hint shows \"= 1.80 m\", state 'valid'\n// user types: 5 f      -> state 'incomplete' (never invalid mid-typing)\n// user types: banana   -> on blur: aria-invalid, error text + announcement\n// on blur              -> input shows \"1.8 m\" (display: 'canonical')\n\nfield.value      // 1.8034 (number, in meters)\nfield.set('6ft') // programmatic input works too\n```\n\nAccessibility is built in, not bolted on: `aria-invalid`, `aria-describedby` wired\nonly while an error exists, debounced visual hints while typing,\n`role=\"alert\"` commit errors, Constraint Validation integration\n(`validationBehavior: 'native'`), IME-composition safe, `:user-invalid` timing.\n\nStyle it with attribute selectors:\n\n```css\n[data-lingo][data-state='invalid'][data-touched] { border-color: crimson; }\n[data-lingo][data-approx] { border-style: dashed; }\n```\n\n### React: `@pascal-app/lingo/react`\n\n```tsx\nimport { completions } from '@pascal-app/lingo/complete'\nimport { useLingoInput } from '@pascal-app/lingo/react'\n\nfunction HeightField() {\n  const field = useLingoInput({\n    kind: 'length',\n    unit: 'm',\n    name: 'height_m',\n    listboxId: 'height-options',\n    complete: (text) => completions(text, { kind: 'length' }),\n  })\n  return <input ref={field.ref} placeholder={`try 5'11\" or 180cm`} />\n}\n```\n\nThe hook exposes `completions`, `highlightedIndex`,\n`setHighlightedIndex()`, and `selectCompletion()` for a caller-rendered\nlistbox. `./complete` stays an injected import; `./react` does not bundle it or\nship popup UI.\n\n### React Native: `@pascal-app/lingo/react-native`\n\n```tsx\nimport { Text, TextInput, View } from 'react-native'\nimport { useLingoTextInput } from '@pascal-app/lingo/react-native'\n\nfunction WeightField() {\n  const field = useLingoTextInput({\n    kind: 'mass',\n    unit: 'kg',\n    min: 0,\n    max: '500 kg',\n  })\n  return (\n    <View>\n      <TextInput {...field.inputProps} placeholder=\"165 lb or 75 kg\" />\n      {field.errorMessage ? <Text>{field.errorMessage}</Text> : null}\n    </View>\n  )\n}\n```\n\n`onChangeText` parses without rewriting, while blur, submit, or `commit()`\ncanonicalizes the display. `value` is the canonical number and `submitValue`\nis the backend-ready string. Inject `complete` from `./complete` for ranked\nsuggestions (`completions` / `selectCompletion()`); render them with your own\n`FlatList`. The entry imports React only—there is no DOM controller,\n`react-native` runtime dependency, or bundled completion UI.\n\n### Web components: `@pascal-app/lingo/element`\n\nA form-associated custom element for design systems with no framework\nadapter. It uses the same parser, wired through `ElementInternals` instead of a\nhidden input:\n\n```html\n<script type=\"module\">\n  import { defineLingoInput } from '@pascal-app/lingo/element'\n  defineLingoInput()\n</script>\n<form>\n  <lingo-input name=\"height_m\" kind=\"length\" unit=\"m\"></lingo-input>\n</form>\n```\n\n`<lingo-input>` keeps a real `<input type=\"text\">` in light DOM, not shadow\nDOM, so native labels and page CSS keep working, and `FormData`, `:invalid`,\nand `formResetCallback` behave like any native control, in Vue, Svelte,\nAngular, or plain HTML.\n\nUsing React Hook Form, TanStack Form, Formik, Vue, Angular, or shadcn\ninstead? Each has its own recipe in [`docs/recipes.md`](https://github.com/pascalorg/lingo/blob/main/packages/lingo/docs/recipes.md).\nThey use the same fields and the same scoped form-UX case: collapse one value + one unit\ndropdown into a single text field, not \"replace the whole form with a\nsentence.\"\n\n### Completions: `@pascal-app/lingo/complete`\n\n`completions(input, opts)` returns **ranked, fully-parsed** canonical readings of\npartial or ambiguous input — prefix fan-out, unit-ambiguity forks, number\nalternatives, implied units, range-tail units — each a `text` plus a successful\nquantity/range/conversion result. It powers comboboxes and is distinct from a parse\ncandidate/alternative/suggestion.\n\n```ts\nimport { completions } from '@pascal-app/lingo/complete'\n\ncompletions('2 f', { kind: 'length' }).map((c) => c.text)\n// ['2 ft', '2 ft-us', '2 fathom', '2 furlong', …]\ncompletions('5', { kind: 'length' }).map((c) => c.text)\n// ['5 m', '5 ft', '5 cm', '5 in', '5 km', '5 mi']\n```\n\nThe DOM field and React hook are headless about this too: inject `complete`,\nrender your own dropdown, and use `onComplete` or the hook's completion state.\nWhen completions are enabled the input gets the combobox side of the ARIA\ncontract (`role=\"combobox\"`, `aria-autocomplete=\"list\"`, `aria-expanded`), and\n`listboxId` sets `aria-controls` for your listbox.\n\n## Extending\n\n```ts\nimport { registerKind, registerUnits, defineFuzzyVocab } from '@pascal-app/lingo'\n\nregisterUnits('length', [{ id: 'smoot', symbol: 'smoot', name: 'smoot',\n  factor: 1.702, system: 'us', aliases: ['smoots'] }])\n\ndefineFuzzyVocab('mass', { profile: 'parcels', unit: 'kg',\n  terms: { light: [0, 5], heavy: [20, 70] } })\n// lingo('heavy', { kind: 'mass', profile: 'parcels' }) -> 20 to 70 kg\n```\n\nCustom kinds (static-rate currencies, house scores, and similar local units) get parsing, conversion,\nformatting, and did-you-mean for free. For a minimal build, `@pascal-app/lingo/core`\nships the engine with an empty registry. Bring only the kinds you need (core\nships without English error copy; register it with\n`setDefaultMessages(englishMessages)` or bring your own via `messages`).\nRegistry inspection APIs such as `unitsOf()` return live definitions. Mutating them\nis an advanced escape hatch, at your own risk.\n\nNeed isolation instead of globals for SSR, multi-tenant apps, or tests? Built-in\n`createLingo()` returns a typed instance with its own registry, messages, and\nfuzzy vocab; custom `kinds`/`registry` instances keep broad string refs because\ntheir vocabulary is user-owned. Nothing you register on an instance (or mutate\nafter passing in) leaks anywhere else:\n\n```ts\nimport { createLingo } from '@pascal-app/lingo'\n\nconst metricOnly = createLingo({ messages: { UNKNOWN_UNIT: 'Metric units only.' } })\nmetricOnly.parse('5 kg')          // isolated instance, same API\n```\n\nParse non-English input by loading packs on an instance — English stays the default:\n\n```ts\nimport { createLingo } from '@pascal-app/lingo'\nimport { parseDate } from '@pascal-app/lingo/date'\nimport { es } from '@pascal-app/lingo/locales/es'\nimport { zh } from '@pascal-app/lingo/locales/zh'\n\nconst app = createLingo({ locales: [es, zh] })\napp.parse('dos kg', { locale: 'es' })           // 2 kg\napp.parse('al menos 2 m', { locale: 'es' })     // ≥ 2 m  (open range)\napp.parse('entre 5 y 10 kg', { locale: 'es' })  // 5–10 kg\n\nconst spanish = app.parseQuantity('treinta y cinco kilos', { locale: 'es' })\nspanish.ok && spanish.quantity.to('kg').value   // 35\n\nconst chinese = app.parseQuantity('三十五公斤', { locale: 'zh' })\nchinese.ok && chinese.quantity.to('kg').value   // 35\n\n// comparators that follow the quantity, as CJK writes them\napp.parse('5キロ未満', { locale: 'ja' })          // < 5 kg  (open range)\napp.parse('5公斤以上', { locale: 'zh' })          // ≥ 5 kg\napp.parse('100元', { locale: 'zh' })             // 100 CNY\n\nconst clock = parseDate('las tres menos cuarto', {\n  locale: 'es',\n  localePacks: [es],\n  now: new Date(2026, 6, 3, 14, 30),\n})\nclock.ok && [clock.date.getHours(), clock.date.getMinutes()] // [2, 45]\n\nconst cjkDate = parseDate('2026年3月5日', { locale: 'zh', localePacks: [zh] })\ncjkDate.ok && [cjkDate.date.getMonth() + 1, cjkDate.date.getDate()] // [3, 5] (local)\n\n// omit `locale` to auto-detect among the loaded packs\n```\n\nPacks (`en`, `en-gb`, `es`, `fr`, `pt`, `zh`, `ja`) are additive and tree-shakeable;\nthey add number words, units, ranges, and relative dates (through\n`@pascal-app/lingo/date`) for their language. Coverage includes Romance\ntens/hundreds/scale words (`ciento veinte`, `mille cinq cents`, `mil millones`),\nCJK scale/grouped numbers and post-unit half, postpositional comparators\n(`5キロ未満`, `5公斤以上`), per-locale currency defaults (`￥` is CNY under `zh`,\nJPY under `ja`), localized clock phrases and period edges, and CJK calendar\ndates and clocks (`2026年3月5日`, `午後3時半`, `明天下午3点`). Requesting an\nunloaded locale returns `LOCALE_NOT_LOADED` rather than silently parsing as\nEnglish; `humanizeDate()` output stays English for now.\n\nThe global `lingo()` is itself a `createLingo()` singleton. One code path,\ntwo tiers of convenience.\n\n## Recipes\n\nThree common field shapes, straight from the public API. The full set lives\nin [`docs/recipes.md`](https://github.com/pascalorg/lingo/blob/main/packages/lingo/docs/recipes.md): field recipes (ingredients, fuzzy\ntemperature, strict scientific fields), server-side validation, AI SDK /\nOpenAI / Anthropic / Gemini / LangChain / MCP / eval recipes, form-library\nrecipes (React Hook Form, TanStack Form, Formik, Vue, Angular, shadcn,\nvanilla), database input, and a per-vertical form-UX gallery.\n\n**Height field (feet/inches to meters):**\n\n```ts\nimport { parseQuantity } from '@pascal-app/lingo'\n\nfunction heightMeters(input: string): number | null {\n  const r = parseQuantity(input, { kind: 'length', strictness: 'confirm' })\n  return r.ok ? r.quantity.to('m').value : null\n}\n\nheightMeters(`5'11\"`) // 1.8034\nheightMeters('180cm') // 1.8\nheightMeters('tall')  // null\n```\n\n**Shipment weight (strict, ranges rejected):**\n\n```ts\nimport { lingo } from '@pascal-app/lingo'\n\nconst shipmentOpts = { kind: 'mass', strictness: 'strict', accept: { ranges: false } } as const\n\nlingo('20 kg', shipmentOpts)    // { ok: true, type: 'quantity', quantity: 20 kg }\nlingo('20-25 kg', shipmentOpts) // ok: false, SINGLE_VALUE_EXPECTED, candidate: range 20-25 kg\n```\n\n**Agent form filling (LLM emits natural language, lingo canonicalizes):**\n\n```ts\nimport { parseQuantity, type Kind } from '@pascal-app/lingo'\n\nconst schema: Record<string, { kind: Kind; unit: string }> = {\n  height: { kind: 'length', unit: 'm' },\n  weight: { kind: 'mass', unit: 'kg' },\n}\n\nfunction canonicalize(field: string, raw: string): number {\n  const { kind, unit } = schema[field]!\n  const r = parseQuantity(raw, { kind, strictness: 'confirm' })\n  if (!r.ok) throw new Error(r.issues[0]?.message ?? `invalid ${field}`)\n  return r.quantity.to(unit).value\n}\n\ncanonicalize('height', `5'11\"`)   // 1.8034\ncanonicalize('weight', '180 lbs') // 81.6466266\n```\n\nFor tool schemas, prefer the `@pascal-app/lingo/ai` fields below. They add\ntool-boundary defaults for ambiguity rejection, bounds, closed schemas, and\nsurfaced warnings.\n\n## For AI: structured output & agent form filling\n\nConstrained decoding guarantees the JSON parses; lingo guarantees the values\nmean what they should. JSON mode can't stop `\"2kg\"` landing in a number field\n(`Number(\"2kg\")` → `NaN`, `z.coerce.number()` → `NaN` too), `\"1,5\"` losing its\nlocale, or `new Date(\"03/04/2025\")` silently picking the wrong month. Models\nare better at emitting `\"5'11\\\"\"` than `1.8034`; `@pascal-app/lingo/ai` makes\nthat string the reliable path.\n\n**Tool-boundary defaults** (plan 020). Human fields default to forgiving because\na person can read the hint. A tool argument has no human in the loop, so the\n`/ai` fields use stricter defaults, each with a one-line escape hatch:\n\n- Genuinely ambiguous numbers fail with a candidate: `\"1,234 kg\"` →\n  `[AMBIGUOUS_NUMBER] ... Did you mean 1234 kg?` so the model can self-correct in one\n  round trip (`escalate: { AMBIGUOUS_NUMBER: 'warning' }` restores absorption).\n- Ignored timezones fail: `\"3pm EST\"` → `[TZ_IGNORED]` instead of silently\n  landing in the host zone.\n- Reference-dependent dates require an explicit `now`: `dateField()` rejects\n  `\"tomorrow\"`, `\"March 5\"`, and time-only `\"at 3pm\"` with `NOW_REQUIRED` and\n  no implicit candidate, so a queued or retried tool call can never drift across\n  midnight (`requireNow: false` opts out by resolving from the wall clock at\n  field validation; fully absolute dates are unaffected).\n- `min`/`max` bounds reject absurd magnitudes (`-5 kg`, `2000000 kg`) with\n  RANGE_MIN/RANGE_MAX; they're advertised in the input description on every\n  field (plus JSON Schema `minimum`/`maximum` on numeric outputs) so the\n  model is steered before it generates. Date-only bounds are local calendar\n  days: `min: '2026-01-01'` accepts `\"2026-01-01\"` in every timezone.\n- `rangeField()` defaults to compact `{ min, max }` numbers. Open-ended input\n  like `\"under 10 kg\"` fails with RANGE_OPEN_BOUND_NOT_ALLOWED there; use\n  `output: 'range'` when the tool should receive full QuantityRange JSON with\n  open bounds, exclusivity, fuzzy/approximate origin, and `baseUnit`.\n- `dateRangeField()` canonicalizes any `parseDateRange` shape — a time slot\n  (`\"2pm to 4pm\"`, `\"9-5\"`, `\"from 3pm\"`), a dated span (`\"Aug 3 - Aug 9\"`), or a\n  whole calendar period (`\"next week\"`, `\"August\"`) — to `{ start?, end?: ISO }`\n  with the same reference-time and timezone guards as `dateField` (`applyZone`\n  resolves real instants; `TZ_IGNORED` escalates to error).\n- `lingoObject` is **closed**: `additionalProperties: false`, unknown keys\n  fail, and the shape matches OpenAI strict structured outputs\n  (`{ passthrough: true }` restores open objects).\n- Benign forgiveness still succeeds. Typos, qualifiers, and assumed units ride\n  along as `warnings` on the success result (`safeParse`), so nothing is silently\n  swallowed. Canonical numbers are float-safe (`1.36077711`, never\n  `1.3607771100000001`).\n\n**Schema-native** (Standard Schema, both halves. AI SDK v6/v7 detect\n`~standard` directly, no wrapper, no Zod):\n\n```ts\nimport { generateText, tool } from 'ai'           // ai@6 or ai@7, no wrapper needed\nimport { quantityField } from '@pascal-app/lingo/ai'\n\nconst weight = quantityField({ kind: 'mass', unit: 'kg', min: 0, max: 200 })\nconst logWeight = tool({\n  description: 'Log a package weight',\n  inputSchema: weight,                            // LingoField passed directly\n  execute: async (kg) => ({ ok: true, kg }),       // \"12 lbs\" → kg is 5.443...\n})\n\nconst { text } = await generateText({ model, tools: { logWeight }, prompt: 'Log 12 lbs.' })\n// Output.object({ schema: lingoObject({ weight, loggedAt: dateField() }) }) works the same\n// way for whole-response structured output. No tool call needed.\n```\n\nThe field's *input* JSON Schema is `type: \"string\"` with a natural-language\ndescription. Strict provider modes let the model write what it's good at,\nand lingo canonicalizes on validation. The *output* is the canonical shape.\nOn **AI SDK v5**, `asSchema()` has no Standard Schema branch yet. Wrap the\nfield with the SDK's own `jsonSchema()` first (the ~10-line bridge is in\n`docs/recipes.md`); `generateObject`/`experimental_repairText` are deprecated\non v6/v7 in favor of `Output.object`/`experimental_repairToolCall` (below).\nOne gap either way: the SDK's schema adapter only reads\n`{success,value}`/`{success,error}`, so lingo's `warnings` (typo-fixed,\nunit-assumed, and similar warnings) never reach it. Call `weight.safeParse(input)` yourself,\ninside `execute()`, to see them.\nDirect lingo field calls keep issues structured: failure issues still have the\nStandard Schema `message`/`path`, and also carry lingo's `code`, `severity`,\nfield-input `span`, `data`, `suggestions`, and did-you-mean `candidate` when\navailable. `[CODE]` stays in the message for model/tool self-repair; callers no\nlonger need to parse that copy.\n\nOther providers use the same field. No lingo-specific adapter needed:\n\n- **OpenAI**: wrap in `lingoObject` (a bare field can't be a tool's whole\n  `parameters`) and set `strict: true`. Wiring a field's `.output()` schema\n  in by mistake is strict-legal but silently reverts the tool to \"emit a bare\n  number.\"\n- **Anthropic**: one cast:\n  `schema['~standard'].jsonSchema.input({...}) as\n  Anthropic.Tool.InputSchema`; `lingoObject`'s closed default already\n  satisfies `strict: true` on both OpenAI and Anthropic.\n- **Gemini**: target `parametersJsonSchema`/`responseJsonSchema`, never\n  classic `parameters`/`responseSchema`. The classic path silently drops or\n  rejects `additionalProperties`.\n- **One schema, many providers**: that same closed shape is also Grok's,\n  Mistral's, Cohere v2's, Groq's, Ollama's, and Hugging Face's exact\n  strict-mode contract. One schema works across those providers without\n  per-provider glue.\n- **LangChain**: `createAgent`/`withStructuredOutput` take a lingo field\n  directly but only validate its shape. Run `canonicalizeValues` (or the\n  field's `.safeParse`) yourself for lingo's ambiguity/bounds checks.\n- **MCP**: `lingoTool()` (`@pascal-app/lingo/mcp`) turns a shape into a full\n  tool descriptor. See MCP tools, below.\n- **Evals**: `quantityMatch`/`dateMatch` canonicalize both sides through the\n  same field before comparing (`\"2 lbs\"` grades equal to `0.90718474` kg) and\n  return `{ pass, score, reason }`. Drop into promptfoo, autoevals, or a\n  plain Vitest assertion.\n\nTwo more small helpers: `optional(field)` keeps a key in the schema's\n`required` list while letting it validate to `null` (the null-union idiom\nOpenAI/Anthropic strict modes both expect, without hand-writing the union);\n`toJSONSchema(field, { io, target })` returns just the JSON Schema fragment\nfor a hand-rolled `parameters`/`input_schema` instead of passing the field\nitself.\n\n**Repair & post-processing** for schemas you don't own. Client-side, no\nextra model call:\n\n```ts\nimport { canonicalizeValues, repairTextWith, repairToolCallWith } from '@pascal-app/lingo/ai'\n\n// AI SDK v6/v7 tool calls; generateObject/experimental_repairText are deprecated;\n// this is their tool-call-shaped successor's repair hook:\nawait generateText({ ...,\n  experimental_repairToolCall: repairToolCallWith({ logWeight: { weight: { kind: 'mass', unit: 'kg' } } }) })\n\n// v5's generateObject still works with the original hook:\nawait generateObject({ ...,\n  experimental_repairText: repairTextWith({ weight: { kind: 'mass', unit: 'kg' } }) })\n\ncanonicalizeValues(payload, { 'items[].weight': { kind: 'mass', unit: 'kg' } })\n```\n\n**MCP tools.** `@pascal-app/lingo/mcp`'s `lingoTool()` turns a shape into a\ncomplete tool descriptor: JSON Schema out, `safeParse` in, structured lingo\nissues for code, and `[CODE]` messages the model can self-correct from\n(plan 021):\n\n```ts\nimport { dateField, quantityField } from '@pascal-app/lingo/ai'\nimport { lingoTool } from '@pascal-app/lingo/mcp'\n\nconst createShipment = lingoTool({\n  name: 'create_shipment',\n  description: 'Create a shipment; natural-language values welcome',\n  input: {\n    weight: quantityField({ kind: 'mass', unit: 'kg', min: 0, max: 500 }),\n    deliverBy: dateField({ min: '2026-01-01' }),\n  },\n  handler: (args) => saveShipment(args),  // canonical kg + ISO date, bounds enforced\n})\n\n// Any MCP SDK that accepts JSON Schema tool input:\nserver.registerTool(\n  createShipment.name,\n  { description: createShipment.description, inputSchema: createShipment.inputSchema },\n  createShipment.callback, // \"1,234 kg\" → isError, \"[AMBIGUOUS_NUMBER] ... Did you mean 1234 kg?\"\n)\n```\n\nWith `requireNow` on, `\"tomorrow\"` bounces back with `NOW_REQUIRED` and no\nimplicit candidate. The model resends an absolute date or the caller supplies a\nreference `now`, and a queued tool call can never drift across midnight.\nBuilding the descriptor by hand instead?\n`lingoObject(shape)['~standard'].jsonSchema.input({ target: 'draft-2020-12' })`\nis the same schema `lingoTool` builds internally, and `.safeParse()` is the\nsame check it runs before your handler.\n\n**Computer use / browser agents**: fields wired with `lingoInput` accept\nwhatever the agent types. Verified live: a synthetic-event agent typing\n`5'11\"` commits `1.8034` to the hidden canonical input, `2 lbs` commits\n`0.907` kg, `1,5 m` commits `1.5`. No agent-specific code; the form itself\nbecomes format-proof.\n\n**One schema for tools and forms.** A `lingoObject` built for a tool argument (like\nthe ones above) also validates and canonicalizes a human form, unchanged,\nthrough any Standard-Schema-aware form resolver:\n\n```tsx\nimport { standardSchemaResolver } from '@hookform/resolvers/standard-schema'\nimport { useForm } from 'react-hook-form'\nimport { dateField, lingoObject, quantityField } from '@pascal-app/lingo/ai'\n\nconst shipment = lingoObject({\n  weight: quantityField({ kind: 'mass', unit: 'kg', min: 0 }),\n  deliverBy: dateField({ now: new Date() }),\n})\n\nuseForm({ resolver: standardSchemaResolver(shipment) })\n// handleSubmit(onValid) receives canonical kg + an ISO date, not raw text.\n// the same shape a model's tool call would produce.\n```\n\nOne caveat: `lingoObject` is closed by default (the tool-boundary default\nabove), so a form submitting fields lingo doesn't declare needs\n`lingoObject(shape, { passthrough: true })`. React Hook Form, TanStack Form,\nFormik, Vue, Angular, and shadcn recipes all live in `docs/recipes.md`.\n\n**Also true of the core API**: explicit `now`, no locale sniffing, stable\nissue codes, `toJSON()`/`fromJSON()`, opt-in `@pascal-app/lingo/describe`\nrich views, numeric confidence, enumerable discriminated alternatives.\n\n**Measured** (`bun run ai-eval`, 160-fixture recorded corpus across 10 failure\nmodes; this is a canonicalization demo, not an LLM benchmark): naive\n`Number()`/`new Date()` coercion accepts 17.5% of the corpus and is silently\nwrong on 6.3% of it (drifted dates parse as the wrong day without an error);\nlingo preprocessing accepts 96.9% with **zero silent-wrongs** and rejects the\nremaining 3.1%, genuinely ambiguous separators like `\"1,234\"`, with a\nmachine-actionable candidate instead of a guess. The worst naive row:\nnatural-language dates, where 50% were accepted and all accepted rows were wrong.\n\n## Issue codes\n\n`EMPTY · NO_VALUE · UNKNOWN_UNIT · KIND_MISMATCH · RANGE_KIND_MISMATCH ·\nCONVERSION_KIND_MISMATCH · RATE_REQUIRED · TRAILING_INPUT · SINGLE_VALUE_EXPECTED ·\nAPPROX_NOT_ALLOWED · UNIT_REQUIRED · CONVERSION_NOT_ALLOWED · NUMBER_FORMAT ·\nNONFINITE · LOCALE_NOT_LOADED · RANGE_MIN · RANGE_MAX · REQUIRED · UNSUPPORTED_DATE · NOW_REQUIRED ·\nRANGE_OPEN_BOUND_NOT_ALLOWED · TYPO_CORRECTED · AMBIGUOUS_NUMBER ·\nAMBIGUOUS_UNIT · AMBIGUOUS_DATE · RANGE_REVERSED · COMPOUND_OVERFLOW ·\nCIVIL_AVERAGE · UNIT_ASSUMED · WEEKDAY_ASSUMED_NEXT · SLANG_UNIT · TZ_IGNORED ·\nAMBIGUOUS_TIMEZONE · AFFINE_DELTA_ASSUMED · EXPRESSION_KIND_MISMATCH ·\nSCALAR_EXPECTED · DIVISION_BY_ZERO · SCALE_ASSUMED`\n\nEvery issue carries a typed `data` payload (`LingoIssue<'UNKNOWN_UNIT'>` knows\n`data.unit` and `data.suggestions`). `NOW_REQUIRED` fires when a date input\ndepends on a reference time (\"in 2d\", \"tomorrow\", \"March 5\", \"at 3pm\") without\nan explicit `now`; no implicit candidate is attached.\n\nOverride any message: `parseQuantity(text, { messages: { UNKNOWN_UNIT: 'Try cm or ft!' } })`.\n\nResult helpers, so you never hand-roll the same narrowing twice:\n\n```ts\nimport { firstError, isQuantity, isNumber, candidateOf, formatIssue } from '@pascal-app/lingo'\n\nconst r = lingo(input, opts)\nif (isQuantity(r)) save(r.quantity.to('m').value)\nelse if (isNumber(r)) save(r.value) // bare number, no unit (\"72\")\nelse showError(formatIssue(firstError(r)!), candidateOf(r)?.quantity)\n```\n\n## Design notes\n\nThe specs live in [`plans/`](https://github.com/pascalorg/lingo/blob/main/plans/README.md) (grammar, unit hazard tables, exact\nconversion factors, ambiguity policy) and the as-built docs in [`wiki/`](https://github.com/pascalorg/lingo/blob/main/wiki/README.md).\nMore field recipes live in [`docs/recipes.md`](https://github.com/pascalorg/lingo/blob/main/packages/lingo/docs/recipes.md).\nThe full credits ledger for libphonenumber-js, date-fns, Humanizer, inclusive-dates,\nchrono-node, react-aria, Maskito, and other references is\n[`wiki/inspiration.md`](https://github.com/pascalorg/lingo/blob/main/wiki/inspiration.md).\n\n## License\n\nMIT\n","readmeFilename":"README.md"}