Your homepage is English. Your menu is French. Your product page is half Spanish because a widget decided to freestyle. That’s not “multilingual”; that’s a content blender.
When Polylang or WPML “mixes languages,” it’s rarely random. It’s a predictable failure mode: wrong language context, wrong object IDs, wrong cache keys, or a migration that quietly left translation relationships in a ditch. Let’s treat it like an incident: reproduce, measure, isolate, and apply fixes that survive the next plugin update.
What “language mixing” really is (and isn’t)
People describe a lot of different bugs as “WPML is mixing languages” or “Polylang is broken.” Under the hood, there are a few distinct classes of problems:
- Context bugs: WordPress renders the page in Language A, but a widget/query runs in Language B because it didn’t inherit the current language or it overrides it.
- Relationship bugs: The site has translations, but the links between originals and translations are missing or wrong. Then the plugin can’t find the right counterpart and falls back.
- Taxonomy/menu bugs: Posts are translated, but categories/tags/menus aren’t translated or aren’t assigned correctly. The UI shows “correct” pages, while navigation pulls “whatever exists.”
- Cache bugs: Your cache stores HTML for Language A and serves it to Language B because the cache key doesn’t vary by language, cookie, URL, or header.
- Rewrite/canonical bugs: URLs resolve, but the language detection chooses the wrong language, or redirects rewrite language prefixes inconsistently.
- String translation bugs: Theme strings, widget titles, or ACF labels are pulled from the wrong domain or wrong language table.
Also: sometimes it’s not the translation plugin. A theme with “clever” custom queries and hard-coded IDs can produce perfect chaos even on a monolingual site. Add multilingual and it goes from “quirk” to “incident.”
One operational truth: mixed-language output is usually deterministic. If you can reproduce it with the same URL + same cookies + same cache state, you can fix it. If you can’t reproduce it, it’s often cache variance or a race between multiple caches.
How WPML and Polylang store language and translations
WPML: translation groups, language codes, and “element IDs”
WPML typically tracks translation relationships in a table that groups “elements” (posts, terms, sometimes strings) into translation groups. Each element has a language code and a translation group ID. If that mapping is wrong or incomplete, WPML has to guess. Guessing is how you get English posts with German categories and a language switcher that goes nowhere.
Polylang: language taxonomy + post meta + relationships
Polylang uses a language taxonomy (terms representing languages) and stores relationships between translated posts. It’s simple in concept, but it inherits all the usual WordPress sharp edges: term caching, object caching, and custom queries that don’t respect taxonomies.
Why both can mix languages even when “settings look right”
Because the settings page is only the control plane. The data plane is your database, cache layers, rewrite rules, and whatever your theme and plugins do in queries. In production, the bug is usually one of:
- Translation links missing (data integrity)
- Wrong language detected (routing)
- Cache key missing language (caching)
- Custom query ignores language filters (application logic)
Interesting facts and historical context (why this is hard)
- WordPress wasn’t born multilingual. For years, multilingual meant “install separate sites” or “hack it with plugins.” The core still assumes one locale per request.
- WPML popularized “translation groups” early. It’s a workable abstraction, but it requires consistent mapping across post types, terms, and meta.
- Polylang leaned into WordPress taxonomies. Clever: it reuses native term mechanics, but also inherits term cache pitfalls.
- Object caching changed the game. Redis/Memcached made WordPress fast, then made multilingual weird because many themes/plugins never varied cache keys by language.
- WooCommerce made it harder. Products aren’t just posts; they’re posts plus variations, attributes, and lookup tables. Language mismatch can cascade into wrong prices, not just wrong text.
- “Language in URL” became an SEO norm. Prefixes (/fr/), subdomains, and parameter-based languages each have different cache and rewrite behavior. Some are easier to reason about; none are free.
- String translation is a second system. Post content translation and UI string translation use different storage and lookup paths, so it’s common to “fix posts” while headers and widgets remain wrong.
- Canonical and hreflang rules evolved. Search engines got stricter about language/region markup. A plugin update that “fixes SEO” can break routing assumptions and expose latent bugs.
- Modern builders (Elementor, WPBakery) add another layer. They store content in serialized blobs; translation plugins integrate, but edge cases around global widgets and templates are still common.
Fast diagnosis playbook
When languages mix, don’t start by toggling random settings. Start like an SRE: identify the layer that’s lying.
First: prove whether it’s cache
- Disable page cache temporarily (or bypass it) and retest the same URL in two languages.
- Check whether the HTML differs when you vary language cookie, language URL prefix, or Accept-Language header.
- If it changes when cache is bypassed: your caches are mis-keyed or not purged per language.
Second: validate language detection and routing
- Confirm the plugin thinks the current request is Language A.
- Verify permalinks/rewrite rules are consistent and not duplicated by multiple plugins.
- Ensure you don’t have both WPML and Polylang active (yes, people do this).
Third: check translation relationships and taxonomy translation
- Pick one broken post. Confirm its translation exists and is linked.
- Check categories/tags/menus for translation equivalents.
- Verify theme queries are language-aware and not hardcoding IDs.
Fourth: look for “helpful” optimizations
- Object cache drop-ins (Redis), persistent caching plugins, full-page CDN caching, and fragment caching.
- Theme-level caches (transients, options storing rendered HTML, page builder template caches).
Decision rule: if the same URL serves different languages without any URL/cookie/header change, it’s almost always cache contamination or shared fragments.
Practical tasks: commands, outputs, decisions (12+)
These tasks assume you have shell access and WP-CLI. If you don’t, you can still apply the logic via your hosting panel—just with more clicking and less truth.
Task 1: Confirm which multilingual plugin is active (and only one)
cr0x@server:~$ wp plugin list --status=active
+-----------------------+--------+-----------+---------+
| name | status | update | version |
+-----------------------+--------+-----------+---------+
| wpml-multilingual-cms | active | none | 4.6.11 |
| wpml-string-translation | active | available | 3.2.7 |
| redis-cache | active | none | 2.5.4 |
+-----------------------+--------+-----------+---------+
What the output means: WPML is active, plus Redis object cache. No Polylang. Good.
Decision: If you see both WPML and Polylang active, stop. Deactivate one before debugging. Two language context providers equals chaos.
Task 2: Verify WP-CLI can see the site URL and environment
cr0x@server:~$ wp option get siteurl
https://example.com
What it means: You’re operating on the expected site (not a stale staging DB).
Decision: If the URL is wrong, fix that first. Mispointed WP-CLI runs are how “debugging” becomes “data loss.”
Task 3: Check permalink structure (rewrite layer)
cr0x@server:~$ wp option get permalink_structure
/%category%/%postname%/
What it means: Pretty permalinks enabled. Language plugins will hook into this.
Decision: If permalinks are empty or inconsistent across environments, flush rewrites after confirming language settings.
Task 4: Flush rewrite rules safely
cr0x@server:~$ wp rewrite flush --hard
Success: Rewrite rules flushed.
What it means: Regenerated rewrite rules in the database and .htaccess (if used).
Decision: If language URLs were 404ing or redirecting incorrectly, retest now. If nothing changes, routing isn’t your primary problem.
Task 5: Identify whether a full-page cache is present (headers)
cr0x@server:~$ curl -sI https://example.com/fr/ | egrep -i 'cache|vary|set-cookie|x-cache|cf-cache|age'
cache-control: max-age=600
vary: Accept-Encoding
x-cache: HIT
age: 534
What it means: There’s a cache (x-cache: HIT) and it does not vary by language cookie or URL beyond /fr/ itself.
Decision: If you use cookie-based language detection and the cache doesn’t vary by cookie, you will serve the wrong language. Fix caching before touching WPML/Polylang settings.
Task 6: Compare the same page in two languages and look for identical cache keys
cr0x@server:~$ curl -sI https://example.com/ | egrep -i 'x-cache|age|cache-control'
x-cache: HIT
age: 410
cr0x@server:~$ curl -sI https://example.com/fr/ | egrep -i 'x-cache|age|cache-control'
x-cache: HIT
age: 410
What it means: Identical age suggests the cache might be collapsing variants (depends on stack, but it’s suspicious).
Decision: If you see suspiciously identical cache behavior across languages, bypass cache and verify content. Then fix cache keying.
Task 7: Bypass cache and test language correctness (origin truth)
cr0x@server:~$ curl -sH 'Cache-Control: no-cache' https://example.com/fr/ | grep -i '
What it means: Origin renders French correctly (at least the HTML lang attribute).
Decision: If origin is correct but cache is wrong, stop debugging WPML/Polylang data. Fix the caching layer first.
Task 8: Verify object cache is enabled (the silent saboteur)
cr0x@server:~$ wp redis status
Status: Connected
Client: PhpRedis (v5.3.7)
Database: 0
Prefix: wp_cache:
What it means: Persistent object cache is on. Great for speed; dangerous if code doesn’t vary by language context.
Decision: If multilingual issues appeared “after enabling Redis,” suspect cached query results or fragments missing language keys.
Task 9: Clear object cache to see if the bug disappears
cr0x@server:~$ wp cache flush
Success: The cache was flushed.
What it means: Any cached objects/queries/fragments are gone.
Decision: If the problem vanishes after a flush and returns later, you have a cache keying problem. Don’t schedule hourly flushes and call it “fixed.”
Task 10: Inspect WPML language settings from the database (sanity check)
cr0x@server:~$ wp option get icl_sitepress_settings --format=json | head
{"icl_lang_sel_type":"dropdown","language_negotiation_type":1,"urls":{"directory_for_default_language":0}}
What it means: WPML negotiation type 1 commonly indicates “language per directory.” Settings exist and are readable.
Decision: If negotiation type is “cookie” or “parameter,” ensure every cache layer varies accordingly. If it’s “directory,” make sure rewrites and canonical redirects preserve it.
Task 11: Check whether a post has translations linked (WPML)
cr0x@server:~$ wp db query "SELECT element_id, element_type, trid, language_code, source_language_code FROM wp_icl_translations WHERE element_id=123;"
+------------+--------------+------+---------------+----------------------+
| element_id | element_type | trid | language_code | source_language_code |
+------------+--------------+------+---------------+----------------------+
| 123 | post_post | 9012 | en | NULL |
+------------+--------------+------+---------------+----------------------+
What it means: Post 123 exists in English, translation group 9012. But we don’t yet see its French sibling.
Decision: Query for the trid. If no other rows exist, translations aren’t linked (or don’t exist). The language switcher may fall back to the wrong thing.
Task 12: Find all translations in that group (WPML)
cr0x@server:~$ wp db query "SELECT element_id, language_code FROM wp_icl_translations WHERE trid=9012;"
+------------+---------------+
| element_id | language_code |
+------------+---------------+
| 123 | en |
| 456 | fr |
+------------+---------------+
What it means: Translation exists (French post ID 456) and is linked.
Decision: If the French page still displays English fragments, it’s likely cache, theme queries, or untranslated taxonomies/strings rather than missing translation linkage.
Task 13: Confirm taxonomy terms are translated and linked (WPML terms)
cr0x@server:~$ wp db query "SELECT element_id, element_type, trid, language_code FROM wp_icl_translations WHERE element_type LIKE 'tax_%' AND element_id=77;"
+------------+---------------------+------+---------------+
| element_id | element_type | trid | language_code |
+------------+---------------------+------+---------------+
| 77 | tax_category | 3001 | en |
+------------+---------------------+------+---------------+
What it means: Category term 77 is only registered for English in WPML’s translation mapping.
Decision: If menus/categories are mixing languages, missing term translations are a primary suspect. Add/attach translated terms or enable term translation for that taxonomy.
Task 14: Detect a theme/plugin custom query that ignores language filters
cr0x@server:~$ wp eval 'global $wp_query; echo "lang=".(defined("ICL_LANGUAGE_CODE")?ICL_LANGUAGE_CODE:"none")."\n";'
lang=fr
What it means: WPML context is French in this request environment (useful when running via web context; in CLI it depends on bootstrap).
Decision: If WPML says “fr” but your widget shows English posts, the widget likely runs a query that bypasses WPML filters (e.g., suppress_filters=true) or uses direct SQL.
Task 15: Find suspicious query patterns in your codebase (suppress_filters)
cr0x@server:~$ grep -R --line-number "suppress_filters" /var/www/html/wp-content | head
/var/www/html/wp-content/themes/acme/functions.php:812: 'suppress_filters' => true,
/var/www/html/wp-content/plugins/acme-widgets/widget-latest.php:55: $q = new WP_Query(['suppress_filters'=>true,'post_type'=>'post']);
What it means: Code explicitly disables filters. WPML/Polylang hooks are filters. That’s your smoking gun.
Decision: Remove suppress_filters, or add language constraints explicitly in a plugin-approved way. If you need suppress_filters for performance, you must manually handle language scoping and cache keys.
Task 16: Verify cron isn’t “helpfully” regenerating caches in the wrong language
cr0x@server:~$ wp cron event list | egrep -i 'wpml|polylang|cache|preload' | head
wpml_cache_clear 2025-12-27 03:10:00 +0000
rocket_preload_cache 2025-12-27 03:15:00 +0000
What it means: A cache preloader exists. If it preloads only default language URLs, it can overwrite shared cache entries.
Decision: Make preload language-aware (preload all language URL variants) or fix cache keying so variants can’t overwrite each other.
Joke #1: A multilingual site without language-aware caching is like a bilingual receptionist who answers every call in whichever language they learned last.
The big failure modes: why languages mix
1) Cache key doesn’t include language (full-page cache)
This is the most common root cause in real life because it shows up as “random.” It’s not random; it’s whichever language warmed the cache first.
Typical triggers:
- Switching from “language in URL” to “language by cookie” without updating cache configuration.
- Adding a CDN or reverse proxy that ignores cookies or strips query parameters.
- Using a caching plugin that optimizes for single-language sites.
Fix: vary cache by language. In practice that means: distinct URL per language (preferable), or include the language cookie in the cache key, or bypass cache when language cookie is present.
2) Object cache and fragment caches miss language scoping
Full-page caches aren’t the only culprit. A persistent object cache can serve cached query results across languages if the cache key doesn’t incorporate language. WPML/Polylang try to integrate, but they can’t fix custom transients or theme caches you wrote in 2017 and forgot about.
Where it happens:
- Transients storing rendered HTML for widgets
- Theme options storing “cached menu HTML”
- Custom “get_posts_cached()” helpers
Fix: include language code in every transient key. If you can’t, delete the optimization and move on with your life.
3) Custom queries that disable filters
If you see suppress_filters=true, assume multilingual is broken until proven otherwise. WPML and Polylang rely on filters to adjust queries.
Fix: remove suppress_filters, or add explicit language constraints using the plugin’s APIs, or use their “current language” context helpers to scope queries.
4) Taxonomy translations missing or mis-linked
This is how you get a French page that lists English categories, or a language switcher that keeps the same category slug but changes the page language. Terms have their own translation mapping. If the mapping isn’t there, plugins fall back.
Fix: translate terms, assign correct translated terms to translated posts, and ensure menus are built per language.
5) Menu and widget configuration isn’t language-specific
Menus are basically configuration. In a multilingual site, configuration needs scoping too. WPML can sync menus; Polylang can assign menus per language. But if you have one menu with hard-coded links, you’ve built a content-mixing machine.
Fix: separate menus per language or use plugin-supported sync tools, and avoid hard-coded absolute links to default language.
6) “Default language” directory setting causes canonical confusion
A classic WPML setup is “directory for default language: off,” meaning the default language lives at / while others live at /fr/, /de/, etc. This works, but it increases chances that caches and canonical redirects flatten paths and mis-detect language.
Fix: if you can afford it, put default language in a directory too. It’s operationally boring, which is the nicest thing you can say about URL design.
7) Mixed-language content inside a single post (human workflow)
Sometimes the plugin is fine and the problem is editorial: translators copy/paste blocks, builders reuse global templates, or someone edited the wrong language version because the admin bar language switcher lied.
Fix: lock workflows: roles, capabilities, and an explicit translation pipeline (even if it’s “always duplicate then translate, never edit original”).
Common mistakes: symptom → root cause → fix
1) Symptom: Homepage occasionally flips language without changing URL
Root cause: full-page cache not varying by language cookie or header.
Fix: use language-in-URL negotiation, or configure cache to vary by language cookie; purge cache after changing negotiation.
2) Symptom: Menus show items in the wrong language, but page content is correct
Root cause: menu assigned globally, not per language; or cached menu HTML not keyed by language.
Fix: assign menus per language (WPML/Polylang feature), and remove any menu fragment cache or add language to its key.
3) Symptom: Category archive shows posts from multiple languages
Root cause: custom query uses suppress_filters; taxonomy translation mapping missing; or “display all languages” setting enabled for taxonomy.
Fix: remove suppress_filters; translate and link terms; ensure the taxonomy is set to “translatable” and filtered by current language.
4) Symptom: Language switcher points to the homepage for some pages
Root cause: translation relationship missing for that content type (post/term/product), or translation exists but is not linked in the translation group.
Fix: rebuild translation links (duplicate and connect properly), run plugin’s “troubleshooting” tools, and verify DB mapping for the affected items.
5) Symptom: WooCommerce cart/checkout shows mixed language strings
Root cause: strings coming from theme/plugin domains not registered for translation, or cached fragments (mini-cart) stored without language key.
Fix: register strings correctly, clear caches, and ensure cart fragments vary by language cookie/URL.
6) Symptom: Admin editing language is “wrong,” leading to edits in the wrong translation
Root cause: user profile language vs site language vs WPML admin language settings confused; editors using quick edit without checking language.
Fix: standardize admin language behavior, train editors to check language indicators, and restrict who can edit originals.
7) Symptom: Hreflang tags are wrong or missing intermittently
Root cause: cached head section shared across languages; or canonical redirect stripping language directory; or inconsistent permalink settings.
Fix: fix cache keying for head fragments; validate canonical behavior; ensure consistent URL format for all languages.
8) Symptom: After migration, translations exist but are “unlinked”
Root cause: DB migration/import didn’t preserve plugin tables or post meta; or staging-to-prod sync missed translation tables.
Fix: re-run migration including WPML/Polylang tables; avoid partial exports; verify translation tables row counts match source before cutover.
Joke #2: “We’ll just copy the database and see what happens” is the multilingual equivalent of “I’ll just restart it” — sometimes it works, but it’s not a strategy.
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption
They had a tidy WordPress setup: WPML, three languages, a caching plugin, and a CDN. The marketing team complained that French visitors were seeing English headlines “sometimes.” Engineering did the classic thing: blamed WPML and opened a ticket with the plugin vendor.
The wrong assumption was simple: “language is determined by URL, so caching is safe.” In reality, the default language was served at /, and the site used a cookie to remember the last selected language for some users. The CDN ignored cookies by design (performance!) and cached / as a single object.
That meant whichever language hit / first after a purge “won” the cache. French users selecting French would trigger French HTML on /, the CDN would store it, and now English users would receive French until the next purge. The incident was intermittent, infuriating, and completely deterministic.
The fix wasn’t a WPML setting. They changed language negotiation to directory-based for all languages including the default (so English became /en/), updated redirects, purged caches, and the problem stopped. They also added a synthetic check that fetched each language homepage and asserted the <html lang> matched. Boring, effective.
Mini-story 2: The optimization that backfired
A product team wanted faster category pages. Someone added a “performance improvement”: cache the rendered “Top Products” widget in a transient for 30 minutes because it did a heavy query. Great idea on a single-language site.
On this site, products were translated per language, and prices/availability differed by market. The cached widget HTML was stored under a key like top_products_widget without any language. So the first request to warm that cache (often English, due to internal staff browsing) determined what everyone saw for the next 30 minutes.
It got worse: the widget included links to products in the warmed language. So French category pages would show English product names, English URLs, and sometimes English currency formatting. Not catastrophic, but it eroded trust and increased support contacts. The team tried “flush cache on publish,” which just made it flap more often.
The eventual fix was: delete the HTML caching and instead cache the underlying IDs per language (and per category), then render language-correct titles/URLs at request time. It used slightly more CPU but restored correctness. In production systems, correctness is performance. Users don’t buy products they don’t understand.
Mini-story 3: The boring but correct practice that saved the day
A different company ran Polylang with WooCommerce and Redis. They’d been burned before, so they did something painfully unsexy: every release had a multilingual smoke test checklist. Not automated at first; just disciplined.
The checklist included: open the homepage, one product page, one category archive, the cart, and checkout in each language; verify menu language; verify currency and locale formatting; verify language switcher links; verify hreflang presence. They also had a rule: if you add caching, you must document the cache key and what varies it.
One day a developer upgraded a theme that introduced a new “global header cache” option. It improved Time To First Byte and made the dashboards look pretty. The smoke tests caught that the header language was wrong on non-default languages right after deployment.
They rolled back within minutes, then re-deployed with the header cache disabled until they could patch it to vary by language. No drama, no customer impact worth mentioning, no “mystery bug” ticket. The boring practice did exactly what it says on the tin: it saved the day.
Checklists / step-by-step plan
Step-by-step plan for fixing mixed-language output (the sane order)
- Pick one reproducible URL that shows mixed language. Write it down. Don’t chase ten bugs at once.
- Bypass full-page cache and compare output. If bypass fixes it, you’re in cache-land.
- Flush object cache and retest. If that fixes it temporarily, you have language-unaware object caching or fragment caching.
- Confirm language negotiation method (URL directory/subdomain vs cookie vs parameter). Align it with your caching strategy.
- Audit custom queries for suppress_filters, direct SQL, and hard-coded IDs.
- Validate translation linkage for the broken content: posts and terms.
- Validate menus per language and ensure the theme renders the correct menu location based on language.
- Check string translation domains for theme and plugins; ensure the correct strings exist in each language.
- Purge caches in the right order: CDN/reverse proxy → page cache plugin → object cache → browser cache.
- Add monitoring: a simple script that fetches key pages per language and asserts language markers.
Operational checklist: before you change settings
- Backup database (including WPML/Polylang tables) and wp-content.
- Note current language negotiation type and URL strategy.
- List all cache layers: browser, CDN, reverse proxy, page cache plugin, object cache.
- Confirm staging environment can reproduce the issue.
Operational checklist: after you apply a fix
- Test a logged-out user path (most caches apply here).
- Test a logged-in editor path (different cookies, different cache behavior).
- Verify language switcher links for at least 5 random pages, not just the homepage.
- Check taxonomy archives and search results (custom queries love to hide there).
- Verify hreflang and canonical tags match the actual URLs.
FAQ
1) Why does the site mix languages only “sometimes”?
Because caches don’t “sometimes” vary by language; they vary by whatever the cache key includes. If language isn’t in the key, whichever language warmed the cache last wins until eviction.
2) Is language-in-URL always better than language-by-cookie?
Operationally, yes. URLs make caching, debugging, and SEO more deterministic. Cookie-based language detection can work, but every cache layer must vary by that cookie, and many won’t unless you force them.
3) My page content is correct, but widgets are in the wrong language. Why?
Widgets often run separate queries and may use cached fragments. If those queries disable filters or use transients without language keys, the widget output can drift from the page language.
4) Can Redis object cache cause language mixing?
Yes, indirectly. Redis isn’t the problem; it faithfully stores whatever keys your code asks for. If your theme/plugin caches “latest posts” without including language in the key, Redis will happily serve the wrong language at scale.
5) Why does the language switcher send some pages to the homepage?
That’s usually missing translation linkage (the translation exists but isn’t connected) or the translation doesn’t exist. The switcher can’t invent a target, so it falls back to a safe page.
6) After migrating, translations look duplicated or unlinked. What happened?
Partial database exports often miss plugin tables or rewrite post IDs in ways that break translation group references. Migrations must include the plugin’s mapping tables and preserve IDs or reconcile them properly.
7) How do I tell if it’s taxonomy translation versus post translation?
If the main content is correct but categories/tags, breadcrumbs, and menus are wrong, suspect taxonomy translation. If the post body is wrong or switcher targets are wrong, suspect post translation linkage or routing.
8) Should I “fix” it by disabling caches permanently?
No. That’s not a fix; that’s a performance incident you schedule for later. Make caches language-aware and purge correctly. Keep the speed, keep the correctness.
9) We use a page builder. Does that change anything?
It adds more caching and more places to store global templates. Many mixed-language issues come from global widgets/templates reused across languages without translation-aware variants.
10) What’s the fastest durable change if I’m stuck?
Move to language-in-URL for all languages (including default), then purge all caches and re-save permalinks. It reduces the degrees of freedom that cause mixing.
One reliability quote worth keeping
“Hope is not a strategy.” — often attributed to operations culture (paraphrased idea)
Conclusion: next steps that stick
If you only do two things, do these: make the language explicit in the URL, and make every cache layer vary by language. Most “mixed language” incidents evaporate when routing and caching stop improvising.
Then do the less exciting work: audit suppress_filters, remove language-agnostic transients, translate and link taxonomies, and add a lightweight monitor that checks key pages per language after every deploy. Multilingual WordPress can be stable. It just doesn’t become stable by accident.