Iurii RoguliaIurii Rogulia
AboutAI EngineeringServicesProjectsAI SkillsStackTestimonialsPhrasesBlog
Contact
Iurii ships.

Iurii Rogulia, senior full-stack software engineer. Professionally building software since 2001.

Think of a number
Hourly ratesChecklistsPrivacy PolicyCookie Policy

Business

TMI Iurii Rogulia
VAT ID: FI29845875
DUNS: 368664211
Lappeenranta, Finland 🇫🇮

  1. Home
  2. /
  3. Projects
  4. /
  5. polytypo.dev – Locale-Correct Typography Engine and Spec

Iurii Specifies: polytypo.dev – Locale-Correct Typography Engine and Spec

September 8, 2026

Open-source spec and engine for locale-correct typography: curly quotes, dashes, ellipses and no-break spaces in 20+ locales. Live in-browser playground.

Live Demo Source code

Stack

TypeScriptJavaScriptPythonPHPGoRubyNode.js

Libraries

ajvesbuildmicromarkpagefindparse5Vitest

Services

GitHub ActionsGitHub PagesnpmPackagistPyPIRubyGemsUmami

Topics

Conformance TestingDeveloper Toolsi18nJSON SchemaOpen SourceProperty-Based TestingSupply-Chain SecurityTypography

Key Results

  • A versioned spec (spec/VERSION is the source of truth): 9 ordered rules across more than 20 locales and four modes, each locale claim backed by a cited normative source (Duden, Imprimerie nationale, Kotus, Chicago, Мильчин and others)
  • Five conformance-tested implementations shipped – JavaScript/TypeScript (npm), Python (PyPI), PHP (Packagist), Go (pkg.go.dev) and Ruby (RubyGems) – each passing the conformance suite for the modes it supports
  • Every fixture doubles as an idempotency case: transform(transform(x)) == transform(x), asserted in CI, not just claimed
  • Live playground at polytypo.dev runs the published npm package directly in the browser – no server round-trip, no mocked output
  • Deploy to GitHub Pages requires an exact, already-reviewed commit SHA – a workflow_dispatch input checked against main or a signed release tag before a single byte builds
polytypo.dev – Locale-Correct Typography Engine and Spec

CI

npm

PyPI

Packagist

Go

RubyGems

License

The Problem

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.

Before & After

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:

LocaleInputOutput
en-USShe said "hello" twice.She said “hello” twice.
en-GBShe said "hello" twice.She said ‘hello’ twice.
de-DEEr sagte "hallo" zweimal.Er sagte „hallo“ zweimal.
frIl a dit "bonjour".Il a dit « bonjour ».
ruОн сказал: "это 'моё' дело".Он сказал: «это „моё“ дело».
fiHä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:

RuleLocaleInputOutput
Ellipsisen-USWait... what?Wait… what?
Apostropheen-USdon'tdon’t
Spacing (fr)frAttendez... 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: the same hyphen, five different outcomes

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:

LocaleInputOutputWhat changed
en-USThe plan - if there is one - fails.The plan—if there is one—fails.em-dash, no spaces (Chicago)
en-GBThe plan - if there is one - fails.The plan – if there is one – fails.en-dash, spaced (Hart’s Rules)
ruМосква - столицаМосква — столицаem-dash, spaced (Мильчин)
en-GBTakes 5-10 daysTakes 5⁠–⁠10 dayswith 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.

The Solution

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.

Under the Hood

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.

Results

MetricValue
Spec versionVersioned in spec/VERSION; each runtime states the spec version it claims
LocalesMore than 20, from en-US, de-DE, fr and ru to cs, el, tr and uk – the list is spec/locales/
Rules9, fixed order, each independently toggleable (ranges is opt-in, off by default)
Implementations shipped5 – JavaScript/TypeScript (npm), Python (PyPI), PHP (Packagist), Go (pkg.go.dev), Ruby (RubyGems)
Modestext, HTML (attribute- and code-tag-safe), Markdown (not in PHP), YAML (named keys only)
ConformanceFixture suite per locale, every case doubling as an idempotency check
LicenseMIT 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.

Iurii RoguliaAvailable

Need something similar?

I build custom solutions – from APIs to full products. Let’s talk about your project.

View all projects

Related projects

eu-vat-rates-data – Free & Open-Source EU VAT Rates Dataset
eu-vat-rates-data – Free & Open-Source EU VAT Rates Dataset
February 25, 2026
eu-vat-rates-data – Free & Open-Source EU VAT Rates Dataset

Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.

Stack

TypeScriptPythonPHPGoRubyNode.js
vatnode.dev mcp – Official MCP Server for EU VAT Validation
vatnode.dev mcp – Official MCP Server for EU VAT Validation
May 20, 2026
vatnode.dev mcp – Official MCP Server for EU VAT Validation

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

TypeScriptNode.js

Related posts

Publishing One Package to Five Registries with GitHub Actions
March 27, 2026· 10 min
Publishing One Package to Five Registries with GitHub Actions

How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist with GitHub Actions – architecture, versioning, and per-ecosystem gotchas.

Stack

TypeScriptPythonPHPGoRuby
How a Typography Pipeline Actually Rewrites Text: Order, Spans, and Fixed Points
September 16, 2026· 25 min
How a Typography Pipeline Actually Rewrites Text: Order, Spans, and Fixed Points

polytypo’s typography engine is a nine-rule pipeline that locates and rewrites text without ever building or reprinting an AST.

Stack

TypeScriptNode.js