You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Deployment:wrangler deploy via Astro Cloudflare adapter
The Bug
getEmDashEntry and getEmDashCollection are documented to automatically resolve the current locale from the Astro i18n request context (via ALS — AsyncLocalStorage). On Cloudflare Workers SSR, this does not work. Pages under /en/ receive PL content instead of EN content.
Reproduction
// src/pages/en/index.astro — this is an /en/ page, Astro i18n locale should be "en"
const {entry: page} = await getEmDashEntry("pages", "home");
// Expected: returns the row with locale='en' and slug='home'
// Actual: returns the row with locale='pl' and slug='home'
Looking at EmDash source, getEmDashCollectionUncached resolves locale in this priority order:
Explicit filter.locale param
ALS request context ctx?.locale
defaultLocale from i18n config
On Cloudflare Workers, step 2 fails — the ALS context either isn't propagated to the D1 query layer, or Astro's i18n middleware doesn't set it in the ALS store that EmDash reads from. The result: every query silently falls back to defaultLocale ("pl"), and your EN pages render PL content with zero errors or warnings.
This is particularly dangerous because it fails silently — no error thrown, no console warning. The page renders fine, just in the wrong language. We only caught it because a human looked at the live site.
Context: The Full Scope of Localizing an EmDash-Powered Site
We want to share the full picture of what i18n on EmDash actually looks like in production, because this bug was just one piece of a much larger puzzle. We localized a complete Polish business website (https://chcedointernetu.pl) to English — a real production site, not a toy project. This took multiple days of work and touched virtually every layer of the stack.
Starting Point
The site was built as a Polish-only website. When EmDash's i18n migration (019_i18n.ts) ran, it added locale and translation_group columns to all ec_* tables and backfilled existing rows with locale = 'pl'. So we started from a state where:
All CMS content (pages, services, FAQ items, feature items, reviews, blog posts) was locale: 'pl'
The Astro i18n config was set up (defaultLocale: "pl", locales: ["pl", "en"], prefixDefaultLocale: false)
A dynamic [slug].astro catch-all page rendered CMS pages (consultations, privacy policy, AI ethics declaration) via getEmDashEntry("pages", slug) + Portable Text
No EN content existed anywhere — neither in CMS nor as static pages
This means the localization effort was additive — we had to build the entire EN layer from scratch on top of an existing, fully-functional PL site, without breaking anything.
The CMS Parity Problem
An important issue we discovered late: on the PL side, pages like consultations, privacy policy, and AI ethics declaration are CMS-driven — they live in ec_pages with Portable Text content, rendered by a dynamic [slug].astro route. This means Konrad can edit them in the EmDash admin panel.
But on the EN side, we created those same pages as hardcoded .astro files (src/pages/en/consultations.astro, src/pages/en/privacy-policy.astro, etc.) with translations baked into the template. This breaks the CMS editing model — the EN pages can't be edited from the admin panel.
The proper solution is an EN catch-all src/pages/en/[slug].astro that loads CMS pages with { locale: "en" }, plus EN CMS entries for each page. We did this correctly for the homepage (created an EN entry in ec_pages with all 60+ fields translated). But doing it for every page is a significant effort, especially when Portable Text content needs to be translated block by block.
This is another area where EmDash could help: a "duplicate page to another locale" feature in the admin panel, or MCP tooling that properly handles locale and translation_group fields during content creation.
What We Had to Localize
This wasn't "just translate the text." Here's what the project actually involved:
1. CMS Content Layer (D1)
Home page: Created a full EN copy in ec_pages with locale: 'en' and translation_group linking to the PL original. ~60 fields translated (hero, stats, sections, contact, meta).
FAQ items: 6 EN entries in ec_faq_items with matching translation_group references.
Feature items: 12 EN entries in ec_feature_items with EN titles, descriptions, and EN-slug URLs.
Reviews: 29 customer reviews translated in the component (since reviews don't have locale support in the CMS schema).
Feature item URLs: Had to be updated in D1 when we changed to EN slugs (/en/services/strony-www → /en/services/websites).
All of this required direct D1 SQL because EmDash's MCP content_create doesn't reliably set locale/translation_group fields (separate issue).
2. Page-Level Translations (Astro Components)
16 EN pages created in src/pages/en/ — each one a careful translation that preserves the same component structure as the PL version.
EN homepage rewrite: From hardcoded content → CMS-driven (matching PL), which is where the locale bug hit us.
11 service detail pages: Complete translation maps for problem/solution/process/FAQ sections (~350 lines of EN translations in a dedicated en-service-translations.ts file), because the CMS stores PL content and the EN pages need to override it.
Knowledge Cards: Entirely new EN page with 8 hardcoded translated cards (the PL version loads from CMS blog posts, but there's no EN blog).
3. URL Architecture
This was surprisingly complex:
EN service slugs: We couldn't use PL CMS slugs in EN URLs (/en/services/strony-www looks broken). Built a bidirectional slug mapping system:
strony-www ↔ websites
sklepy-internetowe ↔ e-commerce
wizytowka-google ↔ google-business-profile
gry-marketingowe ↔ marketing-games
... (11 total)
301 redirects: Old PL slugs under /en/services/ redirect to EN slugs.
URL rewriting in components:ServicesGrid, Features, mega-menu, related services — all needed to translate /uslugi/strony-www → /en/services/websites when lang="en".
Language switcher: Required full bidirectional route mapping (not just prefix /uslugi → /services, but also slug translation within sub-routes).
4. SEO & Structured Data
Every page needed SEO parity between PL and EN:
Hreflang tags: Bidirectional <link rel="alternate" hreflang="pl|en"> with translated URLs.
Schema.org: Organization hasOfferCatalog with EN service URLs, inLanguage: "en" on EN pages, datePublished/dateModified on homepages.
Sitemap: Dynamic generation of EN service URLs with slug translation (the PL sitemap queries CMS, EN sitemap maps those slugs to EN equivalents).
FAQ schema: Present on both PL and EN homepages and service detail pages.
BreadcrumbList: EN labels ("Home > Services > Websites" not "Strona główna > Usługi > Strony WWW").
OG tags:og:locale and og:locale:alternate set correctly per language.
5. Component-Level i18n
Components that render on both PL and EN pages needed lang prop support:
ContactForm: Field labels, button text, status messages, RODO note, Turnstile language.
Reviews: 29 review translations keyed by author name, plus badge/headline/intro text.
HangmanGame: Full rewrite — EN word list (30 marketing terms), EN keyboard layout (no ĄĆĘŁŃÓŚŹŻ row), EN UI strings, EN CTA URLs. Interactive game, not just text.
Cookie consent: Already had EN translations but needed verification.
Inactive tab titles: "Come back to us!" vs "Wróć do nas 😊" — conditional on isEn.
6. Navigation & Footer
Mega-menu: EN service items with translated titles, descriptions, and EN-slug URLs.
Footer: Knowledge Cards link, policy links — conditional per language.
Nav CTA: "Free AI Audit" vs "Darmowy audyt AI" with correct EN URL.
7. Other Touchpoints We Didn't Expect
Things that surprised us:
GEO audit share links: The API generated share URLs hardcoded to /darmowy-audyt-geo. EN pages needed to rewrite these to /en/free-geo-audit on the client side.
Portfolio tech tags: CMS stores Polish tech names ("Leaderboardy", "Dedykowane moduły"). Needed a translation map in the EN portfolio page.
Case study links: EN case-studies page linked to /case-studies/slug instead of /en/case-studies/slug.
Accessibility page: Stated "Website language: Polish (pl)" on the EN version.
Blog strip with dates: PL homepage blog strip needed dates added (for datePublished citability signal), which is a PL-only feature but the infrastructure change touched shared components.
Feature tab filtering broke after slug change: The Features.astro component categorizes cards into tabs (Web, Marketing, AI) by matching URL patterns (url.includes('reklamy'), url.includes('lejki') etc.). When EN feature items got EN-slug URLs (/en/services/advertising instead of /uslugi/reklamy), the pattern matching silently failed — cards vanished from filtered tabs. No error, no warning, just empty tabs. We had to duplicate every URL pattern check with EN equivalents. This is a cascading effect of slug localization that's nearly impossible to anticipate.
The Numbers
~30 files modified across the project
~1800 lines of new translations (service details, reviews, knowledge cards, portfolio tags)
11 bidirectional slug mappings maintained in 5 different places
29 customer review translations
8 knowledge card translations (from PL blog takeaways)
6 CMS content inserts (home page, FAQ items, feature items) via direct D1 SQL
1 new interactive game localization (HangmanGame with separate word list, keyboard, and UI)
What Would Have Helped
The locale auto-detection actually working — this is the bug. Would have saved us hours of debugging why the EN homepage showed Polish content.
A warning when locale falls back — even a console.warn("EmDash: locale not detected from request context, falling back to defaultLocale 'pl'") would have saved us significant debugging time.
MCP content_create supporting locale/translation_group — we had to use raw D1 SQL for all CMS content creation because MCP doesn't expose these fields.
Documentation on i18n with Cloudflare Workers — the current docs show i18n working, but don't mention CF Workers compatibility or the need for explicit locale params.
A getEmDashEntry overload that accepts locale as a string — something like getEmDashEntry("pages", "home", "en") for the common case, rather than the options object.
Summary
The locale auto-detection bug is real and affects any EmDash site on Cloudflare Workers with multiple locales. The workaround (explicit locale param) is simple but you have to know about it — and the silent fallback makes it hard to catch.
More broadly, i18n for a real EmDash site is a massive undertaking that goes far beyond what the CMS layer handles. The CMS provides the foundation (locale column, translation_group), but everything above it — URL architecture, component i18n, SEO parity, slug translation, navigation — is entirely on the developer. This isn't necessarily a problem (EmDash is a CMS, not an i18n framework), but the locale detection bug at the foundation makes the whole tower unstable.
We're happy to provide code examples, migration scripts, or further details if helpful. This was a production localization of a real business site and we learned a lot along the way.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
EmDash i18n:
getEmDashEntry/getEmDashCollectiondon't auto-detect locale on Cloudflare Workers SSREnvironment
i18nconfigured (defaultLocale: "pl",locales: ["pl", "en"],prefixDefaultLocale: false)wrangler deployvia Astro Cloudflare adapterThe Bug
getEmDashEntryandgetEmDashCollectionare documented to automatically resolve the current locale from the Astro i18n request context (via ALS — AsyncLocalStorage). On Cloudflare Workers SSR, this does not work. Pages under/en/receive PL content instead of EN content.Reproduction
Workaround
Passing
localeexplicitly works:Root Cause (suspected)
Looking at EmDash source,
getEmDashCollectionUncachedresolves locale in this priority order:filter.localeparamctx?.localedefaultLocalefrom i18n configOn Cloudflare Workers, step 2 fails — the ALS context either isn't propagated to the D1 query layer, or Astro's i18n middleware doesn't set it in the ALS store that EmDash reads from. The result: every query silently falls back to
defaultLocale("pl"), and your EN pages render PL content with zero errors or warnings.This is particularly dangerous because it fails silently — no error thrown, no console warning. The page renders fine, just in the wrong language. We only caught it because a human looked at the live site.
Context: The Full Scope of Localizing an EmDash-Powered Site
We want to share the full picture of what i18n on EmDash actually looks like in production, because this bug was just one piece of a much larger puzzle. We localized a complete Polish business website (https://chcedointernetu.pl) to English — a real production site, not a toy project. This took multiple days of work and touched virtually every layer of the stack.
Starting Point
The site was built as a Polish-only website. When EmDash's i18n migration (
019_i18n.ts) ran, it addedlocaleandtranslation_groupcolumns to allec_*tables and backfilled existing rows withlocale = 'pl'. So we started from a state where:locale: 'pl'defaultLocale: "pl",locales: ["pl", "en"],prefixDefaultLocale: false)[slug].astrocatch-all page rendered CMS pages (consultations, privacy policy, AI ethics declaration) viagetEmDashEntry("pages", slug)+ Portable TextThis means the localization effort was additive — we had to build the entire EN layer from scratch on top of an existing, fully-functional PL site, without breaking anything.
The CMS Parity Problem
An important issue we discovered late: on the PL side, pages like consultations, privacy policy, and AI ethics declaration are CMS-driven — they live in
ec_pageswith Portable Text content, rendered by a dynamic[slug].astroroute. This means Konrad can edit them in the EmDash admin panel.But on the EN side, we created those same pages as hardcoded
.astrofiles (src/pages/en/consultations.astro,src/pages/en/privacy-policy.astro, etc.) with translations baked into the template. This breaks the CMS editing model — the EN pages can't be edited from the admin panel.The proper solution is an EN catch-all
src/pages/en/[slug].astrothat loads CMS pages with{ locale: "en" }, plus EN CMS entries for each page. We did this correctly for the homepage (created an EN entry inec_pageswith all 60+ fields translated). But doing it for every page is a significant effort, especially when Portable Text content needs to be translated block by block.This is another area where EmDash could help: a "duplicate page to another locale" feature in the admin panel, or MCP tooling that properly handles
localeandtranslation_groupfields during content creation.What We Had to Localize
This wasn't "just translate the text." Here's what the project actually involved:
1. CMS Content Layer (D1)
ec_pageswithlocale: 'en'andtranslation_grouplinking to the PL original. ~60 fields translated (hero, stats, sections, contact, meta).ec_faq_itemswith matchingtranslation_groupreferences.ec_feature_itemswith EN titles, descriptions, and EN-slug URLs./en/services/strony-www→/en/services/websites).All of this required direct D1 SQL because EmDash's MCP
content_createdoesn't reliably setlocale/translation_groupfields (separate issue).2. Page-Level Translations (Astro Components)
src/pages/en/— each one a careful translation that preserves the same component structure as the PL version.en-service-translations.tsfile), because the CMS stores PL content and the EN pages need to override it.3. URL Architecture
This was surprisingly complex:
/en/services/strony-wwwlooks broken). Built a bidirectional slug mapping system:strony-www↔websitessklepy-internetowe↔e-commercewizytowka-google↔google-business-profilegry-marketingowe↔marketing-games/en/services/redirect to EN slugs.ServicesGrid,Features, mega-menu, related services — all needed to translate/uslugi/strony-www→/en/services/websiteswhenlang="en"./uslugi→/services, but also slug translation within sub-routes).4. SEO & Structured Data
Every page needed SEO parity between PL and EN:
<link rel="alternate" hreflang="pl|en">with translated URLs.hasOfferCatalogwith EN service URLs,inLanguage: "en"on EN pages,datePublished/dateModifiedon homepages.og:localeandog:locale:alternateset correctly per language.5. Component-Level i18n
Components that render on both PL and EN pages needed
langprop support:isEn.6. Navigation & Footer
7. Other Touchpoints We Didn't Expect
Things that surprised us:
/darmowy-audyt-geo. EN pages needed to rewrite these to/en/free-geo-auditon the client side./case-studies/sluginstead of/en/case-studies/slug.datePublishedcitability signal), which is a PL-only feature but the infrastructure change touched shared components.Features.astrocomponent categorizes cards into tabs (Web, Marketing, AI) by matching URL patterns (url.includes('reklamy'),url.includes('lejki')etc.). When EN feature items got EN-slug URLs (/en/services/advertisinginstead of/uslugi/reklamy), the pattern matching silently failed — cards vanished from filtered tabs. No error, no warning, just empty tabs. We had to duplicate every URL pattern check with EN equivalents. This is a cascading effect of slug localization that's nearly impossible to anticipate.The Numbers
What Would Have Helped
The locale auto-detection actually working — this is the bug. Would have saved us hours of debugging why the EN homepage showed Polish content.
A warning when locale falls back — even a
console.warn("EmDash: locale not detected from request context, falling back to defaultLocale 'pl'")would have saved us significant debugging time.MCP
content_createsupportinglocale/translation_group— we had to use raw D1 SQL for all CMS content creation because MCP doesn't expose these fields.Documentation on i18n with Cloudflare Workers — the current docs show i18n working, but don't mention CF Workers compatibility or the need for explicit locale params.
A
getEmDashEntryoverload that accepts locale as a string — something likegetEmDashEntry("pages", "home", "en")for the common case, rather than the options object.Summary
The locale auto-detection bug is real and affects any EmDash site on Cloudflare Workers with multiple locales. The workaround (explicit
localeparam) is simple but you have to know about it — and the silent fallback makes it hard to catch.More broadly, i18n for a real EmDash site is a massive undertaking that goes far beyond what the CMS layer handles. The CMS provides the foundation (locale column, translation_group), but everything above it — URL architecture, component i18n, SEO parity, slug translation, navigation — is entirely on the developer. This isn't necessarily a problem (EmDash is a CMS, not an i18n framework), but the locale detection bug at the foundation makes the whole tower unstable.
We're happy to provide code examples, migration scripts, or further details if helpful. This was a production localization of a real business site and we learned a lot along the way.
Site: https://chcedointernetu.pl (PL) / https://chcedointernetu.pl/en (EN)
Stack: Astro 6 + EmDash 0.12 + Cloudflare Workers (D1 + R2)
Beta Was this translation helpful? Give feedback.
All reactions