Skip to content

WPML Migration Plan — Edge Translation via Cloudflare Workers

Created 2026-03-10
Updated 2026-03-10
Status draft
Tags websitemultilingualtranslationwpmlmigrationinfrastructurecloudflare

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.


  • 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
LanguageMethodScope
EnglishDefaultFull site
JapaneseWPML translation pairsPartial (policies, some pages)
FrenchStandalone WP pagesPolicies only (Bill 96 compliance)
  • 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)

WordPress stays monolingual (English only). A Cloudflare Worker sits in front of the site and handles all multilingual logic:

  1. Visitor requests /fr/about/
  2. Worker strips the language prefix → fetches /about/ from WordPress origin
  3. Worker looks up French translations from Cloudflare KV store
  4. Worker replaces English text nodes in the HTML with French translations
  5. Worker injects hreflang <link> tags, sets lang="fr" on <html>, rewrites internal links
  6. 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.

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)
  1. WordPress is unaware of translations — zero plugin overhead
  2. Translations live as flat files (JSON) in the vault — single source of truth
  3. SimpleLocalize provides the UI — web editor for reviewing translations, auto-translate, translation memory
  4. Cloudflare Worker handles everything at the edge — routing, text replacement, hreflang, language switcher injection, geo-redirect
  5. Claude Code is the deployment tool — vault → TMS → KV → Worker

  • 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
TierPriceKeysLanguagesAuto-translate
CommunityFree25010150k chars initial
Developer(check site)1,000Unlimited300k chars (+100k/mo)
Team€35/mo4,000Unlimited500k chars (+200k/mo)
Business€99/mo12,000UnlimitedHigher 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.

Terminal window
# Install
npm install -g @simplelocalize/cli
# Push source strings from vault to SimpleLocalize
simplelocalize upload \
--apiKey $SIMPLELOCALIZE_API_KEY \
--uploadPath ./translations/en/{ns}.json \
--uploadFormat single-language-json
# Auto-translate new keys
simplelocalize auto-translate --apiKey $SIMPLELOCALIZE_API_KEY
# Pull reviewed translations back to vault
simplelocalize download \
--apiKey $SIMPLELOCALIZE_API_KEY \
--downloadPath ./translations/{lang}/{ns}.json \
--downloadFormat single-language-json
TMSMonthly costWhy 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 seatPer-seat pricing, enterprise-focused
Lokalise$120+/mo per seatOverkill 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:

baseworks.com/fr/* → Worker strips /fr/, fetches English page, translates to French
baseworks.com/ja/* → Worker strips /ja/, fetches English page, translates to Japanese
baseworks.com/* → Passes through to origin (English, no transformation)

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 — simplified
class 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);

Worker injects <link rel="alternate" hreflang="..."> tags into <head> for every supported language, plus x-default pointing to English.

All internal links (<a href="/about/">) get rewritten to include the language prefix (<a href="/fr/about/">) so navigation stays in the selected language.

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.

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.

ComponentCost
Workers Free tier100k requests/day — likely sufficient
Workers Paid$5/mo for 10M requests (if needed)
KV storageFree tier: 100k reads/day, 1k writes/day
R2 (if storing large translation dicts)Free egress, $0.015/GB stored
Total$0-5/mo

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.

CategoryWorker handles?How
Static page text (headings, paragraphs, lists)YesHTMLRewriter replaces text nodes
Navigation menusYesHTMLRewriter matches menu link text
Buttons and CTAsYesHTMLRewriter matches button/link text
Footer contentYesHTMLRewriter matches footer text
Meta tags (title, description, OG)YesHTMLRewriter rewrites <title>, <meta> tags
WooCommerce product namesYesHTMLRewriter matches product title text
WooCommerce product descriptionsYesHTMLRewriter matches description container
WooCommerce cart/checkout labelsYesHTMLRewriter matches form labels and button text
WooCommerce emailsPartialEmails bypass Cloudflare — need a WP filter or PO file for email strings
Elementor dynamic contentYesHTMLRewriter works on the rendered HTML, regardless of how it was built
Images with textNoNeed separate translated images (or use CSS content replacement)
JavaScript-rendered contentPartialContent injected by JS after page load won’t be caught by HTMLRewriter. Need client-side translation script for AJAX content
Schema.org / JSON-LDYesWorker can parse and replace <script type="application/ld+json"> content

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:

  1. Use WordPress PO files for WooCommerce email strings (standard gettext, no plugin needed)
  2. 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.


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
└── ...

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": "..."
}

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-translations.sh
#!/bin/bash
# Run by Claude Code after translations are finalized
LANG=$1 # e.g., "fr"
# 1. Pull latest from SimpleLocalize
simplelocalize 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 KV
wrangler kv:bulk put \
--namespace-id $KV_NAMESPACE_ID \
./translations/deploy/kv-$LANG.json
# 4. Purge Cloudflare cache for translated URLs
curl -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."
User: "Translate the Practice Sessions page into French"
Claude Code:
1. Reads en/pages/practice-sessions.json from vault
2. Writes fr/pages/practice-sessions.json (drafts translation or triggers auto-translate)
3. Pushes to SimpleLocalize: simplelocalize upload
4. User reviews in SimpleLocalize web UI (optional)
5. Pulls final version: simplelocalize download
6. Builds KV bundle: node scripts/build-kv-bundle.js fr
7. Uploads to KV: wrangler kv:bulk put
8. Purges CF cache
9. Verifies /fr/montreal-practice-sessions/ renders correctly

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/
└── ...
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, ... }

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

WooCommerce and Elementor sometimes load content via AJAX. The Worker handles this by:

  1. AJAX HTML fragments — If the AJAX endpoint returns HTML and goes through Cloudflare, the Worker can intercept and translate (same HTMLRewriter approach)
  2. AJAX JSON responses — The Worker can intercept JSON responses from WP REST API and translate known fields
  3. 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:

  1. 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-ipcountry header)
  2. Sets switch_to_locale() before WooCommerce sends an email
  3. 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.


SEO requirementHow the Worker handles it
Unique URLs per language/fr/about/, /ja/about/ — Worker routes these
hreflang tagsWorker injects <link rel="alternate"> into <head>
x-default hreflangPoints to English (no prefix) URL
lang attribute on <html>Worker sets lang="fr" / lang="ja"
Canonical URLsWorker sets <link rel="canonical"> to the language-specific URL
Translated meta title and descriptionWorker replaces <title> and <meta name="description">
Translated OG tagsWorker replaces og:title, og:description, og:url
Translated Schema.org JSON-LDWorker parses and replaces translatable fields in JSON-LD
SitemapSeparate multilingual sitemap generated and served by the Worker (or a static file in R2)
No cross-language canonicalsEach language version is self-canonical
Crawlable by botsGeo-redirect uses 302 (temporary) on homepage only; all language URLs accessible directly

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.


LanguageCodeURL prefixPriorityStatus
Englishen/ (default)ActiveFull site
Frenchfr/fr/HighPartial (policies only, standalone pages)
Japaneseja/ja/ActivePartial (WPML pairs to migrate)
LanguageCodeURL prefixNotes
(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).


  • 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
  • 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
  • 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
  • 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
  • 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)
  • 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
  • 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

ItemAnnual cost
WPML Multilingual CMS (renewal)~$39-99/yr
Database overhead (hosting performance impact)Indirect cost
Total~$39-99/yr
ItemAnnual cost
SimpleLocalize Team tier€420/yr (~$460 USD)
Cloudflare Workers$0-60/yr (likely free tier)
Cloudflare KV$0 (free tier sufficient)
Total€420-480/yr ($460-530 USD)

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

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

RiskImpactMitigation
HTMLRewriter misses some text nodes or breaks layoutMediumThorough testing on staging; use CSS selectors to target specific elements rather than all text
WooCommerce AJAX content not translatedMediumClient-side fallback script for JS-rendered content; test every WooCommerce flow
Translation dictionary grows too large for KV single-key readsLowSplit into per-page KV entries (already planned); KV values can be up to 25 MB each
Cloudflare Worker adds latencyLowWorkers run in <1ms typically; translation lookup from KV adds ~10-50ms — still faster than WPML’s PHP processing
SEO disruption during migrationMedium1:1 URL mapping from old WPML URLs to new Worker URLs; 301 redirects for any URL structure changes
SimpleLocalize pricing changesLowTranslations are JSON files — can switch to any TMS or manage manually
Worker errors cause site downtimeHighWorker has a pass-through fallback — if translation lookup fails, serve the English page. Never block the response.

  1. Which additional languages beyond EN/FR/JA? — Affects content volume and translation timeline
  2. 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
  3. Language switcher design — Floating widget, header dropdown, or footer links?
  4. WooCommerce email language — Store customer language preference via hidden field at checkout, or infer from shipping country?
  5. Timeline — When to start? Phase 0 (preparation) can begin immediately without affecting the live site
  6. Worker hosting — Use Cloudflare Workers (recommended) or explore alternatives (Fastly Compute, Vercel Edge)?
  7. Who reviews auto-translations? — Claude Code drafts, but human review before publish is recommended

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)