fix(taxonomies): respect active locale in admin surfaces - #2342
fix(taxonomies): respect active locale in admin surfaces#2342fbartolitsch wants to merge 7 commits into
Conversation
Group translated definitions by logical identity before rendering admin navigation and editor choices. Scope visible term counts and cache entries to the resolved content locale.
🦋 Changeset detectedLatest commit: a19cfe4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 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 |
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
This is a focused bug fix with the right shape: normalize localized taxonomy definitions to one logical row per locale, then use that resolved locale in admin labels, editor choices, manifest metadata, and visible term counts. I read the full diff, the changed source files, and the relevant route/handler call sites.
Headline: the approach is sound, but the sidebar navigation only respects the active locale for the label — the taxonomy-management link it produces drops the ?locale= parameter, so clicking a DE item lands on the default-locale page. That undermines the stated goal and should be fixed before merge.
I also found a smaller edge where the API term-list/term-get handlers still scope visible counts off the lowest-locale taxonomy definition rather than the active-locale one. That is safe when translations share collections, but it is inconsistent with the public taxonomies/index.ts helpers and worth aligning.
No SQL-injection issues: the dynamic locale predicate is interpolated as a Kysely parameter. No new logged-out round-trips are introduced; the count query just gains a locale predicate and a locale-aware cache key. Tests cover the new resolver, editor filtering, manifest identity, and locale-scoped counts.
Findings
-
[needs fixing]
packages/admin/src/components/Sidebar.tsx:254The sidebar resolves the active-locale taxonomy label using
routeLocale, but the generated taxonomy nav items omit the locale, so the link navigates to e.g./taxonomies/coursewithout?locale=de. Clicking a German taxonomy in the sidebar will open the taxonomy-management route in the configured default/locale-less fallback, which partially defeats the PR's goal of respecting the active content locale on admin surfaces.Extend
NavItemto carry optional query params and append them inresolveItemPathso taxonomy links preserve the active locale:// in the manageItems construction ...getSidebarTaxonomies(manifest.taxonomies, routeLocale, manifest.i18n?.defaultLocale).map( (tax) => ({ to: "/taxonomies/$taxonomy" as const, label: tax.label, icon: getTaxonomyNavIcon(tax.name), params: { taxonomy: tax.name }, search: routeLocale ? { locale: routeLocale } : undefined, minRole: ROLE_EDITOR, }), ), // interface NavItem { ... search?: Record<string, string>; ... } function resolveItemPath(item: NavItem): string { let path = item.to; if (item.params) { for (const [key, value] of Object.entries(item.params)) { path = path.replace(`$${key}`, value); } } if (item.search && Object.keys(item.search).length > 0) { const q = new URLSearchParams(item.search).toString(); path += `?${q}`; } return path; }
-
[suggestion]
packages/core/src/api/handlers/taxonomies.ts:393handleTermListresolves the taxonomy withrequireTaxonomyDef(db, taxonomyName)(no locale), which returns the lowest-locale row, and then scopes visible counts todefCollections(lookup.def). If a taxonomy's translations declare differentcollections, the active-locale term list will count the wrong collections. The public helpergetTaxonomyDef(name, options)already resolves the active-locale definition.Suggestion: pass the resolved
localetorequireTaxonomyDefhere (and inhandleTermGetat line 659) so counts use the same definition the user is actually viewing:const lookup = await requireTaxonomyDef(db, taxonomyName, locale);
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
ascorbic
left a comment
There was a problem hiding this comment.
The direction is right and most of this is well built — the client-side resolveTaxonomyDefinitions fallback chain is exactly the right shape, the cache keys correctly grow a locale component, and the dialect-parameterized test extensions are thorough. Two things before merge: a server-side regression for partially-translated sites (inline below), and the changeset undersells a public-site behavior change.
On the changeset: the per-locale counts flow into getTaxonomyTerms, which is the public taxonomy widget API — multi-locale sites will see their live term counts change (from cross-locale totals to per-locale). I agree the new counts are the correct ones — the old totals didn't match what a visitor clicking through actually sees — but that's an observable public-site change dressed in admin-fix language. Say it plainly in the changeset, and I'd bump emdash to minor.
| const lookup = await requireTaxonomyDef(db, taxonomyName); | ||
| // use the active definition for its collection scope. | ||
| const locale = options.locale ? resolveConfiguredLocale(options.locale) : undefined; | ||
| const lookup = await requireTaxonomyDef(db, taxonomyName, locale); |
There was a problem hiding this comment.
This is a hard where locale = match with no fallback (requireTaxonomyDef filters exactly when a locale is passed), so a site with DE content but taxonomy defs only defined in EN — the normal state of any partially-translated site — now gets NOT_FOUND from the term list for DE entries, where it worked before this PR. handleTermGet has the milder version of the same problem: the lenient lookup fails, defCollections becomes [], and the count silently reads 0.
The client half of this PR already handles this correctly (resolveTaxonomyDefinitions: active locale → default → deterministic first). The server needs the same leniency — prefer the exact-locale def, fall back to any row of the group — since the def is only used for its collections scope here. Something like ordering by locale = ? DESC and taking the first row instead of filtering.
Worth a test pinning it: term list for a locale that has terms and content but no translated def row.
@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/registry-verification
@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: |
|
The query snapshots can be resolved by accepting main's version: they'll be updated once you push to CI |
There was a problem hiding this comment.
Re-review of PR #2342. The approach is sound: resolve localized taxonomy definitions to one logical row per locale, then use that resolved locale for sidebar labels/links, editor choices, manifest identity metadata, and count queries. The two findings from the previous review are both addressed — sidebar taxonomy links now carry ?locale= and the admin term-list/term-get handlers resolve the taxonomy definition for the active locale before counting.
I read the full diff, the changed files, and the relevant tests. Headline conclusion: the implementation is close, but the sidebar's active-state matching was not updated for links that now include query params, so taxonomy items in a non-default locale are never highlighted as active. That's a regression against the PR's goal and should be fixed before merge.
Everything else looks good: SQL is parameterized, count queries only add a locale predicate without extra round trips, manifest identity metadata includes id/locale/translationGroup, editor choices resolve to the entry locale, tests reproduce the new resolver/filtering/count behavior, and changesets cover both packages.
Findings
-
[needs fixing]
packages/admin/src/components/Sidebar.tsx:212-216Taxonomy nav items now include
?locale=...initemPathviaresolveItemPath, butisItemActivecomparescurrentPath(location.pathname, no query string) against the fullitemPath. That means/taxonomies/course?locale=denever matches/taxonomies/course, so the active taxonomy is not highlighted when a non-default locale is selected — undermining the PR's goal of respecting the active locale on admin surfaces.Strip the query string before matching, and add a regression test that calls
isItemActivewith a query-param-bearing path.function isItemActive(itemPath: string, currentPath: string): boolean { if (itemPath === "/") return currentPath === "/"; const path = itemPath.split("?")[0]; return currentPath === path || currentPath.startsWith(`${path}/`); }
Head branch was pushed to by a user without write access
|
Updated this branch with the upstream merge and adjusted the locale fallback tests. Focused taxonomy tests and lint pass locally. Full |
What does this PR do?
Summary
Scope
id,locale, andtranslationGroup).@emdash-cms/adminand minor changeset foremdash.Validation
pnpm formatandpnpm format:checkpnpm buildpnpm typecheckpnpm lintandpnpm lint:quickgit diff --cached upstream/main --checkRisk and rollback
translationGroup; legacy definitions without that metadata fall back to their taxonomy name.emdashchangeset is therefore minor.upstream/main; no history rewrite or force-push was used.Security/privacy impact
No new external calls, credentials, permissions, or personal-data processing are introduced.
Fixes #2224
Fixes #2338
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges are included.AI-generated code disclosure
Screenshots / test output
Targeted automated tests: