Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.
Open-source spec and engine for locale-correct typography: curly quotes, dashes, ellipses and no-break spaces in 20+ locales. Live in-browser playground.
Key Results

Every language has its own rules for quotes, dashes and spacing, and almost no software respects them. English text gets French guillemets from a careless find-and-replace; German gets English’s curly quotes the wrong way round; every locale gets a straight - where a language-specific dash belongs. The usual fix is a pile of regexes that half-work for one language and quietly corrupt the next – because ‘smart quotes’ was never actually a locale-aware problem, it was a find-and-replace problem wearing a locale’s clothes.
The deeper issue is that nobody had written down what ‘correct’ means, locale by locale, in a form software could check itself against. Style guides exist – Duden for German, the Imprimerie nationale for French, Kotus for Finnish, Chicago for US English, Мильчин for Russian – but each lives in its own document, in its own language, with no shared vocabulary and no test suite. A team shipping to many markets either hand-rolls a set of rules per language or ships the same English-shaped punctuation everywhere.
Same input, a different correct answer per locale – this is what ‘locale-aware’ actually buys you. Six locales below, taken verbatim from the spec’s own fixture suite (spec/fixtures/*.json) – the same fixtures the implementations are tested against:
| Locale | Input | Output |
|---|---|---|
| en-US | She said "hello" twice. | She said “hello” twice. |
| en-GB | She said "hello" twice. | She said ‘hello’ twice. |
| de-DE | Er sagte "hallo" zweimal. | Er sagte „hallo“ zweimal. |
| fr | Il a dit "bonjour". | Il a dit « bonjour ». |
| ru | Он сказал: "это 'моё' дело". | Он сказал: «это „моё“ дело». |
| fi | Hän sanoi "moi" ja lähti. | Hän sanoi ”moi” ja lähti. |
Beyond quotes, the same engine rewrites ellipses, apostrophes and spacing – each locale-specific, each cited to a normative source:
| Rule | Locale | Input | Output |
|---|---|---|---|
| Ellipsis | en-US | Wait... what? | Wait… what? |
| Apostrophe | en-US | don't | don’t |
| Spacing (fr) | fr | Attendez... quoi ? | Attendez… quoi ? |
The French row isn’t a typo: before a ?, !, : or ;, French typography calls for a narrow no-break space (U+202F) instead of a regular one – invisible on the page, but exactly the kind of character a naive find-and-replace would never produce and a human reviewer would never catch in a diff.
Dashes are the clearest illustration of what ‘locale-aware’ means in practice – the same raw input resolves to a different correct glyph, different spacing, or is left untouched entirely, depending on locale and context:
| Locale | Input | Output | What changed |
|---|---|---|---|
| en-US | The plan - if there is one - fails. | The plan—if there is one—fails. | em-dash, no spaces (Chicago) |
| en-GB | The plan - if there is one - fails. | The plan – if there is one – fails. | en-dash, spaced (Hart’s Rules) |
| ru | Москва - столица | Москва — столица | em-dash, spaced (Мильчин) |
| en-GB | Takes 5-10 days | Takes 5–10 days | with the opt-in ranges rule: en-dash, plus an invisible U+2060 word joiner on each side so the range can’t break across a line |
| ru | в XV-XVII веках | в XV-XVII веках (hyphen unchanged) | hyphen left alone on purpose (the only edit is a no-break space after в) – normalising it would collide with a genuine em-dash range between Roman numerals |
The same hand-typed hyphen produces three different glyphs with three different spacing rules from three different style guides – and in the last row, the ‘correct’ answer is to change nothing, because acting on it would have been wrong. Try any of these – and the other locales, plus HTML, Markdown and YAML input – live at polytypo.dev.
I wrote polytypo spec-first: the rules exist as versioned prose and JSON Schema-validated locale data in spec/ before any code touches them, and the contribution order is enforced – rule semantics, then locale data with a mandatory citation, then fixtures, then the implementation. Nine rules run in a fixed order (spacing, ellipsis, ranges, dashes, hyphen-binding, quotes, apostrophe, symbols, no-break spaces) across more than 20 locales, each one resolved from an alias table rather than a platform locale library, so en always means en-US and an unrecognized locale throws instead of silently falling back to English.
Every fixture in spec/fixtures/ is a literal in/out pair, mirrored by a CI-generated escaped-\uXXXX copy – because a diff full of invisible U+00A0 no-break spaces is unreviewable by a human otherwise, and the no-break-space rule’s entire job is to be invisible. The same fixture set doubles as an idempotency check: the test runner asserts that running output back through the engine returns it unchanged, so ‘already correct’ text is provably a fixed point, not just assumed to be one.
The spec ships with five conformance-tested implementations – JavaScript/TypeScript on npm, Python on PyPI, PHP on Packagist, Go via pkg.go.dev and Ruby on RubyGems – each validated against the same fixture suite for the modes it supports, so the same text transformed in a Node service, a Python pipeline or a Go binary produces byte-identical output in the modes they share. The runtimes are not identical in scope: PHP has no Markdown mode, and the Python, Go and Ruby Markdown modes handle CommonMark only, not MDX. Each language lives in its own repository (polytypo-js, polytypo-python, polytypo-php, polytypo-go, polytypo-ruby) rather than a monorepo package, so a consumer only ever installs the language they use.
The promo site at polytypo.dev is not a static demo of hand-picked examples – the playground loads the actual published npm bundle and runs the real transform() call on whatever text a visitor pastes in, with the exact API call shown alongside the output. The ‘six correct answers’ comparison on the homepage runs the same input sentence through six locales, so the claim that every one of them is correct is checked by the reader, not taken on faith.
Deploy is gated on provenance, not just on tests passing. pages-deploy.yml takes a workflow_dispatch input naming the exact commit SHA to deploy, then verifies that SHA is either an ancestor of main or tagged site-vX.Y.Z before the build job runs at all – a manually triggered deploy of an unreviewed or off-branch commit is refused outright, on top of GitHub’s own protected-environment approval for github-pages. Nothing merges to production by accident, and nothing deploys without first proving where it came from.
The engine is a set of disjoint substring replacements, nothing else. The parser only locates text – in html mode it walks the DOM and skips code, pre, kbd, samp, var, script, style, textarea and all attributes; in markdown mode it skips code spans, fenced blocks and link destinations; in yaml mode it touches only the values of the keys the caller names. It never rewrites structure, so running polytypo over a document can’t turn a code sample’s straight quotes into curly ones by mistake.
The promo site tests its own restraint. Its test suite (tests/promo/) includes a no-external-requests check and a hostile-output check – assertions that the playground never phones home and that it can’t be coaxed into rendering unsanitized output – alongside a link-depth test and a generated-pages check that catches a stale build before it ships.
Locale claims cite a source, or they don’t exist. Every spec/locales/<code>.json file carries a mandatory sources array pointing at the normative reference for that locale, and a dedicated fixture-citation-guard test fails the build if a locale file is missing one. A style choice in polytypo is traceable to a style guide, not to a contributor’s preference.
| Metric | Value |
|---|---|
| Spec version | Versioned in spec/VERSION; each runtime states the spec version it claims |
| Locales | More than 20, from en-US, de-DE, fr and ru to cs, el, tr and uk – the list is spec/locales/ |
| Rules | 9, fixed order, each independently toggleable (ranges is opt-in, off by default) |
| Implementations shipped | 5 – JavaScript/TypeScript (npm), Python (PyPI), PHP (Packagist), Go (pkg.go.dev), Ruby (RubyGems) |
| Modes | text, HTML (attribute- and code-tag-safe), Markdown (not in PHP), YAML (named keys only) |
| Conformance | Fixture suite per locale, every case doubling as an idempotency check |
| License | MIT for the code; brand assets under separate terms |
Building polytypo end to end – spec, five language runtimes, a public conformance suite and a live promo site with a SHA-gated deploy pipeline – is the same discipline I bring to client work: write down what ‘correct’ means before writing code, make the test suite prove it rather than assert it, and treat the deploy path itself as something that needs a security review, not just a green checkmark.
AvailableNeed something similar?
I build custom solutions – from APIs to full products. Let’s talk about your project.
Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.
Open-source MCP server that lets Claude Desktop, Cursor and other MCP clients validate EU VAT numbers and look up rates directly in chat.
Stack
How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist with GitHub Actions – architecture, versioning, and per-ecosystem gotchas.
polytypo’s typography engine is a nine-rule pipeline that locates and rewrites text without ever building or reprinting an AST.
Stack