Dev & EngARTICLE

PostgreSQL 19 fixes Chinese error messages stalled for seven years

Ruohang Feng rewrote PostgreSQL's Chinese localization from scratch, revealing translation errors that had confused production debugging for decades.

PostgreSQL 19 fixes Chinese error messages stalled for seven years
Image: Roberto Diniz

PostgreSQL has a localization system (NLS, based on gettext) as mature as any other part of the database: every string the server emits goes through a message catalog (.po/.mo) maintained by volunteers, language by language. The project only ships a language in a release if that language's catalogs hit 80% coverage. It's an objective bar, published at babel.postgresql.org, and as of July this year it was set to leave six languages out of PostgreSQL 19: Czech, Greek, Italian, Brazilian Portuguese, Simplified Chinese, and Traditional Chinese.

The Simplified Chinese case is what Ruohang Feng, founder of Pigsty and a longtime translator of the PostgreSQL documentation, describes in detail on Planet PostgreSQL. And the problem wasn't just that 66% coverage fell below the cutoff. It was what that number was hiding.

What Was Actually Broken

When gettext can't find a translation for a string, it falls back to English. In practice, this produces bilingual errors: the first half of the message in Chinese, the second in English, the same concept spelled two different ways on different screens of the same tool. The postgres catalog, which concentrates 6,826 strings (more than half of all message text in the codebase and the source of almost every ERROR that appears in the log), stood at 61%. libpq, the base of practically every PostgreSQL driver, at 12%.

The header of the main catalog's translation file hadn't changed since PostgreSQL 12, released in 2019 and out of support since 2024. Seven years and three months, eight major versions, without a single line changed. Everything that went into PostgreSQL in that span (logical replication, JIT, parallel execution, asynchronous I/O) simply had no Chinese translation at all. About 2,660 server-side strings had never been translated, and another thousand, translated years earlier, no longer matched the original English text because the source code had changed underneath them.

Worse than the absence of translation is the wrong translation. Feng documents cases like Report bugs to, translated literally as "report the cockroaches/bugs to," because the Chinese term used for "bug" since 2001 is the insect, not the software defect. Fourteen catalogs carry this error. And the most serious from an operational standpoint: out of memory had four different translations coexisting in the tree, and one of them, used in part of the code, means "buffer overflow" (memory overrun, writing past the end of a buffer), not "memory exhausted." These are two completely distinct root causes: one tells the operator to add RAM, review connection counts, and adjust work_mem; the other tells them to review code looking for memory corruption. An error message that points in the wrong direction during an incident isn't just a translation failure, it's active noise in the debugging process.

The Format String Bug Hidden in the Translation

Beyond the semantic errors, Feng found two concrete technical bugs in the catalogs. In pg_ctl, the message invalid binary "%s": %m lost the entire %m in the Chinese version: the operator learned which file was wrong, but never learned what the system's errno had to say, precisely the most valuable part of the message for diagnosing the problem.

The ecpg case is more serious. The original English string (msgid) has exactly one %s. The Chinese translation (msgstr) used two positional specifiers, %1$s and %2$s. Under Chinese locale, this makes the internal printf try to read a second argument that the call never passed, a classic undefined behavior from a malformed format string. It's the kind of bug that, in a different context, would be treated as a security vulnerability; here, it survived inside a translation file for who knows how many years.

How the Work Was Redone

Feng volunteered on the pgsql-translators list on September 11 to take over the Simplified Chinese catalogs from scratch, rather than applying spot patches. The stated reason: most inconsistencies are between files, and a single glossary solves what a pile of small patches doesn't.

The method combined machine translation (he cites the models Fable 5.1, Cloud Fable 5.1, and Codex Astra 6) with a glossary built from years of professional translation of the PostgreSQL documentation, of "Designing Data-Intensive Applications," and of "PostgreSQL Internals." Every technical term (including modals like "cannot," "shall not," and "must not") had exactly one Chinese form fixed before any mass translation began; after that, cross-consistency checks between components and between versions, with a clean msgfmt --check --check-format on every file before going to the list.

The result: 28 catalogs, six branches (PostgreSQL 14 through 19), 67,487 strings, 100% coverage, zero vague or empty entries. Peter Eisentraut merged it into the official translations repository on September 18, in time to make it into PostgreSQL 19.

What This Changes for Those Running Databases in Production

The case is about Chinese, but the yardstick applies to any multilingual stack running PostgreSQL, MySQL, or any system that uses gettext or an equivalent i18n mechanism for error messages. Three practical takeaways come out of this episode:

  • Translation coverage isn't cosmetic, it's operational data. A catalog at 66% isn't "a bit rough," it's a mix of two languages inside the same log, which degrades incident triage time. If your team runs a non-English locale in production, it's worth checking the language's actual coverage at babel.postgresql.org before trusting the messages.
  • A poorly done translation can invert the root cause. The out of memory versus "overflow" case is the most direct example: the error message is the first signal the operator sees, and if it points to the wrong diagnosis, incident response time goes up, not down.
  • A format string in a translation is a real bug surface, not just a style issue. The ecpg case shows that a translation with a different number of positional arguments than the original msgid goes unnoticed until someone runs under that specific locale. Any project that accepts translation contributions should run msgfmt --check-format (or equivalent) as a CI gate, not as an occasional manual check.

There's also a relevant side note for anyone evaluating "national" database distributions: Feng verified that several PostgreSQL distributions sold in China, with Chinese localization as a selling point, shipped the same incorrect upstream .po file, byte for byte, including the same "bug-as-insect" and "overflow" errors. None of those vendors appeared among the names who historically maintained this catalog. It's a message about the difference between packaging an open-source project and actually maintaining the parts of it that the product itself advertises as an advantage.

What Remains Open

PostgreSQL 19 hadn't been frozen yet when the article was published, and Feng himself notes that coverage had already dropped from 100% to 99% because of new strings entering the postgres catalog in the final days before the freeze; he maintains the pgsty/pgnls project to track these changes daily and promised one final pass to 100% after the freeze. In other words: translation coverage in a living project isn't a permanent state, it's something that needs continuous maintenance every release cycle, exactly the kind of work that went seven years without an owner. The warning also applies to anyone managing i18n in any internal software: AI automation has cut the cost of mass translation by an order of magnitude, but it doesn't eliminate the need for someone to review, with a fixed glossary, every string that comes in afterward.

Translated from the Brazilian Portuguese original · Read the original