The moment a product takes its first customer from another country, its forms start to break. A UK postcode has a space in the middle, a German VAT number starts with DE, an Indian PIN never starts with 0, a Japanese price has no decimals, and nobody types a phone number the same way twice. Most teams patch this with a regular expression per field, then a paid API, then a bug report.
We hit the same wall building software for customers in different countries, so we packaged the rules once. The result is six small TypeScript libraries, free and with no third-party dependencies, that check international data offline.
The six packages
@lacspace/country: ISO 3166-1 countries with alpha-2, alpha-3 and numeric codes, names, calling codes, currencies and regions. Looks up by any code, name or common alias such as "UK".@lacspace/currency: ISO 4217 currencies with their real minor units (two for dollars, zero for yen), conversion to integer minor units and formatting.@lacspace/iban: IBAN validation and parsing (ISO 13616, MOD 97-10) for every registry country, plus BIC/SWIFT and ISIN checks.@lacspace/tax-id: tax and business IDs for 44 countries, using each scheme's real checksum where one exists.@lacspace/phone: parse any human spelling of a phone number into E.164, with international and national formats.@lacspace/postal-code: validate and normalise postal codes for 129 countries, with the correct spacing and casing.
What it looks like
Every output below came from running the code against the published packages.
import { validateTaxId } from "@lacspace/tax-id";
validateTaxId("DE136695976");
// { valid: true, country: "DE", type: "vat", strength: "checksum", normalized: "DE136695976" }
validateTaxId("51 824 753 556", { country: "AU" });
// { valid: true, country: "AU", type: "abn", strength: "checksum", normalized: "51824753556" }
validateTaxId("27AAPFU0939F1ZV", { country: "IN" });
// { valid: true, country: "IN", type: "gstin", strength: "checksum", ... }
import { parseIban } from "@lacspace/iban";
parseIban("GB82 WEST 1234 5698 7654 32").valid; // true
parseIban("GB83WEST12345698765432");
// { valid: false, reason: "check-digits", country: "GB" }
import { formatPhone, parsePhone } from "@lacspace/phone";
formatPhone("(415) 555-2671", "e164", "US"); // "+14155552671"
parsePhone("07911 123456", { defaultCountry: "GB" }).international; // "+44 7911 123456"
import { validatePostalCode, formatPostalCode, isValidPostalCode } from "@lacspace/postal-code";
validatePostalCode("sw1a 1aa", "GB"); // { valid: true, country: "GB", normalized: "SW1A 1AA" }
formatPostalCode("902101234", "US"); // "90210-1234"
isValidPostalCode("012345", "IN"); // false: Indian PINs never start with 0
import { formatCurrency, toMinor } from "@lacspace/currency";
formatCurrency(1234.5, "USD"); // "$1,234.50"
formatCurrency(1234.5, "EUR", { locale: "de" }); // "1.234,50 €"
formatCurrency(1234.5, "JPY"); // "¥1,235" (yen has no minor unit)
toMinor(19.99, "USD"); // 1999
Why offline matters
A validation API adds a network call, a key to manage and a bill, and it fails when the network does. These checks run in the browser as the user types, so a mistyped IBAN or VAT number is caught before the form is sent, and the same function runs again on the server, so nothing slips through. There is no quota and nothing leaves your system.
Be clear about the limit, though: a valid checksum proves a number is well formed, not that the company or account exists. Use these packages to stop typos and junk at the door, and the official registers when you need proof of registration.
Store money and phones the safe way
Two habits remove most international bugs. Store amounts as integers in the currency's minor unit (cents, paise, pence) and format only for display; @lacspace/currency knows which currencies have two decimals and which have none. Store phone numbers in E.164, such as +61412345678, and format them for display per country; @lacspace/phone gives you both from one parse.
Get started
npm install @lacspace/country @lacspace/currency @lacspace/iban @lacspace/tax-id @lacspace/phone @lacspace/postal-code
Each package ships ESM and CommonJS builds with TypeScript types and works in Node.js, browsers, Deno, Bun and edge runtimes. They are part of the @lacspace open-source packages; the full catalogue is on developer.lacspace.com.
Building a product that has to work across countries? Book a free 30-minute call and we will tell you honestly what we would build first.
Frequently asked questions
Which tax IDs can @lacspace/tax-id check?
It covers 44 countries, including EU VAT numbers for all 27 member states plus the UK, Indian GSTIN and PAN, Australian ABN, ACN and TFN, New Zealand IRD, Canadian BN, US EIN, Brazilian CPF and CNPJ, Nepali PAN, Singapore UEN, South Korean and Japanese business numbers and South African tax numbers. Where a scheme has a checksum, the real checksum is verified; the result tells you whether a match was by checksum or by format only.
Does a valid checksum mean the business is registered?
No. A checksum proves the number is well formed, which catches typos and made-up numbers before they reach your database. To confirm a business is active, you still need the official register, such as VIES for EU VAT or the ABN Lookup in Australia.
Do these packages call an API?
No. All the data and rules ship inside the package, so validation runs offline, in the browser or on the server, with no key, quota or network round trip.
Are they free for commercial use?
Yes. They are published under the Lacspace Free Licence, which allows commercial use. The licence centre has the full text.
Do they have dependencies?
None from third parties. Five of the six have zero dependencies; @lacspace/phone depends only on @lacspace/country for calling codes.





