Skip to content

Commit b1b4fb4

Browse files
fix(core): apply localeCode validation to taxonomy/menu locale filter (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>
1 parent 960d5ba commit b1b4fb4

3 files changed

Lines changed: 9 additions & 2 deletions

File tree

.changeset/localecode-preserve-casing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"emdash": patch
33
---
44

5-
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 lowercased the value, but config locales, the stored `locale` column, and the public query path all preserve the original casing, so the filter matched nothing. Validation stays case-insensitive; the value is now preserved verbatim. Closes #1551.
5+
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 lowercased the value, but config locales, the stored `locale` column, and the public query path all preserve the original casing, so the filter matched nothing. Validation stays case-insensitive; the value is now preserved verbatim. The taxonomy and menu endpoints' shared `?locale=` filter now applies the same BCP-47 validation, so a malformed locale is rejected with a clear error instead of silently matching nothing. Closes #1551.

packages/core/src/api/schemas/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const localeCode = z.string().regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "In
6464
/** Shared `?locale=xx` query shape for endpoints that filter by locale. */
6565
export const localeFilterQuery = z
6666
.object({
67-
locale: z.string().min(1).optional(),
67+
locale: localeCode.optional(),
6868
})
6969
.meta({ id: "LocaleFilterQuery" });
7070

packages/core/tests/unit/api/schemas.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
updateFieldBody,
99
httpUrl,
1010
localeCode,
11+
localeFilterQuery,
1112
mediaUploadUrlBody,
1213
DEFAULT_MAX_UPLOAD_SIZE,
1314
} from "../../../src/api/schemas/index.js";
@@ -157,6 +158,12 @@ describe("localeCode validator (#1551)", () => {
157158
const result = contentCreateBody.parse({ data: { title: "Hi" }, locale: "pt-BR" });
158159
expect(result.locale).toBe("pt-BR");
159160
});
161+
162+
it("localeFilterQuery keeps the ?locale= casing and rejects malformed values", () => {
163+
expect(localeFilterQuery.parse({ locale: "zh-TW" }).locale).toBe("zh-TW");
164+
expect(localeFilterQuery.parse({}).locale).toBeUndefined();
165+
expect(() => localeFilterQuery.parse({ locale: "_invalid_" })).toThrow();
166+
});
160167
});
161168

162169
describe("httpUrl validator", () => {

0 commit comments

Comments
 (0)