WordPress
WPML, Polylang, TranslatePress: 19 multisite-valkuilen
Drie vorige teams. Drie translation-plugins. Eén WordPress-multisite. En een content-editor om 23:00 die zich afvraagt waarom de Franse homepage de Engelse titel toont.

Het is 23:14 in een kantoor in Mechelen en de content-lead staart naar de Franse staging-homepage. De titel zegt Welcome to Mechelen. De URL zegt /fr/. De body is in het Frans. Het titelveld in de translation-editor is leeg, maar de front-end zou moeten terugvallen op Franse copy, niet Engelse. Ze heeft dit ticket deze maand al zes keer ingediend. De eerste vijf keer schreef een developer een SQL-oneliner tegen postmeta en kwam de titel terug. Daarna ging het weer stuk.
Dit was week drie van het samenvoegen van zeven WordPress-microsites tot één multisite voor een Belgische toerismedienst van 24 mensen. Elk van de drie vorige content-teams had een andere translation-plugin gekozen: WPML op de grootste microsite, Polylang op drie van de kleinere, TranslatePress op twee. De doel-stack was één netwerk op WordPress 6.5 met WPML als standaard, sub-sites gemapt op /nl, /fr, /en-paden onder het hoofddomein.
Onderstaande cheatsheet schreven we tijdens de migratie. Negentien valkuilen, in vijf categorieën, elk gelabeld met één van drie verdicten: Admin (een content-editor lost het op in de WordPress-UI), Hybrid (de UI laat je het proberen, maar een developer-audit is verstandig), of Script (geen UI-pad; rol een wp db query uit tegen de relatietabellen en hou de logs in de gaten).
URL's en routing
1. Zelfde slug in twee talen (Polylang)
Verdict: Admin. Polylang slaat slugs per taal op maar blokkeert geen collisions. Een Franse en een Nederlandse post kunnen beide eindigen op /agenda, waarna de tweede stilletjes de URL overneemt. Een editor kan er één hernoemen. De valkuil: 301-redirects vanaf de oude microsite wijzen naar de verkeerde taal. Fix de slug en check daarna de log van de Redirection-plugin.
2. wp_unique_post_slug-collisions tijdens bulk-import
Verdict: Script. Als je 8.000 historische posts in één keer importeert, hangt wp_unique_post_slug er -2, -3 aan vast bij alles wat botst, inclusief de eigen vertaalsiblings. Na import krijg je museum in het Nederlands en museum-2 in het Frans. Geen enkel Admin-scherm laat dit zien. We draaiden een CLI-scan tegen wp_posts.post_name gegroepeerd op de WPML-trid en herschreven de suffixen.
3. Permalink-structuur verschilt per locale (TranslatePress)
Verdict: Hybrid. TranslatePress routeert via één canonical URL en wisselt van taal via een query string of sub-folder. Als het vorige team pretty permalinks had voor Nederlands maar niet voor Frans, toont de sitemap twee verschillende patronen en indexeert Google geen van beide netjes. Editors kunnen er één kiezen in Instellingen. Een developer wist daarna de redirect-cache en dient de sitemap opnieuw in.
4. /fr/-prefix mist na multisite-domeinmapping
Verdict: Script. Als je een sub-site mapt op een sub-folder, slaat WPML de taalprefix op in wp_options.icl_sitepress_settings onder het oude domein. De Admin-UI laat je dit na de verhuizing niet meer herschrijven. We patchten de serialized array met een korte PHP-oneliner en flushten de rewrite rules.
Content en metadata
5. ACF-velden niet gemarkeerd voor copy bij vertaling (WPML)
Verdict: Admin. Advanced Custom Fields heeft per veldgroep een markering nodig: Copy of Translate, in het WPML-veldenpaneel. Editors kunnen dit zelf doen. De adder: als het vorige team het nooit instelde, zitten de Franse vertalingen vol lege velden die niemand opvalt omdat de templates terugvallen op de Nederlandse waarden.
6. Yoast SEO-meta die wegloopt tussen vertaalsiblings
Verdict: Hybrid. Yoast slaat zijn meta op in postmeta per post, niet per trid. Dus als een marketeer de Nederlandse meta-description bijwerkt, blijft de Franse sibling staan op wat er drie maanden geleden stond. De Yoast WPML-glue-plugin lost dit deels op. Editors kunnen beide versies vanuit de Admin bijwerken. Een developer moet de drift nog steeds doorlopen voor je live gaat.
7. Featured image kwijt bij vertaaldubbel
Verdict: Admin. WPML dupliceert posts, maar niet altijd de featured image, afhankelijk van de Media translation-instelling. Een editor kan 'm opnieuw koppelen. Wij maakten de fix permanent door 'copy featured image from original' aan te zetten in de WPML-media-instellingen.
8. Hergebruikte blocks (wp_block CPT) niet aangezet voor vertaling
Verdict: Admin. Het wp_block-posttype is standaard verborgen voor de translation manager. Tot je het aanvinkt in Instellingen, toont de Franse homepage de Nederlandse CTA-block. Editors kunnen het zelf aanzetten. Daarna moeten ze elk hergebruikt block één keer opnieuw vertalen.
9. Custom post type geregistreerd na WPML-save
Verdict: Script. Als een theme een CPT registreert (bijvoorbeeld event) op een action hook die afvuurt nadat de WPML-instellingenpagina is gerenderd, verschijnt de CPT niet in de translation-config. De editor ziet niks vreemds. We schreven een CLI-command die elke CPT langsloopt, aan WPML vraagt wat de translation mode is en de ontbrekende entries rechtstreeks in icl_sitepress_settings schrijft.
10. Block-editor inner HTML met onvertaalde strings
Verdict: Script. Gutenberg slaat blocks op als HTML met inline strings. Als een Franse pagina is gedupliceerd vanuit het Nederlands en de editor alleen de zichtbare tekst in het titelveld heeft aangepast, staat de aria-label op een knop twee blocks lager nog steeds in het Nederlands. Screenreaders vangen dit op; visuele review niet. We scanden post_content met regex op de meest voorkomende Nederlandse UI-woorden en markeerden 318 pagina's voor review.
Taxonomieën en menu's
11. Categorie-vertaalgroepen wees na term-merge
Verdict: Script. Polylang slaat translation-relaties op in een verborgen taxonomie genaamd term_translations, met de gekoppelde groep geserializeerd in wp_terms.description. Als iemand twee categorieën samenvoegt vanuit de Admin, refereert de serialized array nog steeds naar de verwijderde term. De site blijft werken tot je een nieuwe post onder de samengevoegde categorie probeert te vertalen, en dan geeft de taal-switcher een 404. De reparatie: loop elke term_translations-rij langs en herschrijf de description.
12. Menu-items wijzen naar verwijderde vertaal-ID's
Verdict: Script. WordPress-menu's verwijzen naar posts via ID, niet via trid. Als je een vertaling verwijdert en opnieuw aanmaakt, verandert het ID. Het menu wijst nog steeds naar de oude. De Admin toont het menu-item als geldig, want de rij in wp_postmeta met _menu_item_object_id bestaat nog. We schreven een CLI-script dat menu-meta left-joinde met wp_posts en elk menu-item met een ontbrekend doel opsomde.
13. Tag-vertalingen zonder originele taal-entry
Verdict: Admin. Polylang maakt soms de Franse tag aan maar vergeet de Nederlandse als deel van dezelfde groep te markeren. Editors kunnen dit repareren vanaf het Tags-scherm door op de kleine vlag-kolom te klikken. De fix kost tien seconden per tag. Met 600 tags: reken op een middag.
SEO en sitemaps
14. Sitemap zonder hreflang per taal
Verdict: Admin als Yoast WPML-glue geïnstalleerd is; anders Script. De standaard WordPress 6.5-sitemap (6.5 release notes) weet niets van translation-groepen. Zonder de glue-plugin indexeert Google de Franse en Nederlandse versies als duplicate content. Het Admin-pad: installeer de glue en zet één checkbox aan. Het Script-pad, als je geen extra plugin kunt installeren: filter wp_sitemaps_posts_entry en injecteer zelf alternate-rels.
15. Canonical URL wijst naar standaardtaal
Verdict: Hybrid. Als een post geen vertaling heeft, kan WPML hem verbergen of terugvallen op de standaardtaal. In fallback-modus wijst de canonical naar de Nederlandse URL, ook als de bezoeker op /fr/ landt. Editors kunnen de policy site-breed wijzigen. Developers moeten eerst auditen welke CPT's hem gebruiken voor je iets verandert.
16. REST API lekt draft-vertalingen
Verdict: Script. Het wp/v2/posts-endpoint retourneert alle taalversies van een post, inclusief drafts, tenzij je expliciet filtert op lang. Een headless front-end die op de Nederlandse site is gebouwd kan per ongeluk ongepubliceerde Franse copy renderen. De fix is een rest_post_query-filter dat de huidige taal injecteert. Hier is geen Admin-scherm voor.
Performance en cron
17. WPML-icl_translations-tabel groeit voorbij 500k rijen
Verdict: Script. Elke post, term, menu-item en string krijgt een rij. Na een paar jaar en wat imports wordt wp_icl_translations niet meer goed geïndexeerd. Editor-schermen met 200 posts erin hebben opeens 8 seconden nodig om te laden. De reparatie: vacuüm de orphan-rijen.
# 1. Maak een snapshot van de tabel voordat je iets aanraakt
wp db export icl-snapshot.sql --tables=wp_icl_translations
# 2. Toon orphan translation-rijen waarvan het post-target niet meer bestaat
wp db query "
SELECT t.translation_id, t.element_id, t.language_code
FROM wp_icl_translations t
LEFT JOIN wp_posts p ON p.ID = t.element_id
WHERE t.element_type LIKE 'post_%' AND p.ID IS NULL
LIMIT 50;
"
# 3. Na verificatie: verwijder in batches
wp db query "
DELETE t FROM wp_icl_translations t
LEFT JOIN wp_posts p ON p.ID = t.element_id
WHERE t.element_type LIKE 'post_%' AND p.ID IS NULL
LIMIT 1000;
"18. TranslatePress auto-translate verbrandt DeepL-credits per cron-run
Verdict: Admin. TranslatePress kan zo zijn ingesteld dat elke nieuwe string die hij ziet automatisch wordt vertaald. Als dezelfde cron-gegenereerde event-ticker elke 15 minuten met een nieuwe timestamp-string langskomt, blijft de DeepL-rekening oplopen. Een editor kan auto-translate voor onbekende strings uitzetten in de Admin. Waarschijnlijk verstandig.
19. Polylang-taalswitcher in edge-cache
Verdict: Hybrid. Als je CDN de pagina-HTML cachet zonder te variëren op de taal-cookie, kan een Nederlandse bezoeker op de gecachete Franse versie landen. De Admin-fix: laat de taal-switcher distinctieve URL's per taal gebruiken (sub-folders, geen query strings). De developer-fix: voeg Vary: Cookie toe aan de cache-regels voor de homepage en elke landingspagina die de taal-cookie gebruikt om te redirecten.
Het script dat we om middernacht schreven
Case 11, de orphan term_translations-groep, is degene die we op een donderdag om 00:40 draaiden, zes uur nadat staging groen stond. Het is ook het kleinste voorbeeld van waarom dit soort consolidaties in WP-CLI thuishoren en niet in de Admin-UI.
<?php
// rebuild-polylang-translations.php
// Draaien met: wp eval-file rebuild-polylang-translations.php
$languages = ['nl', 'fr', 'en'];
$terms = get_terms([
'taxonomy' => 'post_translations',
'hide_empty' => false,
]);
foreach ($terms as $term) {
$translations = maybe_unserialize($term->description);
if (!is_array($translations)) {
WP_CLI::warning("Skipping {$term->term_id}: not a serialized array");
continue;
}
foreach ($languages as $code) {
if (!isset($translations[$code])) {
$translations[$code] = 0;
}
}
wp_update_term($term->term_id, 'post_translations', [
'description' => maybe_serialize($translations),
]);
WP_CLI::log("Rebuilt term {$term->term_id}");
}
Translation-plugins zijn geen uitwisselbare databases. WPML, Polylang en TranslatePress slaan de relatie tussen vertaalsiblings elk op een andere plek op. Consolideren betekent kiezen welke plek de canon is en de andere herschrijven naar die canon.
Wat deze consolidatie écht kostte
De toerismedienst had vier weken begroot voor de multisite-verhuizing. Wij hadden er zes nodig. Ongeveer de helft van de extra tijd ging op aan Case 9 en Case 11: custom post types die op een late hook waren geregistreerd, en term-groepen die uit elkaar waren gelopen na een categorie-opschoning die niemand zich nog herinnerde uit 2019. De andere helft was het opnieuw draaien van de WPML translation memory-import nadat we zagen dat strings die via String Translation waren geregistreerd niet werden gematcht tegen de nieuwe sub-site-context.
Toen we de consolidatie voor de toerismedienst draaiden, was Case 17 de valkuil waar we steeds tegenaan liepen: een wp_icl_translations-tabel die in zes jaar voorbij de 600.000 rijen was gegroeid en elke Admin-list-query traag maakte. We losten het uiteindelijk op door het snapshot-en-vacuüm-script hierboven tijdens het cut-over-weekend op elke sub-site te draaien, en daarna een maandelijkse cron toe te voegen die orphan-groei boven de 2% flagt. Dat soort werk bedoelen we als we het hebben over legacy-migratie: het gaat vooral om relatietabellen en CLI-scripts, met een rustige content-editor stand-by.
Sta je op het punt een vergelijkbare consolidatie te starten? Het kleinste wat je vandaag kunt doen, is wp db size --tables --format=csv | sort -t, -k2 -n -r | head -20 draaien op elke site die je wilt samenvoegen. De translation-relatietabellen die die lijst domineren, bepalen het tempo van de rest van het project.
Kern
Een translation-plugin-migratie is geen content-migratie. Het is een relatietabel-migratie met editorial UI erbovenop, en de wpcli-scripts vormen het echte plan.
FAQ
Kunnen we WPML, Polylang en TranslatePress naast elkaar draaien tijdens een consolidatie?
Alleen op aparte sub-sites van een multisite. Ze registreren elk botsende filters op dezelfde hooks. Twee tegelijk op dezelfde site breekt de taal-switcher en corrumpeert translation-groepen.
Welke translation-plugin moeten we als standaard kiezen voor een multisite?
WPML voor grote redactieteams die per-veld-vertaalcontrole nodig hebben. Polylang voor kleinere sites die minder overhead willen. TranslatePress als je team visuele front-end-bewerking prefereert. De keuze is redactioneel, niet technisch.
Wat gaat als eerste stuk als een translation-tabel voorbij de 500.000 rijen groeit?
Admin-lijst-schermen. De post-lijst en term-lijst joinen beide met de translation-tabel bij elke load. Orphan-rijen laten MySQL de index overslaan. Vacuüm de orphans maandelijks en het symptoom verdwijnt.
Heeft WordPress 6.5 iets veranderd aan hoe deze plugins zich gedragen?
Het nieuwe sitemap-gedrag en de block-pattern-overrides leggen een paar extra valkuilen bloot, maar de kern-translation-tabellen van alle drie de plugins werken nog precies zoals in 6.4.
Is er een Admin-only-pad door alle 19 cases heen?
Nee. Grofweg de helft van de cases vraagt om een database-query of een wpcli-script. Plan een developer-pairing-sessie in tijdens cut-over, niet alleen editor-tijd.