I have watched this move go wrong for thirty years in different costumes. In the mid-nineties we shipped multi-language CD-ROMs that a proofreader had signed off in Istanbul and that an Italian distributor rang back about because the Italian string in the main menu read like a joke. The string was fine. The context around it was not. The system had no way to know, and the humans who did know had already left for the next project.
The modern version is a content pipeline. You pay to automate editorial work, get through the first fifty posts delighted, and then quietly find out the pipeline has been shipping something embarrassing in a language the sign-off team does not read. The agency does not tell you because the agency did not notice.
Here are the three editorial rules a daily bilingual WordPress pipeline of ours broke in a hundred and sixty days, and the multilingual publish gates we added after each one. Each is a short Python script you could read over a coffee. If you run a multilingual WordPress estate, these are the questions to put to whoever manages it on your behalf.
A pipeline is only as serious as what it refuses to publish
The pipeline is ours. It runs a daily post in English and Turkish on The WP Clan sites, starting 4 June 2026, and the moratorium file records a hundred and fifty-five posts covered as of writing. Three hundred and ten published artefacts over a hundred and sixty days, plus pages and taxonomy writes. Small by enterprise standards, large enough to break in interesting ways.
The pipeline has one job: produce a piece of editorial work in two languages and publish it live to two WordPress sites. It does that well enough on a good day. The harder half is to refuse to publish on a bad day. That refusal is the publish gate. We broke three different rules in those hundred and sixty days, and each fix was a scanner that exits non-zero when the rule is violated. Three scripts, around a hundred and seventy lines of Python between them, and a small change to the publish script. That is the surface area of the entire problem.
The register scanner exists because a crude Turkish idiom made it to a live post
The first rule we broke was register. Turkish has a family of idioms that read as warm and folksy in conversation and as crass in print. They are the sort of thing you say to a cousin at a wedding. They are not the sort of thing you put on a page that an enterprise reader is going to read with their arms folded. On a day I would rather not remember, one of them ended up in a published post on the Turkish site under our name.
My reaction was blunt. The scanner went in the next day, 14 September 2026, and the header of the file still records why: “added 2026-09-14 after a crude Turkish idiom reached a live post.” It is thirty-odd whole-word patterns in Turkish and English, case-insensitive, scanned against the plain text of every HTML file about to go up. It exits one on any hit, which blocks the publish step.
The interesting part was writing it. The cost was not deciding which words to flag. That was a short conversation. The cost was realising that in Turkish, folded through Python’s case-insensitive matching, the ordinary adverb sık (meaning “frequent”) collides with a crude word differing only by a dotted and undotted i. You cannot add the crude word to a list without half the Turkish language reading as a violation. The scanner deliberately omits the bare form and relies on fuller patterns that do not collide. The English equivalents had the same issue in a smaller way: “dumb pipe” is standard technical vocabulary, so the bare word is not on the list; “garbage” is legitimate in accessibility copy, so it is not on the list either.
That is the buyer lesson nobody will tell you up front: a register scanner in a language you do not personally speak is not a word list, it is a word list carefully cut to the grammar of that language. If someone pitches you one in forty-eight hours, ask which collisions they handled. A blank look means a scanner that either misses hits or flags ordinary writing. Both are expensive.
The title case scanner exists because thirteen posts shipped in sentence case
The second rule we broke was house style. On 13 September 2026 we rewrote the editor’s voice file as part of a wider editorial reset. One of the paragraphs that got lost in that rewrite was the house rule on title case. For thirteen consecutive days every post went live with a sentence-case title, in both languages, and nobody noticed. The previous hundred posts had honoured the rule without complaint, and the writer had quietly assumed that rules written on 4 June would still be written on 28 September.
They were not. The scanner that catches this, added 28 September 2026 with the rule restored on the same day, is a round-trip test. For every post dated on or after the reset, the title is passed through a deterministic title-case function. If the stored title does not match the function’s output, the post is blocked. The function handles English AP rules (short conjunctions and prepositions of three letters or fewer stay lowercase, the first and last word and anything after a colon are capitalised) and Turkish case rules (every word capitalised, with the dotted-i distinction preserved, so i becomes İ and ı becomes I). Names that are lowercase by their own convention, like wp-cli and theme.json, are left alone.
The buyer lesson is simple and unpleasant. If your house rule lives only in a documentation file, it will be lost in the next refactor of that file, and the next thirteen pieces of work will all be wrong before anyone looks up. The fix is a second surface behind the rule. We have two now: the paragraph in the config file and the script that enforces it. One cannot quietly delete the other without the pipeline refusing to run.
The em dash scanner exists because the em dash gives the game away
The third rule we broke twice, in opposite directions. On 27 September 2026 we added a rule banning the em dash outright, in any language, on any surface. The reasoning was not aesthetic. The em dash has become the single most recognisable tell of machine-written prose, and a reader who spots one stops following the argument and starts assessing whether a person wrote it. That cost is higher than the punctuation is worth.
What happened next is written into the voice file: “This section was written on 09-27 and lost the same day in a git checkout during the cleanup. Restored 2026-09-28. The scanner had been enforcing a rule with nothing written behind it in between.” For twenty-four hours, the scanner refused em dashes for a reason that existed nowhere in prose a human could read. The next writer along would reasonably have assumed the scanner was buggy and disabled it. The rule survived because the scanner did; it survived better once the paragraph came back.
The scanner itself flags two things: the em dash character, and the spaced hyphen in prose, which is how the em dash smuggles itself back in when a writer edits around the rule. It deliberately does not flag anything inside a <pre> or <code> block, because inside code, calc(100vw – 160px) is real code and the dash is data, not prose. The en dash is left alone because the en dash is the correct typography for numeric and date ranges, as in 1,400 to 2,200 words. Rules like these do not live in a word list; they live in roughly sixty lines of Python with the right regular expressions.
What a scanner cannot catch, and what to do about it
Three scanners for three rules we broke. There is a fourth incident the scanners did not catch, and it is worth putting on the table, because it is the shape of the next class of failure.
On 14 October 2026, a category pass on both the English and Turkish sites ran wp post term set <id> category 1 129 130 through wp-cli without the --by=id flag. The flag is not optional. Without it, wp-cli takes 1, 129 and 130 as category names, not ids, does not find categories with those names, and quietly creates them. We ended that morning with three new top-level categories on each site, literally named “1”, “129” and “130”, and a round of clean-up that was fun for nobody. The permanent fix moved the category pass inside the publish script and pinned --by=id, and the CLAUDE file now carries the same warning in red for any future hand-pass.
The reason this one did not get caught is that our scanners all look at prose. A scanner on HTML cannot see a stray category. The first class of bug lives in the thing you ship, and the gate is a scanner. The second class lives in the infrastructure you ship with, and the gate is a deployment script that never falls back to the default behaviour of the tools it uses. Both classes exist on any WordPress estate at scale. Build one kind of gate and you will always be cleaning up the other kind of mess.
What this build actually looks like on a WordPress multisite that is not ours
If you run a multilingual WordPress estate and you want to put this pattern in place, here is the shape of the work. None of it is novel. All of it is cheap. The expensive part is agreeing which rules you want behind the gate, not writing the gate.
- A register scanner per language, with whole-word patterns and lookbehinds for the collisions that language’s grammar creates. In our case, around a page of Python for Turkish and English combined, plus one in-code note per omitted word explaining why it is omitted. Future languages add their own word list to the same file.
- A punctuation scanner for anything whose presence itself is an editorial tell. The em dash is the obvious one. Depending on the house voice, you may also want to flag non-breaking spaces where they should not be, smart quotes in code blocks, and the ellipsis character used in prose where three dots are wanted.
- A title case scanner that round-trips through a deterministic function and only enforces on posts after the date the rule was agreed. Enforcing retroactively over an existing archive is the quickest way to get the scanner turned off by whoever has to clean up ten thousand pre-reset titles.
- A publish script that never passes anything to wp-cli or the REST API without being explicit about which field is being set and whether it is a slug, an id or a name. No defaults, no fallbacks. The three-line fix we added to our own publish script is specifically the
--by=idswitch on the category pass, and every other place where wp-cli will accept either an id or a name has the same decision made in writing.
Data the gates touch, plainly: the register and dashes scanners look at HTML on disk, the titles scanner at a JSON file with slug, title and excerpt per language. None touch customer data, payment records or WooCommerce order tables. The moment a publish gate needs access to customer PII, a different conversation about scope and security has to happen. Keep editorial gates on editorial surfaces, and let commerce gates sit in the order pipeline where they belong.
Effort, honestly: each scanner was roughly a day to write, including the collision analysis for the language, plus a day across the three to wire them into the publish hook. A WordPress multisite of two or three languages with a single editorial team is about a week’s work, including the meeting where you agree the rules. A fifteen-language estate with regional teams is four to six weeks, because the hard conversation about whose register definition wins happens fifteen times instead of twice.
What we would do if you handed us a multilingual estate without these gates
I would start with an inventory. Not of plugins, themes or servers. An inventory of the editorial rules that currently live only in the heads of three or four people who have been there a while, and that the pipeline honours by accident. House style, banned words per market, title case, punctuation, taxonomy conventions, slug format, hreflang pairings. Whatever comes back, turn each line into a scanner that can refuse to publish, and wire the scanners into the deploy. Leave the organisation with a shorter stack of things that only one person knows how to not break, and a script someone else can read in an afternoon. That is the move. It does not make a conference keynote, but the pipeline that ships after it is one you can hand over without losing sleep.
Last modified: November 11, 2026