Skip to content

fix(core): preserve locale casing in API locale filter (#1551)#1572

Open
marcusbellamyshaw-cell wants to merge 2 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1551-localecode-preserve-casing
Open

fix(core): preserve locale casing in API locale filter (#1551)#1572
marcusbellamyshaw-cell wants to merge 2 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1551-localecode-preserve-casing

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the content and search APIs returning zero results when filtering by a locale with an uppercase region or script subtag (e.g. ?locale=zh-TW, pt-BR, zh-Hant).

The localeCode validator applied .transform(v => v.toLowerCase()), but config locales/defaultLocale, the stored locale column, and the public query path all preserve the original BCP-47 casing. SQLite/D1 string comparison is case-sensitive, so the lowercased filter matched nothing. The sibling localeFilterQuery (taxonomies/menus) never lowercased, so this was also an internal inconsistency.

Fix: drop the transform so the value is preserved verbatim. Validation stays case-insensitive (the /i regex is unchanged).

Closes #1551.

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • I have added a changeset (if this PR changes a published package)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

Screenshots / test output

New localeCode validator (#1551) describe in packages/core/tests/unit/api/schemas.test.ts: preserves zh-TW/pt-BR/zh-Hant, still validates case-insensitively, rejects malformed codes, and verifies contentListQuery/contentCreateBody keep the casing.

The `localeCode` validator lowercased the `?locale=` filter and create-body
locale, but config `locales`/`defaultLocale`, the stored `locale` column, and
the public query path all keep the raw BCP-47 casing. As a result the content
and search APIs returned zero rows for any locale with an uppercase region or
script subtag (e.g. zh-TW, pt-BR, zh-Hant).

Drop the `.toLowerCase()` transform so the value is preserved verbatim;
validation stays case-insensitive. This also matches the sibling
`localeFilterQuery` (taxonomies/menus), which never lowercased.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e288db7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jun 23, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix at the right layer. Lowercasing the locale in the shared schema was the source of the zero-results bug: queries like ?locale=zh-TW became zh-tw, which no longer matched rows stored under the canonical zh-TW casing. Dropping the .transform() while keeping the case-insensitive regex makes contentListQuery, contentCreateBody, and the search schemas preserve the caller’s casing, which matches how config locales/defaultLocale and the ec_* locale column store values.

I checked the call graph: localeCode is only used in packages/core/src/api/schemas/content.ts and search.ts. Their handlers/routes pass the parsed locale through verbatim, the content repository does an exact equality match on the locale column, and the search module does the same. No consumer was relying on the previous lowercased output. The new unit tests in schemas.test.ts cover the changed behavior, and the changeset is present and user-focused.

Two non-blocking completeness points:

  1. The old transform lowercased explicit locales on create, so existing rows can already be stored as zh-tw. Removing the transform fixes new queries, but those pre-existing lowercase rows may still be missed by canonical-uppercase filters unless a forward-only migration canonicalizes stored locale casing.
  2. Right below the changed localeCode, localeFilterQuery (used by taxonomies/menus) and the taxonomy/menu create bodies still accept any string for locale, so the API has inconsistent BCP-47 validation. Reusing localeCode there would preserve casing and tighten validation without affecting behavior.

Findings

  • [suggestion] packages/core/src/api/schemas/common.ts:62

    Removing the transform stops future lowercasing, but contentCreateBody previously lowercased every explicit locale. That means sites already have rows stored as zh-tw (from authors who passed zh-TW). After this patch, a canonical ?locale=zh-TW query will still miss those existing rows because the repository does an exact locale = 'zh-TW' match.

    Consider adding a forward-only migration that canonicalizes the locale column in every ec_* table against the configured locales (getI18nConfig().locales), case-insensitively, so the fix applies to existing data too.

  • [suggestion] packages/core/src/api/schemas/common.ts:64-69

    localeFilterQuery is described as the shared ?locale= shape, but it still uses a plain z.string() while contentListQuery and the search schemas now use the stricter localeCode. That leaves taxonomies/menus (and their create/update bodies, which also use z.string()) accepting arbitrary strings such as _invalid_ for a locale.

    Since localeCode no longer lowercases the value, reusing it here would tighten validation and keep locale handling consistent across the API:

    /** Shared `?locale=xx` query shape for endpoints that filter by locale. */
    export const localeFilterQuery = z
    	.object({
    		locale: localeCode.optional(),
    	})
    	.meta({ id: "LocaleFilterQuery" });
    
…emdash-cms#1551)

The shared `localeFilterQuery` (used by taxonomy and menu endpoints) still
used a plain `z.string()` while `contentListQuery` and the search schemas
adopted the stricter, casing-preserving `localeCode`. Reusing `localeCode`
here tightens BCP-47 validation and keeps locale handling consistent across
the API: a malformed `?locale=` value is now rejected instead of silently
matching zero rows. Addresses non-blocking review feedback on emdash-cms#1572.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jun 24, 2026
@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed the two completeness points:

✅ Suggestion #2localeFilterQuery tightening (done in e288db7): Switched the shared ?locale= filter to reuse localeCode, so the taxonomy/menu endpoints now get the same casing-preserving BCP-47 validation as contentListQuery and search. Confirmed all 25 call sites use it purely as an optional query filter, so the only behavior change is that a malformed locale (e.g. _invalid_) is now rejected with a clear error instead of silently matching zero rows. Added a unit test and a sentence to the changeset.

⏭️ Suggestion #1 — forward-only migration for pre-existing lowercase rows: Deliberately deferring this out of #1572. It's a real point, but rewriting the locale column across every ec_* table is a data migration with meaningfully larger blast radius than this API-layer fix, and it deserves its own PR + changeset + test coverage (including the case where a site legitimately uses lowercase locales). Keeping #1572 scoped to the query-path fix per the repo's scope-discipline guidance. I'll open a follow-up issue/PR to canonicalize stored locale casing against getI18nConfig().locales.

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1572

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1572

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1572

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1572

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1572

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1572

emdash

npm i https://pkg.pr.new/emdash@1572

create-emdash

npm i https://pkg.pr.new/create-emdash@1572

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1572

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1572

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1572

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1572

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1572

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1572

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1572

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1572

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1572

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1572

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1572

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1572

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1572

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1572

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1572

commit: e288db7

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor Author

Filed the deferred locale-canonicalization migration as #1599 for tracking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-rereview Author pushed changes since the last review size/M

2 participants