Ferrocat 3.6, a translation catalog engine for Rust
Your translations, ready to ship.Catalogs that Git can merge and people can trust.
Every sentence an application shows lives in a translation catalog. Ferrocat keeps those catalogs right as the code changes: it updates them, checks what translators wrote, keeps human corrections visible and compiles what your application loads. It reads Gettext PO, ICU messages and FCL, a format with one line per message. For teams whose translations live in the repository.
- 457 of 457conformance checks from polib, pofile, Babel, ICU and FormatJS
- 3 modesGettext PO, ICU PO and FCL, chosen explicitly
- 1 lineper message in FCL, so unrelated edits merge cleanly
- Rust 1.94+MIT or Apache-2.0, library and CLI
Every sentence your app says, in one catalog.
An application speaks in hundreds of small sentences: a button label, a greeting, a line that counts the items in a cart. To show them in German or Japanese, a team collects every sentence from the code into a translation catalog. Translators fill in the other side, and the application looks each sentence up while it runs. The catalog lives next to the code, in the same repository, and changes with it.
The most common catalog is the PO file of GNU gettext. Each entry holds the sentence in the source language, its translation, the place in the code it came from and a note for the translator. Ferrocat reads and writes PO, so the tools your translators use today keep working.
The hard part is not reading the file. It is keeping it right while the code moves on. Developers add sentences, reword others and delete a few. Every change has to reach every language without losing a translation someone already paid for. Ferrocat compares what the code says now with what the catalog holds, adds what is new, keeps what still matches, and sets aside what the code no longer uses, with the date it happened.
msgid ""msgstr """Content-Transfer-Encoding: 8bit\n""Content-Type: text/plain; charset=utf-8\n""Language: de\n""MIME-Version: 1.0\n""X-Generator: ferrocat\n"#: src/Cart.tsx#CartSummarymsgid "{count, plural, one {# item} other {# items}} in your cart"msgstr ""#~ #@ obsolete-since: 2026-11-07#~ #: src/Cart.tsx#CartSummary#~ msgid "Continue shopping"#~ msgstr "Weiter einkaufen"#. Shown after sign-in#: src/Header.tsx#Greetingmsgid "Hello {name}"msgstr "Hallo {name}"#. Primary checkout button#: src/Checkout.tsx#PayButtonmsgctxt "checkout"msgid "Pay now"msgstr "Jetzt bezahlen"
Real output of ferrocat 3.4.2: one message added with an empty translation, two messages kept with their translation, and one message the code no longer uses marked obsolete, with the date.
Translator-friendly files. Git-friendly lines.
Keep PO where translation tools expect it. Choose FCL where machines write the catalog and Git has to merge it.
A PO entry spans several lines: comments, a context, the source, the translation. When two branches both touch a catalog, Git compares those lines, and edits that have nothing to do with each other can still meet in one conflict. Ferrocat Catalog Lines, FCL for short, writes each message on exactly one line and sorts the lines in a fixed order. Unrelated edits land on different lines, and an entry can never be torn apart by a merge.
You choose the format per catalog, and say so explicitly. Classic PO keeps gettext’s own plural rules. ICU PO and FCL carry ICU messages. Converting between ICU PO and FCL keeps every message, with its origin and its notes. Switch the sheet below to see the same German catalog both ways.
%FCL1(tab)source=en(tab)locale=de(tab)order=collated{count, plural, one {# item} other {# items}} in your cart(tab)(tab){count, plural, one {# Artikel} other {# Artikel}} im Warenkorb(tab)r=src/Cart.tsx#CartSummaryHello {name}(tab)(tab)Hallo {name}(tab)r=src/Header.tsx#Greeting(tab)c=Shown after sign-inPay now(tab)checkout(tab)Jetzt bezahlen(tab)r=src/Checkout.tsx#PayButton(tab)c=Primary checkout button
Real output of ferrocat 3.4.2. In FCL each message is one line, and marks a tab between its fields: message, context, translation, then where it comes from (r=) and the note for translators (c=).
Plurals and placeholders, checked.
A translation can read perfectly and still break the screen. Ferrocat compares its structure with the source before your users see it.
“1 items in your cart” is the classic mistake. ICU messages avoid it: a message names its placeholders in braces and lists one wording per plural form, and the application picks the right one at run time. The source below says “1 item” and “3 items” correctly, and the German translation has to keep that structure.
Translators are careful people, and placeholders still get translated, dropped or flattened. The runtime then shows a raw brace, an empty gap or the wrong plural. Ferrocat parses both messages and compares them: every placeholder the source needs, every plural and choice, every formatting style and markup tag.
English source
German translation
No diagnostics. The translation keeps the source’s structure.
Real output of ferrocat 3.4.2. Choose a translation above.
Find the missing words before your users do.
Point the command-line tool at your catalogs. It reports every gap, and its exit status fails the build.
A release audit compares every target language with the source catalog: translations that are missing or empty, entries still marked for review, ICU messages that do not parse or do not match their source. Errors and warnings come with the language, the message and a stable code, in plain text for people and as JSON for your tooling.
Put it in the pipeline that builds your release. A missing translation then stops the merge, not a customer.
msgid ""msgstr """Language: de\n""Content-Type: text/plain; charset=UTF-8\n"msgctxt "checkout"msgid "Pay now"msgstr ""msgctxt "greeting"msgid "Hello {name}"msgstr "Hallo {name}"
ferrocat audit --source-locale en --source docs/examples/source.po --target de=docs/examples/missing.po
- Catalog audit: 2 source messages, 1 target locales, 1 diagnostics (1 errors, 0 warnings, 0 info)
- error catalog.empty_translation [de "checkout" / "Pay now"; de]: Locale `de` has an empty translation.
Exit status 1: the release check fails.
Saved runs of the Rust command-line tool against an English source with the same two messages.
Show the English source and the JSON report
msgid ""msgstr """Language: en\n""Content-Type: text/plain; charset=UTF-8\n"msgctxt "checkout"msgid "Pay now"msgstr "Pay now"msgctxt "greeting"msgid "Hello {name}"msgstr "Hello {name}"
{"diagnostics": [{"code": "catalog.empty_translation","message": "Locale `de` has an empty translation.","name": "de","severity": "error","source_key": {"locale": "de","msgctxt": "checkout","msgid": "Pay now"}}],"summary": {"diagnostics": 1,"errors": 1,"infos": 0,"source_messages": 2,"target_locales": 1,"warnings": 0}}
A human correction leaves a trace.
Machine translation can write the first draft. When a person improves it, the catalog knows.
Many teams let a model translate new strings and ask a person to review the result. The trouble starts on the next run: which values did the machine write, and which did a reviewer fix? Overwrite the wrong one, and a careful correction is gone.
When a tool writes a translation, Ferrocat can store a fingerprint of exactly that value beside it, with the model and its confidence. As long as the text and the fingerprint agree, the value is still machine output. Once a person edits it, they no longer agree, and every later review or translation run can see that a human stepped in. Ferrocat reports it; your policy decides what happens next.
%FCL1(tab)source=en(tab)locale=de(tab)order=collatedPay now(tab)checkout(tab)Jetzt bezahlen(tab)r=src/Checkout.tsx#PayButton(tab)c=Primary checkout button(tab)lock=MJvtg6ldT1tuIn1smwSDEg(tab)ai=example/mt:0.92
%FCL1(tab)source=en(tab)locale=de(tab)order=collatedPay now(tab)checkout(tab)Zahlungspflichtig bestellen(tab)r=src/Checkout.tsx#PayButton(tab)c=Primary checkout button(tab)lock=MJvtg6ldT1tuIn1smwSDEg(tab)ai=example/mt:0.92
- Translations changed since the last state1
- Machine translations still as written0
- Machine translations a person has changed1
Real output of ferrocat 3.4.2, review_catalogs() comparing the two states. The fingerprint (lock=) still belongs to “Jetzt bezahlen”, so the new value is reported as a person’s change, not as machine output.
Compile what the application loads.
One file per language, keyed for fast lookup, with every fallback written down.
An application does not need comments, origins or review marks while it runs. It needs the right text for a key, quickly. Ferrocat compiles the catalogs for one language into exactly that: a map from stable keys to messages, ready to ship as JSON beside your code.
Gaps do not disappear in the process. A message without a translation falls back to the next language you name, and the artifact lists it under missing, so the build can warn or stop.
msgid "Hello {name}"msgstr "Hallo {name}"msgctxt "checkout"msgid "Pay now"msgstr "Jetzt bezahlen"
{"diagnostics": [],"messages": {"JM4sleTdWBE": "Jetzt bezahlen","NfZ_uPJfKww": "{count, plural, one {# item} other {# items}} in your cart","ndf4SYIwaQI": "Hallo {name}"},"missing": [{"key": "NfZ_uPJfKww","requested_locale": "de","resolved_locale": "en","source_key": {"msgctxt": null,"msgid": "{count, plural, one {# item} other {# items}} in your cart"}}],"schema_version": 1}
Real output of ferrocat 3.4.2, with English as the fallback. The cart message has no German translation yet, so it falls back to the English source and is listed under missing.
Catalog work at the speed of your build.
Reading a catalog and adding new strings to it, Ferrocat finished ahead of GNU msgmerge and the gettext libraries we compared for Node.js, PHP and Python.
Catalog work runs more often than people expect: on every extraction, in every pull request that touches a string, before every release, once per language. On a large product, that is many thousands of messages each time. When the catalog step takes seconds, developers start skipping it.
Ferrocat scans the bytes of a catalog directly, and its update moves messages from the old catalog into the new one instead of copying them. Switch the chart between the two everyday jobs.
Time for the same work
Ferrocat = 1×, the line through every row. A bar past it took longer.
The published comparisons also cover pofile, pofile-ts and Babel, and a full update with ICU checks. Every task and its conditions
Show the figures as a table
| Task | Tool | Version measured | Time |
|---|---|---|---|
| Read a catalog | gettext-parser | gettext-parser@9.1.1 | 22.09× |
| Read a catalog | gettext/gettext | gettext/gettext@5.7.3 | 10.00× |
| Read a catalog | polib | polib@1.2.0 | 22.28× |
| Add new strings | GNU msgmerge | msgmerge (GNU gettext-tools) 1.0 | 60.55× |
| Add new strings | polib | polib@1.2.0 | 34.58× |
| Add new strings | gettext/gettext | gettext/gettext@5.7.3 | 16.73× |
Held to the tests of the tools teams know.
PO has quirks that only show up in real catalogs. Ferrocat runs cases taken from the libraries that met them first.
A catalog engine is only as good as its odd cases: escaped quotes, a byte-order mark, obsolete entries, contexts, values spread over several lines. The libraries below have collected such cases for years. Ferrocat adapts their tests, keeps the source of each one, and runs them with its own test suite.
Most cases must parse and match the expected structure. Five must be rejected, because the upstream test expects an error there.
- polib, the Python PO library88 / 88
- pofile, the JavaScript PO library301 / 301
- Babel, Python’s i18n toolkit32 / 32
- ICU, the official MessageFormat tests32 / 32
- FormatJS message syntax4 / 4
Source-attributed conformance fixtures, counted by the repository’s conformance harness. Sources, licenses and scope
A focused engine. Your application around it.
Ferrocat is a translation catalog engine. It reads, merges, and compiles translation catalogs. It leaves the rest of internationalization to the tools around it, and you need none of them to use it.
- Rust applications and tools
- Add the library with
cargo add ferrocatand call it directly. - Release checks in CI
- Install the command line with
cargo install ferrocat-cliand audit on every build. - TypeScript applications
- Palamedes, an i18n toolchain for TypeScript apps, runs on Ferrocat. There is no npm package of Ferrocat itself.
- Not included
- Finding strings in your source code, translating them, or running a translation platform. Those stay with your tools.
gettext taught software to speak in catalogs. Ferrocat carries that model into Git and AI workflows, where merges stay conflict-free and human corrections stay authoritative. Ferrocat continues pofile-ts, its TypeScript predecessor, which stays available as an archived reference. It is one of the Ferramenta engines, made by Sebastian Software in Mainz, Germany.
Put your catalogs in order.
Start with the Rust library, or add the audit to your release pipeline first. Both read the catalogs you already have.
GitHub repositoryDocumentationAPI referenceIntegration and support