WPML Migration Plan — Edge Translation via Cloudflare Workers
Goal: Remove WPML entirely from baseworks.com. Replace it with a custom Cloudflare Worker that acts as a translation proxy — no WordPress multilingual plugin needed. Translations managed in the Obsidian vault + SimpleLocalize (TMS with UI), synced and deployed by Claude Code.
Scope: baseworks.com (portfolio + WooCommerce). Practice.baseworks.com and crm.baseworks.com are English-only, not in scope.
Constraint: No additional WordPress plugins for translation. No Weglot. Custom edge solution only.
1. Current State
Section titled “1. Current State”What WPML does today
Section titled “What WPML does today”- Manages EN ↔ JA translation pairs for pages and posts
- Provides language switcher, URL routing (
/ja/subdirectory), and hreflang tags - Does NOT manage French — French pages are standalone WordPress pages (Quebec Bill 96 compliance)
- WCML (WooCommerce Multilingual) is NOT in use — currency is via WCPay multi-currency
Current language coverage
Section titled “Current language coverage”| Language | Method | Scope |
|---|---|---|
| English | Default | Full site |
| Japanese | WPML translation pairs | Partial (policies, some pages) |
| French | Standalone WP pages | Policies only (Bill 96 compliance) |
Pain points
Section titled “Pain points”- WPML database bloat (custom
icl_*tables, string translations, language assignments) - Backend slowness from WPML hooks firing on every page load
- Renewal cost relative to value delivered
- Translation updates require WP admin — not automatable from the vault
- Plugin dependency chain (WPML core + String Translation + Media Translation)
2. Target Architecture
Section titled “2. Target Architecture”Core idea: Translation Proxy at the Edge
Section titled “Core idea: Translation Proxy at the Edge”WordPress stays monolingual (English only). A Cloudflare Worker sits in front of the site and handles all multilingual logic:
- Visitor requests
/fr/about/ - Worker strips the language prefix → fetches
/about/from WordPress origin - Worker looks up French translations from Cloudflare KV store
- Worker replaces English text nodes in the HTML with French translations
- Worker injects hreflang
<link>tags, setslang="fr"on<html>, rewrites internal links - Visitor receives a fully translated page — WordPress never knew
This is the same architecture Weglot uses, but self-hosted on Cloudflare infrastructure you already control.
Architecture diagram
Section titled “Architecture diagram”Obsidian vault (YAML/JSON translation files) │ │ Claude Code runs simplelocalize-cli ▼SimpleLocalize (TMS — web UI for review + auto-translate) │ │ CLI download → JSON export ▼Cloudflare KV / R2 (translation dictionary per language) │ │ Worker reads translations at request time ▼Cloudflare Worker (translation proxy) │ │ Fetches English HTML from origin │ Replaces text → injects hreflang → rewrites links ▼Visitor sees translated page
WordPress (English only, no multilingual plugin)Design principles
Section titled “Design principles”- WordPress is unaware of translations — zero plugin overhead
- Translations live as flat files (JSON) in the vault — single source of truth
- SimpleLocalize provides the UI — web editor for reviewing translations, auto-translate, translation memory
- Cloudflare Worker handles everything at the edge — routing, text replacement, hreflang, language switcher injection, geo-redirect
- Claude Code is the deployment tool — vault → TMS → KV → Worker
3. TMS: SimpleLocalize
Section titled “3. TMS: SimpleLocalize”Why SimpleLocalize
Section titled “Why SimpleLocalize”- Cheapest TMS with a real CLI and web translation editor
- Supports JSON, YAML, PO — all vault-friendly formats
- Built-in auto-translation (DeepL/Google)
- Translation memory across projects
- No per-seat pricing
- REST API for full programmatic control
Pricing (verified March 2026)
Section titled “Pricing (verified March 2026)”| Tier | Price | Keys | Languages | Auto-translate |
|---|---|---|---|---|
| Community | Free | 250 | 10 | 150k chars initial |
| Developer | (check site) | 1,000 | Unlimited | 300k chars (+100k/mo) |
| Team | €35/mo | 4,000 | Unlimited | 500k chars (+200k/mo) |
| Business | €99/mo | 12,000 | Unlimited | Higher limits |
For Baseworks: Team tier at €35/mo. Handles 4,000 keys across unlimited languages. Estimated need: ~2,500 keys for current site across 5 languages.
CLI workflow
Section titled “CLI workflow”# Installnpm install -g @simplelocalize/cli
# Push source strings from vault to SimpleLocalizesimplelocalize upload \ --apiKey $SIMPLELOCALIZE_API_KEY \ --uploadPath ./translations/en/{ns}.json \ --uploadFormat single-language-json
# Auto-translate new keyssimplelocalize auto-translate --apiKey $SIMPLELOCALIZE_API_KEY
# Pull reviewed translations back to vaultsimplelocalize download \ --apiKey $SIMPLELOCALIZE_API_KEY \ --downloadPath ./translations/{lang}/{ns}.json \ --downloadFormat single-language-jsonAlternatives considered
Section titled “Alternatives considered”| TMS | Monthly cost | Why not chosen |
|---|---|---|
| Crowdin | $50/mo (Pro) | More expensive, features beyond what’s needed |
| POEditor | $20/mo (3,000 strings) | No CLI tool, API-only |
| Phrase Strings | $25-69/mo per seat | Per-seat pricing, enterprise-focused |
| Lokalise | $120+/mo per seat | Overkill for this scale |
4. Cloudflare Worker — The Translation Proxy
Section titled “4. Cloudflare Worker — The Translation Proxy”This is the core technical component. The Worker does six things:
4.1 Language routing
Section titled “4.1 Language routing”baseworks.com/fr/* → Worker strips /fr/, fetches English page, translates to Frenchbaseworks.com/ja/* → Worker strips /ja/, fetches English page, translates to Japanesebaseworks.com/* → Passes through to origin (English, no transformation)4.2 HTML text replacement
Section titled “4.2 HTML text replacement”The Worker uses the HTMLRewriter API (built into Cloudflare Workers) to transform the HTML response stream. This is not regex-based string replacement — it’s a proper streaming HTML parser.
// Conceptual — simplifiedclass TextTranslator { constructor(translations) { this.translations = translations; } text(text) { const translated = this.translations[text.text.trim()]; if (translated) { text.replace(translated); } }}
// In the Worker fetch handler:const response = await fetch(originUrl);const translations = await KV_TRANSLATIONS.get(`fr:page:/about/`, { type: 'json' });
return new HTMLRewriter() .on('*', new TextTranslator(translations)) .on('html', new LangAttributeSetter('fr')) .on('head', new HreflangInjector(url, supportedLanguages)) .on('a[href]', new LinkRewriter('fr')) .transform(response);4.3 hreflang injection
Section titled “4.3 hreflang injection”Worker injects <link rel="alternate" hreflang="..."> tags into <head> for every supported language, plus x-default pointing to English.
4.4 Internal link rewriting
Section titled “4.4 Internal link rewriting”All internal links (<a href="/about/">) get rewritten to include the language prefix (<a href="/fr/about/">) so navigation stays in the selected language.
4.5 Language switcher injection
Section titled “4.5 Language switcher injection”Worker injects a lightweight language switcher (HTML/CSS snippet) into the page — either in the header or as a floating widget. No WordPress plugin needed.
4.6 Geo-based homepage redirect
Section titled “4.6 Geo-based homepage redirect”On the homepage only (/), Worker checks request.cf.country and Accept-Language header to redirect to the appropriate language version. Uses 302 (temporary) to preserve SEO crawlability.
Cloudflare cost
Section titled “Cloudflare cost”| Component | Cost |
|---|---|
| Workers Free tier | 100k requests/day — likely sufficient |
| Workers Paid | $5/mo for 10M requests (if needed) |
| KV storage | Free tier: 100k reads/day, 1k writes/day |
| R2 (if storing large translation dicts) | Free egress, $0.015/GB stored |
| Total | $0-5/mo |
5. What Gets Translated
Section titled “5. What Gets Translated”Translation dictionary structure
Section titled “Translation dictionary structure”Translations are stored as key-value JSON where the key is the English source text (or a stable key) and the value is the translation.
Two approaches, can be combined:
Approach A: Source-text keying (simpler, good for static content)
Section titled “Approach A: Source-text keying (simpler, good for static content)”{ "About Us": "À propos de nous", "Our Programs": "Nos programmes", "Add to Cart": "Ajouter au panier", "Practice Sessions": "Sessions de pratique"}Approach B: Page-scoped keying (better for page-specific content)
Section titled “Approach B: Page-scoped keying (better for page-specific content)”{ "/about/": { "title": "À propos de nous", "meta_description": "Baseworks est un cadre d'éducation physique...", "sections": { "hero_heading": "...", "hero_body": "..." } }}Recommendation: Use Approach B (page-scoped) for page content and Approach A for shared UI strings (buttons, menus, footer). This maps cleanly to the vault file structure.
Content categories and handling
Section titled “Content categories and handling”| Category | Worker handles? | How |
|---|---|---|
| Static page text (headings, paragraphs, lists) | Yes | HTMLRewriter replaces text nodes |
| Navigation menus | Yes | HTMLRewriter matches menu link text |
| Buttons and CTAs | Yes | HTMLRewriter matches button/link text |
| Footer content | Yes | HTMLRewriter matches footer text |
| Meta tags (title, description, OG) | Yes | HTMLRewriter rewrites <title>, <meta> tags |
| WooCommerce product names | Yes | HTMLRewriter matches product title text |
| WooCommerce product descriptions | Yes | HTMLRewriter matches description container |
| WooCommerce cart/checkout labels | Yes | HTMLRewriter matches form labels and button text |
| WooCommerce emails | Partial | Emails bypass Cloudflare — need a WP filter or PO file for email strings |
| Elementor dynamic content | Yes | HTMLRewriter works on the rendered HTML, regardless of how it was built |
| Images with text | No | Need separate translated images (or use CSS content replacement) |
| JavaScript-rendered content | Partial | Content injected by JS after page load won’t be caught by HTMLRewriter. Need client-side translation script for AJAX content |
| Schema.org / JSON-LD | Yes | Worker can parse and replace <script type="application/ld+json"> content |
WooCommerce-specific considerations
Section titled “WooCommerce-specific considerations”Checkout flow: WooCommerce checkout forms render server-side HTML. The Worker can translate all visible labels, error messages, and button text. Currency display is already handled by WCPay multi-currency (no WCML needed).
Cart AJAX: WooCommerce uses AJAX for cart updates (add to cart, quantity changes). These return HTML fragments that go through Cloudflare, so the Worker can intercept and translate them if the AJAX endpoint is routed through the same domain.
Order emails: These are sent server-side and bypass Cloudflare. For translated emails, two options:
- Use WordPress PO files for WooCommerce email strings (standard gettext, no plugin needed)
- Use a small mu-plugin that sets the locale based on the customer’s language preference (stored as order meta)
Receipts/invoices: Same as emails — server-side generated. Need a locale-setting mu-plugin or use a WooCommerce PDF invoice plugin that respects locale.
6. Vault File Structure
Section titled “6. Vault File Structure”03-resources/translations/├── README.md # Workflow documentation├── config/│ ├── simplelocalize.yml # CLI config│ ├── languages.yml # Supported languages + metadata│ └── page-map.yml # URL → translation file mapping├── en/ # English source strings│ ├── global.json # Shared UI: nav, footer, buttons, forms│ ├── woocommerce.json # Product names, cart/checkout labels│ ├── legal.json # Policy page content│ └── pages/│ ├── about.json # /about/ page content│ ├── programs.json # /programs/ page content│ ├── practice-sessions.json # /montreal-practice-sessions/ content│ ├── study-group.json # Study group page content│ └── ...├── fr/ # French translations (same structure)│ ├── global.json│ ├── woocommerce.json│ ├── legal.json│ └── pages/│ └── ...├── ja/ # Japanese translations│ └── ...└── deploy/ # Generated deployment bundles ├── kv-fr.json # Merged JSON for Cloudflare KV upload ├── kv-ja.json └── ...File format example
Section titled “File format example”en/pages/about.json:
{ "_meta": { "url": "/about/", "title": "About Baseworks", "last_synced": "2026-03-10" }, "page_title": "About Baseworks", "meta_description": "Baseworks is a physical education framework...", "hero_heading": "A framework for physical education", "hero_body": "Baseworks provides a structured approach to physical education...", "section_1_heading": "Our Approach", "section_1_body": "..."}fr/pages/about.json:
{ "_meta": { "url": "/fr/about/", "title": "À propos de Baseworks", "last_synced": "2026-03-10" }, "page_title": "À propos de Baseworks", "meta_description": "Baseworks est un cadre d'éducation physique...", "hero_heading": "Un cadre pour l'éducation physique", "hero_body": "Baseworks fournit une approche structurée de l'éducation physique...", "section_1_heading": "Notre approche", "section_1_body": "..."}7. Deployment Pipeline
Section titled “7. Deployment Pipeline”How translations get from vault to visitor
Section titled “How translations get from vault to visitor”Step 1: Edit translation files in vault (Claude Code or manual) │Step 2: Push to SimpleLocalize (CLI upload) │Step 3: Review/auto-translate in SimpleLocalize web UI │Step 4: Pull reviewed translations back to vault (CLI download) │Step 5: Build KV bundle (merge per-page JSONs into single KV-ready file per language) │Step 6: Upload KV bundle to Cloudflare KV (wrangler CLI) │Step 7: Worker immediately serves updated translations (no deploy needed — KV is live)Deploy script (to be built)
Section titled “Deploy script (to be built)”#!/bin/bash# Run by Claude Code after translations are finalized
LANG=$1 # e.g., "fr"
# 1. Pull latest from SimpleLocalizesimplelocalize download \ --apiKey $SIMPLELOCALIZE_API_KEY \ --downloadPath ./translations/$LANG/{ns}.json \ --downloadFormat single-language-json
# 2. Build KV bundle (merge all page files into one JSON)node scripts/build-kv-bundle.js $LANG
# 3. Upload to Cloudflare KVwrangler kv:bulk put \ --namespace-id $KV_NAMESPACE_ID \ ./translations/deploy/kv-$LANG.json
# 4. Purge Cloudflare cache for translated URLscurl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -d '{"prefixes":["baseworks.com/'$LANG'/"]}'
echo "Deployed $LANG translations to edge."Claude Code workflow example
Section titled “Claude Code workflow example”User: "Translate the Practice Sessions page into French"
Claude Code:1. Reads en/pages/practice-sessions.json from vault2. Writes fr/pages/practice-sessions.json (drafts translation or triggers auto-translate)3. Pushes to SimpleLocalize: simplelocalize upload4. User reviews in SimpleLocalize web UI (optional)5. Pulls final version: simplelocalize download6. Builds KV bundle: node scripts/build-kv-bundle.js fr7. Uploads to KV: wrangler kv:bulk put8. Purges CF cache9. Verifies /fr/montreal-practice-sessions/ renders correctly8. Cloudflare Worker — Technical Detail
Section titled “8. Cloudflare Worker — Technical Detail”Worker structure (Wrangler project)
Section titled “Worker structure (Wrangler project)”workers/baseworks-i18n/├── wrangler.toml # Worker config├── src/│ ├── index.js # Main entry point│ ├── router.js # Language detection + URL routing│ ├── translator.js # HTMLRewriter handlers for text replacement│ ├── hreflang.js # hreflang tag injection│ ├── link-rewriter.js # Internal link prefix rewriting│ ├── meta-translator.js # <title>, <meta>, OG tag translation│ ├── schema-translator.js # JSON-LD Schema.org translation│ ├── switcher.js # Language switcher HTML injection│ └── geo-redirect.js # Homepage geo-redirect logic└── test/ └── ...KV namespace structure
Section titled “KV namespace structure”Key format: {lang}:{type}:{path}
Examples: fr:page:/about/ → { "page_title": "...", "hero_heading": "...", ... } fr:global → { "Add to Cart": "Ajouter au panier", ... } fr:woocommerce → { "Product": "Produit", "Quantity": "Quantité", ... } fr:meta:/about/ → { "title": "...", "description": "...", "og:title": "..." } ja:page:/about/ → { ... } ja:global → { ... } config:languages → ["en", "fr", "ja"] config:routes → { "/about/": true, "/programs/": true, ... }How HTMLRewriter translation works
Section titled “How HTMLRewriter translation works”The Worker doesn’t do naive string replacement. It uses Cloudflare’s HTMLRewriter, which is a streaming HTML parser that can:
- Match specific CSS selectors (e.g.,
h1,.entry-title,#product-description) - Modify text content of matched elements
- Add/remove/modify attributes
- Inject new elements
This means translations can be targeted precisely:
- Translate
<h1>but not<code>blocks - Translate visible text but not
data-*attributes - Translate
<title>and<meta name="description">separately from body text
Handling dynamic/AJAX content
Section titled “Handling dynamic/AJAX content”WooCommerce and Elementor sometimes load content via AJAX. The Worker handles this by:
- AJAX HTML fragments — If the AJAX endpoint returns HTML and goes through Cloudflare, the Worker can intercept and translate (same HTMLRewriter approach)
- AJAX JSON responses — The Worker can intercept JSON responses from WP REST API and translate known fields
- Client-side fallback — For content rendered entirely by JavaScript, inject a small client-side translation script that reads from the same translation dictionary (served as a JS bundle from KV)
// Client-side fallback for JS-rendered content (injected by Worker)<script> window.__bwTranslations = { /* loaded from KV */ }; // MutationObserver watches for DOM changes and translates new text nodes</script>9. WooCommerce Email Translation (the one WordPress-side piece)
Section titled “9. WooCommerce Email Translation (the one WordPress-side piece)”Emails are the one area the Worker cannot handle because they’re sent server-side and never pass through Cloudflare.
Solution: Lightweight mu-plugin (not a “translation plugin”)
Section titled “Solution: Lightweight mu-plugin (not a “translation plugin”)”A small mu-plugin (~50 lines) that:
- Stores the customer’s language preference as WooCommerce order meta (captured from the URL language prefix via a hidden field at checkout, or from the
cf-ipcountryheader) - Sets
switch_to_locale()before WooCommerce sends an email - Loads the appropriate PO file for WooCommerce strings
This is NOT a multilingual plugin — it’s a locale switcher for emails only. WordPress core already supports switch_to_locale() natively.
// mu-plugins/bw-order-locale.php (~50 lines)add_action('woocommerce_checkout_order_created', function($order) { $lang = sanitize_text_field($_POST['bw_language'] ?? 'en'); $order->update_meta_data('_bw_language', $lang); $order->save();});
add_action('woocommerce_email_before_order_table', function($order) { $lang = $order->get_meta('_bw_language') ?: 'en'; $locale_map = ['fr' => 'fr_FR', 'ja' => 'ja']; if (isset($locale_map[$lang])) { switch_to_locale($locale_map[$lang]); }});
add_action('woocommerce_email_after_order_table', function() { restore_previous_locale();});PO files for WooCommerce email strings would be generated from the vault translations and deployed to wp-content/languages/woocommerce/ via WP-CLI over SSH.
10. SEO and AI Crawling
Section titled “10. SEO and AI Crawling”What the Worker provides
Section titled “What the Worker provides”| SEO requirement | How the Worker handles it |
|---|---|
| Unique URLs per language | /fr/about/, /ja/about/ — Worker routes these |
| hreflang tags | Worker injects <link rel="alternate"> into <head> |
x-default hreflang | Points to English (no prefix) URL |
lang attribute on <html> | Worker sets lang="fr" / lang="ja" |
| Canonical URLs | Worker sets <link rel="canonical"> to the language-specific URL |
| Translated meta title and description | Worker replaces <title> and <meta name="description"> |
| Translated OG tags | Worker replaces og:title, og:description, og:url |
| Translated Schema.org JSON-LD | Worker parses and replaces translatable fields in JSON-LD |
| Sitemap | Separate multilingual sitemap generated and served by the Worker (or a static file in R2) |
| No cross-language canonicals | Each language version is self-canonical |
| Crawlable by bots | Geo-redirect uses 302 (temporary) on homepage only; all language URLs accessible directly |
Multilingual sitemap
Section titled “Multilingual sitemap”The Worker can serve a sitemap index at /sitemap-languages.xml that lists all translated URLs. Alternatively, generate a static sitemap XML during the deploy step and upload it to R2/KV.
11. Language Plan
Section titled “11. Language Plan”Confirmed languages
Section titled “Confirmed languages”| Language | Code | URL prefix | Priority | Status |
|---|---|---|---|---|
| English | en | / (default) | Active | Full site |
| French | fr | /fr/ | High | Partial (policies only, standalone pages) |
| Japanese | ja | /ja/ | Active | Partial (WPML pairs to migrate) |
Future languages (TBD)
Section titled “Future languages (TBD)”| Language | Code | URL prefix | Notes |
|---|---|---|---|
| (TBD) | — | — | Decision needed — which languages are planned? |
| (TBD) | — | — | SimpleLocalize supports unlimited languages on all paid tiers |
Decision needed: Which additional languages beyond EN/FR/JA? This affects content volume and translation workload, not cost (SimpleLocalize and Cloudflare both support unlimited languages at no extra charge).
12. Migration Plan — Phases
Section titled “12. Migration Plan — Phases”Phase 0: Preparation
Section titled “Phase 0: Preparation”- Full site backup (database + files) to Backblaze B2
- Export all WPML translations (XLIFF export from WPML Translation Management)
- Audit and document every WPML-translated page/post with its translation pair IDs
- Audit WooCommerce: list all products, attributes, and email templates that need translation
- Audit Elementor pages: list all pages built with Elementor and their translatable content
- Create SimpleLocalize account (Team tier, €35/mo)
- Install SimpleLocalize CLI on Patrick’s Mac and VPS
- Install Wrangler CLI (Cloudflare Workers toolchain)
- Set up Cloudflare KV namespace for translations
Phase 1: Build the translation dictionary
Section titled “Phase 1: Build the translation dictionary”- Create vault file structure (
03-resources/translations/) - Extract English source strings from every page on baseworks.com (Claude Code can crawl and extract)
- Organize strings into JSON files per the vault structure (global, woocommerce, legal, pages/*)
- Import existing Japanese translations from WPML export into the vault JSON structure
- Import existing French policy translations into the vault JSON structure
- Push all source strings to SimpleLocalize
- Run auto-translate for French and Japanese on new/untranslated keys
- Review auto-translations in SimpleLocalize UI
Phase 2: Build the Cloudflare Worker
Section titled “Phase 2: Build the Cloudflare Worker”- Scaffold Wrangler project (
workers/baseworks-i18n/) - Implement language router (URL prefix detection + stripping)
- Implement HTMLRewriter text translator (reads translations from KV)
- Implement hreflang tag injection
- Implement internal link rewriting
- Implement meta tag translation (
<title>,<meta>, OG tags) - Implement Schema.org JSON-LD translation
- Implement language switcher injection
- Implement homepage geo-redirect (302,
request.cf.country) - Build the deploy script (vault → SimpleLocalize → KV → cache purge)
- Build the KV bundle builder (merge per-page JSONs into KV-ready format)
- Write tests for the Worker
Phase 3: Test on staging
Section titled “Phase 3: Test on staging”- Deploy Worker to staging.baseworks.com (separate Cloudflare zone or Worker route)
- Test every translated page renders correctly in FR and JA
- Test WooCommerce: product pages, cart, checkout in each language
- Test AJAX content (add to cart, cart updates, dynamic widgets)
- Test hreflang tags with Google’s hreflang testing tool
- Test Schema.org output with Google Rich Results Test
- Test language switcher functionality
- Test geo-redirect behavior
- Test SEOPress sitemap includes translated URLs (or deploy custom sitemap)
- Test with crawl tool (Screaming Frog or similar) to verify all language URLs are accessible
- Load test to verify Worker performance under traffic
Phase 4: Production cutover
Section titled “Phase 4: Production cutover”- Full production backup to B2
- Deploy Worker to baseworks.com Cloudflare zone
- Upload translation KV bundles
- Verify live site in all languages
- Verify WooCommerce checkout in all languages
- Deploy email locale mu-plugin
- Deploy WooCommerce PO files for email strings
- Deactivate WPML on production
- Test everything again post-WPML-deactivation
- Monitor for 48 hours (check error logs, 404s, checkout conversions)
Phase 5: Cleanup
Section titled “Phase 5: Cleanup”- Delete WPML plugin files from server
- Drop WPML database tables (
icl_*tables) after confirming no dependencies - Remove standalone French policy pages (now served by the Worker from the main English pages)
- Optimize database
- Update DNS/Cloudflare settings if needed
- Document the full workflow for Asia
- Set up monitoring alerts for Worker errors
Phase 6: Expansion
Section titled “Phase 6: Expansion”- Translate remaining pages into French (full site coverage)
- Add additional languages
- Build a translation dashboard (optional — SimpleLocalize UI may suffice)
- Set up CI/CD: Git push to translations → auto-deploy to KV
13. Cost Comparison
Section titled “13. Cost Comparison”Current (WPML)
Section titled “Current (WPML)”| Item | Annual cost |
|---|---|
| WPML Multilingual CMS (renewal) | ~$39-99/yr |
| Database overhead (hosting performance impact) | Indirect cost |
| Total | ~$39-99/yr |
Proposed (Edge Translation)
Section titled “Proposed (Edge Translation)”| Item | Annual cost |
|---|---|
| SimpleLocalize Team tier | €420/yr (~$460 USD) |
| Cloudflare Workers | $0-60/yr (likely free tier) |
| Cloudflare KV | $0 (free tier sufficient) |
| Total |
Cost justification
Section titled “Cost justification”Higher dollar cost than WPML, but:
- Zero WordPress overhead — no plugin hooks, no database bloat, no backend slowness
- Edge-fast delivery — translations served from Cloudflare’s global network, not computed by PHP
- Full automation — Claude Code manages the entire pipeline without WP admin access
- Vault-based source of truth — version-controlled, reviewable, portable
- No vendor lock-in — JSON files work with any TMS; Worker code is yours
- No WordPress plugin compatibility issues — ever
- Unlimited languages at no extra cost — scale freely
Budget-conscious alternative
Section titled “Budget-conscious alternative”If €35/mo for SimpleLocalize is too much initially:
- Start with SimpleLocalize Community (free, 250 keys) for a proof of concept
- Manage translations directly in vault JSON files (Claude Code writes them, no TMS UI)
- Add the paid TMS tier later when the translation volume justifies it
- Worker + KV deployment works the same either way
14. Risks and Mitigations
Section titled “14. Risks and Mitigations”| Risk | Impact | Mitigation |
|---|---|---|
| HTMLRewriter misses some text nodes or breaks layout | Medium | Thorough testing on staging; use CSS selectors to target specific elements rather than all text |
| WooCommerce AJAX content not translated | Medium | Client-side fallback script for JS-rendered content; test every WooCommerce flow |
| Translation dictionary grows too large for KV single-key reads | Low | Split into per-page KV entries (already planned); KV values can be up to 25 MB each |
| Cloudflare Worker adds latency | Low | Workers run in <1ms typically; translation lookup from KV adds ~10-50ms — still faster than WPML’s PHP processing |
| SEO disruption during migration | Medium | 1:1 URL mapping from old WPML URLs to new Worker URLs; 301 redirects for any URL structure changes |
| SimpleLocalize pricing changes | Low | Translations are JSON files — can switch to any TMS or manage manually |
| Worker errors cause site downtime | High | Worker has a pass-through fallback — if translation lookup fails, serve the English page. Never block the response. |
15. Open Decisions
Section titled “15. Open Decisions”- Which additional languages beyond EN/FR/JA? — Affects content volume and translation timeline
- Translation approach: source-text keying vs. CSS-selector targeting? — Source-text is simpler but fragile if English text changes; CSS selectors are more robust but require mapping each page element
- Language switcher design — Floating widget, header dropdown, or footer links?
- WooCommerce email language — Store customer language preference via hidden field at checkout, or infer from shipping country?
- Timeline — When to start? Phase 0 (preparation) can begin immediately without affecting the live site
- Worker hosting — Use Cloudflare Workers (recommended) or explore alternatives (Fastly Compute, Vercel Edge)?
- Who reviews auto-translations? — Claude Code drafts, but human review before publish is recommended
16. Technical Prerequisites
Section titled “16. Technical Prerequisites”Before starting Phase 2 (Worker development):
- Cloudflare account with baseworks.com zone (already in place)
- Wrangler CLI installed and authenticated
- Cloudflare KV namespace created
- SimpleLocalize account and API key
- SSH access to WordPress server for email PO file deployment
- Node.js environment for the KV bundle builder script
- Test environment (staging.baseworks.com behind Cloudflare)
17. References
Section titled “17. References”- SimpleLocalize pricing
- SimpleLocalize CLI docs
- Cloudflare HTMLRewriter
- Cloudflare Workers KV
- Cloudflare Workers pricing
- Wrangler CLI
- Crowdin pricing (alternative TMS)
- POEditor pricing (alternative TMS)