fix(core): preserve locale casing in API locale filter (#1551)#1572
fix(core): preserve locale casing in API locale filter (#1551)#1572marcusbellamyshaw-cell wants to merge 2 commits into
Conversation
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 detectedLatest commit: e288db7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
There was a problem hiding this comment.
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:
- 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. - Right below the changed
localeCode,localeFilterQuery(used by taxonomies/menus) and the taxonomy/menu create bodies still accept any string forlocale, so the API has inconsistent BCP-47 validation. ReusinglocaleCodethere would preserve casing and tighten validation without affecting behavior.
Findings
-
[suggestion]
packages/core/src/api/schemas/common.ts:62Removing the transform stops future lowercasing, but
contentCreateBodypreviously lowercased every explicit locale. That means sites already have rows stored aszh-tw(from authors who passedzh-TW). After this patch, a canonical?locale=zh-TWquery will still miss those existing rows because the repository does an exactlocale = 'zh-TW'match.Consider adding a forward-only migration that canonicalizes the
localecolumn in everyec_*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-69localeFilterQueryis described as the shared?locale=shape, but it still uses a plainz.string()whilecontentListQueryand the search schemas now use the stricterlocaleCode. That leaves taxonomies/menus (and their create/update bodies, which also usez.string()) accepting arbitrary strings such as_invalid_for a locale.Since
localeCodeno 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>
|
Thanks for the thorough review. Addressed the two completeness points: ✅ Suggestion #2 — ⏭️ Suggestion #1 — forward-only migration for pre-existing lowercase rows: Deliberately deferring this out of #1572. It's a real point, but rewriting the |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
|
Filed the deferred locale-canonicalization migration as #1599 for tracking. |
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
localeCodevalidator applied.transform(v => v.toLowerCase()), but configlocales/defaultLocale, the storedlocalecolumn, 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 siblinglocaleFilterQuery(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
/iregex is unchanged).Closes #1551.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
New
localeCode validator (#1551)describe inpackages/core/tests/unit/api/schemas.test.ts: preserveszh-TW/pt-BR/zh-Hant, still validates case-insensitively, rejects malformed codes, and verifiescontentListQuery/contentCreateBodykeep the casing.