2026 · Case study · Phill Morgan
Multilingual CMS Conversion (EN / FR / DE)
Converted a monolingual Craft CMS commerce site into a multilingual platform serving English, French, and German markets. 74 Twig templates, 170 field translation methods, 14 content sections. Staged, migrated, and cut over live with zero content loss.
Available for portfolio review on request.
Stack
- Craft CMS 4
- Twig
- PHP 8
- MySQL
- Neo
- SEOmatic
- Retour
- DeployHQ
Role & Scope
Technical lead. Feasibility audit, architecture, implementation, data migration, live cutover. Coordinated with the designer on translation QA and with the content team on the source-content freeze window.
Overview
A European horticultural brand, trading in the UK for years on a single English site with established domain and SEO authority, wanted to open new search markets in France and Germany. Native-language content, proper hreflang, localised URLs, and no loss of the ranking the English site had earned over its lifetime.
The existing platform was Craft CMS 4 with a deep content model: 74 Twig templates, 170 individual fields, 14 content sections, Neo blocks used heavily for flexible page composition. Multilingual was never a requirement when it was built, and nothing about the original content model had been designed with translation in mind.
The work was to take that monolingual site and extend it into three sites (EN, FR, DE) without rebuilding it, without losing content, and without the outage window that a content-model rewrite would imply.
Key decisions
Native Craft multi-site over a headless i18n rewrite. Craft has first-class multi-site support built on a site-group primitive and per-field translation methods. A headless approach (keep Craft, move the front-end to an i18n-aware framework) would have been a greenfield rebuild: months of work, new deployment story, new bugs. The native path meant the editors’ experience stayed identical, the content model stayed stable, and the delta was in configuration and template changes rather than architecture. The cost: we were tied to Craft’s multi-site conventions, including how Neo propagates across sites. That trade held up; the cost materialised only at the edges (see Neo below).
Flipping field translation methods in place over rebuilding the content model. Every field in Craft has a translation method (site, siteGroup, language, custom, none). Most of the 170 fields needed flipping from the default none to site, so that each locale could hold its own translated value. Rebuilding the content model from scratch with translation-aware fields would have been architecturally cleaner but would have broken every existing piece of content. Flipping in place meant the English values carried through unchanged and the French and German columns started empty, ready for translation. The trade: we inherited some historical field-model decisions that a fresh content model would have cleaned up, which is a scope decision taken deliberately and flagged for a later pass.
A hard content-freeze window over gradual migration. Live editors kept making content changes up to the moment of cutover; a gradual migration would have been a merge nightmare across three sites. The call was to lock all content editing for a defined window, run the migration against a stable snapshot, verify against that snapshot, and cut over. The cost was real (a business cost in editorial downtime, negotiated with the client), but the engineering cost of doing it any other way was larger. The freeze was 48 hours; no content was lost; no merge conflicts.
Bespoke Neo propagation over Craft’s defaults. Neo blocks are Craft’s flexible page-composition plugin; they have their own rules for how block content propagates across sites in a group. Out of the box, the behaviour didn’t match what the content model needed: block structures needed to propagate (so editors didn’t have to rebuild pages in each locale) but block field values needed to be per-site (so each locale could be translated). This required a targeted pass of configuration and, in a couple of places, patching how Neo was being invoked. The trade: a small maintenance surface. A future Neo update could change those defaults, so the decision is documented in the repo and flagged for review on version bumps.
Discovery and feasibility
The work started as a feasibility audit before any implementation commitment. That audit documented:
- Template coverage. 74 Twig templates, split into the ones that emitted user-facing strings that needed translation and the ones that were pure scaffolding.
- Field inventory. All 170 fields, their current translation methods, which ones needed flipping, which ones were safe to leave as site-agnostic (SKUs, internal references).
- Section inventory. 14 content sections (channel, structure, and single-type), mapped against which ones needed multi-site entry propagation.
- Plugin compatibility. Neo, SEOmatic (for per-locale meta and
hreflangemission), Retour (for per-locale redirect maps). Every third-party plugin audited for multi-site readiness. - Risk surface. Identified the Neo propagation behaviour as the single piece most likely to bite.
The audit was the deliverable of a first engagement; the implementation was a second, de-risked by the audit’s findings.
Implementation
The implementation was roughly: configuration first, templates second, content third.
- Site configuration. Added FR and DE as new sites inside the existing site group, with language codes, locale-aware base URLs, and the correct primary-site status. Kept EN as the default fallback for any locale resolution ambiguity.
- Field translation method flips. Ran in batches, verified in staging against representative content. Where a field held a reference (entry, asset, category), translation method was set to propagate the reference but allow the target entry/asset to be translated independently.
- Template internationalisation. Every user-facing string in Twig got wrapped in
|t('site'), with translation keys organised in per-category.phptranslation files. Existing English strings stayed as the source; FR and DE translations lived alongside. hreflangand SEO. SEOmatic configured to emithreflangacross the three sites, including thex-defaultthat pointed back at EN. Per-locale page titles, meta descriptions, and Open Graph tags all parameterised. Structured data per locale where it mattered (schema.orginLanguageon relevant types).- Redirect map via Retour. Any URL path changes between the monolingual and multilingual URL structures were mapped in Retour for 301 redirects, preserving existing SEO authority.
- Language switcher. A small Twig partial emitted correctly-alternate URLs for the current page across locales, with fallbacks for pages that didn’t exist in every locale.
Deployment
- Full multilingual build ran on a staged environment for two weeks before cutover. Real content, real translations, real SEO tags, real
hreflang. - Client-side QA walked through every page in every locale, focusing on content completeness, translation QA, and link integrity.
- Content-freeze window agreed with the client and announced internally.
- Cutover was: freeze, final migration run against the stable snapshot, config push, DNS/routing updates, unfreeze. The window was 48 hours; cutover itself took about two.
- Post-cutover monitoring for 48 hours: redirect hit rates, 404 rates, search-console errors, analytics continuity per locale. Nothing unexpected surfaced.
What I’d do differently
If I did this again, I’d push harder for a content-model audit before the feasibility audit rather than as part of it. Some of the original field decisions (optional vs. required, text vs. rich text, Neo vs. Matrix) were made for a monolingual site and carried friction into the multilingual one; flagging those explicitly in a “future cleanup” document at the start would have given the client a clear bill for technical debt separate from the i18n work itself, and made it easier to address post-launch. The multilingual conversion shipped, but the content model is still carrying a few decisions that deserve another pass.