Bilingual Checkout Funnel
An English sales funnel inside a French WooCommerce store, no multilingual plugin
Context
A French-language WooCommerce site needed to sell a book to an English-speaking audience through a dedicated funnel, landing, checkout and confirmation fully in English, without translating the rest of the store and without bolting a site-wide multilingual plugin onto a live production site for the sake of three pages. WPML-class machinery means duplicated content models, translation tables, locale switching and a permanent maintenance tax; for one product's purchase path, that is the wrong tool by an order of magnitude.
The engineered alternative: a self-contained translation layer that intercepts the PHP gettext filter chain for exactly three allowlisted funnel slugs, leaves the site locale untouched, survives WooCommerce's AJAX checkout re-rendering, and fails closed, the worst possible failure mode is a string rendering in French, never a broken storefront. Eleven tracked iterations took it to a hardened v1.1.0 running in production.
The Challenge
- The translation had to cover strings from three sources at once, theme, WooCommerce core and the funnel plugin, each with its own text domain and timing.
- WooCommerce re-renders checkout fragments over AJAX on every shipping, coupon or payment change, naive translations revert mid-checkout, exactly where trust matters most.
- The French storefront is live revenue: the layer needed a provable inability to affect anything outside the funnel.
- No theme edits allowed, the deliverable had to deploy as a self-contained snippet with instant rollback.
Scope and Approach
-
01
Right-sized architecture
Rejected the site-wide multilingual-plugin route, a full locale switch and translation tables are the wrong tool for one funnel on a live store, in favor of a self-contained translation layer intercepting the gettext filter chain, deployed as a versioned snippet with zero theme edits.
-
02
Fail-closed scoping
The layer activates on an allowlist of exactly three funnel slugs and nothing else, scope checked before any filter attaches. Off-funnel requests never even pay the filter overhead, and the failure mode is "renders in French", never "broken storefront".
-
03
Filter-chain translation
English applied through the PHP gettext filters across the theme, WooCommerce and funnel-plugin text domains on those pages, one product's entire purchase path coherent in English without touching the site locale or duplicating a single content object.
-
04
AJAX-safe checkout
Hooked the front-end checkout refresh cycle so translations re-apply after every WooCommerce fragment re-render, English survives shipping selection, coupon application and payment-method switches, the exact points where scoped translations usually silently revert.
-
05
Hardened iteration
Eleven tracked iterations to the semver-tagged v1.1.0 release, every version retained, edge cases found in real checkout flows fixed one at a time, with the client running a known-good build and an instant rollback path throughout.
Architecture flow
Technical Deep Dive
Why gettext filters instead of locale switching
Switching the locale per-request looks cleaner but drags the whole translation apparatus with it, .mo loading, plugin locale side effects, cache-key fragmentation. Intercepting the gettext filter chain translates exactly the strings that render on exactly the pages that matter, with the original French as the automatic fallback for anything unmapped, inherently fail-safe.
The checkout-fragment problem
WooCommerce's checkout is not a static page; it is re-rendered server-side and injected over AJAX on every state change. Server-side filters cover the fragments only if the scoping recognizes fragment requests as belonging to the funnel, and the front-end hook covers what renders client-side. Missing either half is why "translated checkouts" revert to the source language the moment a customer picks shipping.
Fail-closed as a design proof
The allowlist is checked positively, the layer does nothing unless the current request provably belongs to the three funnel slugs. There is no code path in which the French storefront is modified, which is a property you can state to a nervous client as an architectural fact rather than a testing outcome.
What eleven iterations bought
The version history reads like a hardening log: strings surfacing from unexpected text domains, fragment requests needing scope recognition, edge cases found only by walking the real checkout with real payment methods. Each iteration closed one class of leak and shipped independently, by v1.1.0 the layer had been proven against the checkout as customers actually use it, not as the happy path imagines it.
Engineering Decisions
Three pages of i18n over a multilingual platform
The business needed one funnel in English, not a bilingual site. Matching the solution size to the problem saved the client the permanent WPML maintenance tax and kept the live store's risk surface at zero.
Snippet deployment with retained versions
Deployed as a self-contained versioned snippet, no theme coupling, no build step, rollback is "activate the previous version". Eleven iterations were cheap precisely because deployment was.
Translations in one reviewable map
The English strings live in a single translation mapping inside the snippet rather than scattered through template overrides, the author can read every customer-facing sentence in one place, request a wording change, and receive it as a one-line diff in the next version.
Results
- An English purchase funnel live inside a French store, one product, three pages, zero multilingual plugin, zero site-locale changes.
- Fail-closed by construction: outside the allowlisted funnel, the French site cannot be affected.
- Translations survive WooCommerce checkout-fragment AJAX refreshes, the point where scoped i18n usually breaks.
- In production and selling, hardened across 11 tracked iterations to v1.1.0 with instant rollback at every step.
Have a system that needs
this kind of work?
Tell me what is breaking or what you need built. I will reply with how I would approach it and what I would check first.