fix: preserve taxonomy assignments across locale fallbacks - #2508
Conversation
🦋 Changeset detectedLatest commit: dab936f 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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | dab936f | Aug 17 2026, 09:26 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | dab936f | Aug 17 2026, 09:28 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | dab936f | Aug 17 2026, 09:30 AM |
There was a problem hiding this comment.
This PR takes the right approach to #2451: taxonomy assignments are stored on the shared translation group and resolved to a concrete term at read time (entry locale first, configured default second), while the admin UI surfaces fallback terms and unresolved groups rather than hiding them. The folded hydration change keeps public content reads at a single query round-trip, and the new contentLocale metadata in the manifest is additive.
I read the diff and the full changed files in core/admin, traced the new repository methods, the loader fold, the admin sidebar, and the settings panel. The SQL uses Kysely parameterization and sql.ref() consistently, new admin strings are Lingui-wrapped, and the Tailwind is logical. Tests cover exact fallback, missing-default fallback, no arbitrary fallback, unresolved groups, manifest metadata, and the loader fold. Overall the code is clean and the change fits EmDash's architecture.
I have two non-blocking suggestions: one about a route/API contract that accepts a locale query param it no longer uses, and one about surfacing fallback locale badges in tag-picker suggestions so the "resolved term keeps its actual locale" contract is visible everywhere terms are offered.
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes locale handling for taxonomy assignments by keeping assignments locale-neutral while resolving a single display term deterministically (entry locale first, then configured default), and surfacing unresolved groups + content-locale policy to the admin UI.
Changes:
- Update term hydration (folded queries + runtime helpers) to resolve one term per translation group using preferred-locale → default-locale fallback.
- Extend APIs and admin UI to expose term locale, unresolved assignment groups, and an action to create entry-locale term translations.
- Add manifest
contentLocalemetadata and tests covering implicit English defaults and fallback behavior.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/query-counts.queries.d1.json | Updates SQL snapshots for folded hydration query changes while preserving query counts. |
| packages/core/tests/unit/runtime/manifest-build.test.ts | Adds coverage for manifest.contentLocale (implicit vs configured default). |
| packages/core/tests/integration/taxonomies/taxonomy-locale-terms.test.ts | Adds integration coverage for fallback term resolution and unresolved assignment payloads. |
| packages/core/tests/integration/loader-fold.test.ts | Verifies folded hydration picks default-locale terms and avoids arbitrary-locale fallbacks. |
| packages/core/src/taxonomies/index.ts | Implements preferred→default locale resolution for entry term hydration. |
| packages/core/src/query.ts | Updates hydration comments/behavior documentation around preferred locale handling. |
| packages/core/src/loader.ts | Updates folded hydration subquery to resolve preferred→default locale terms. |
| packages/core/src/emdash-runtime.ts | Surfaces contentLocale in manifest using virtual or runtime i18n config. |
| packages/core/src/database/repositories/taxonomy.ts | Adds resolved listing and assignment-resolution helpers with fallback + unresolved metadata. |
| packages/core/src/astro/types.ts | Extends manifest type with optional contentLocale. |
| packages/core/src/astro/routes/api/taxonomies/[name]/terms/index.ts | Threads resolveFallback query flag into term listing handler. |
| packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts | Returns resolved terms plus unresolved assignment groups and locale metadata. |
| packages/core/src/api/schemas/taxonomies.ts | Adds resolveFallback query schema for term listing. |
| packages/core/src/api/handlers/taxonomies.ts | Implements resolved-by-group term listing and tree building in fallback mode. |
| packages/admin/tests/components/TaxonomySidebar.test.tsx | Adds UI coverage for fallback badge, unresolved assignment preservation, and resolved picker requests. |
| packages/admin/tests/components/ContentSettingsPanel.test.tsx | Adds UI coverage for stored content locale display and implicit-English warning. |
| packages/admin/src/lib/api/client.ts | Extends admin manifest typing to include contentLocale. |
| packages/admin/src/components/TaxonomySidebar.tsx | Displays fallback badges, unresolved assignments, and supports creating translations from unresolved groups. |
| packages/admin/src/components/ContentSettingsPanel.tsx | Displays content locale and shows implicit-English default warning banner. |
| .changeset/tidy-taxonomy-locales.md | Documents the patch behavior changes for taxonomy locale resolution and content-locale visibility. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
@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: |
Scope checkThis PR changes 1,067 lines across 21 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
There was a problem hiding this comment.
This revision still takes the right approach to #2451: taxonomy assignments stay on the shared translation group and are resolved to a concrete term at read time (entry locale first, configured default second), while the admin surfaces fallback terms and unresolved groups. I re-read the diff and the full changed files in packages/core and packages/admin, traced the new repository methods, the loader fold, the manifest/metadata additions, and the updated query-count snapshots.
Both non-blocking suggestions from the previous review are now addressed:
- Unused
localequery param —fetchEntryTermsandsetEntryTermsinTaxonomySidebarno longer append alocalequery parameter to the entry-terms route; the route resolves the entry's locale server-side from the stored content row. - Fallback-locale badges in tag suggestions —
TermLocaleBadgenow appears in the flat tag-picker dropdown, in selected tags, and next to hierarchical checkbox labels, so the "resolved term keeps its actual locale" contract is visible everywhere terms are offered.
I found one new non-blocking issue: the inline "Create [locale] translation" action is shown unconditionally, but the backing POST /_emdash/api/taxonomies/:name/terms/:slug/translations route requires taxonomies:manage (EDITOR). Authors editing their own content can see and click the button, but the server will reject the request. ContentSettingsPanel already has currentUser, so the permission can be threaded down and the button gated. The existing create-term button has the same gap; ideally both would share a canManageTaxonomies flag.
There was a problem hiding this comment.
This revision is still the right fix for #2451: taxonomy assignments stay on the shared translation group, read-time resolution prefers the entry locale and falls back to the configured default, and the admin surfaces fallback badges and unresolved assignments without adding logged-out round-trips. The folded-query change keeps the hydration subquery inside the existing SELECT list, and the updated query-count snapshots appear to reflect only the intentional SQL-text change.
The two prior non-blocking items are addressed: fetchEntryTerms / setEntryTerms no longer pass a client-side locale query param, and TermLocaleBadge now appears in selected tags, flat suggestions, and hierarchical checkbox labels.
I have one remaining non-blocking concern. TaxonomySidebar renders state-changing create actions (new term, new category, and the new "Create [locale] translation" button) for every authenticated editor user, but the backing routes require the taxonomies:manage permission, which maps to Role.EDITOR. Authors editing their own content (content:edit_own) can click those buttons and will receive a 403. ContentSettingsPanel already has currentUser, so the fix is to thread a canManageTaxonomies flag down and gate both create actions. I did not run tests, lint, or the query-count harness; this review is static only.
8b8a428 to
113f1ba
Compare
There was a problem hiding this comment.
This is the right fix for #2451 and the prior review items are addressed: taxonomy assignments stay on the shared translation group, read-time resolution uses the entry locale then falls back to the configured default, the admin surfaces fallback badges and unresolved assignments, and the public folded query keeps the hydration subquery inside the existing SELECT list so logged-out routes don’t pay an extra round trip. Static review found the code well-structured and consistent with AGENTS.md conventions (Lingui-wrapped strings, logical Tailwind/inline-start indentation, permission checks in routes, locale-aware queries, changeset present).
I did not run tests, lint, or the query-count harness; the PR description’s counts/static claims are taken as unverified. The only remaining non-blocking issue is a UX gap in the flat-tag picker: term creation errors are surfaced for hierarchical taxonomies but not for flat taxonomies.
Findings
-
[suggestion]
packages/admin/src/components/TaxonomySidebar.tsx:626-637The same
createTermMutationpowers both term creation paths. A failure in the hierarchical branch is rendered inline below the “Add new category” button, butTagInputdoes not receive the error, so flat-tag creation failures leave no visible feedback.Pass the mutation error into
TagInputand render it below the autocomplete, or add anonErrortoast tocreateTermMutationso both branches announce failures consistently.<TagInput terms={terms} selectedIds={selectedIds} onAdd={handleAdd} onRemove={handleRemove} onCreate={(label) => createTermMutation.mutate(label)} isCreating={createTermMutation.isPending} createError={createTermMutation.error} label={taxonomy.label} entryLocale={resolvedEntryLocale} canCreate={canManageTaxonomies} />
There was a problem hiding this comment.
This remains the right fix for #2451 and the re-review did not surface any new blocking issues. Assignments stay on translation groups, read-time resolution prefers the entry locale and falls back to the configured default, the admin surfaces fallback badges and unresolved assignments with a translation CTA, and the public loader folds the fallback logic into the existing hydration subquery so logged-out routes shouldn't pay an extra round trip. AGENTS.md conventions look consistent (Lingui-wrapped strings, logical Tailwind properties, permission checks in API routes, locale-aware queries, and a present changeset).
The one previously raised item is still open: the flat-tag picker still does not surface createTermMutation errors, while the hierarchical category creation path renders the same mutation's error inline. Passing the error into TagInput and rendering it there (or below the autocomplete) fixes the asymmetry and gives flat-tag creation failures visible feedback.
I did not run the test suite, linter, or query-count harness; the PR description's static claims are taken as unverified.
Findings
-
[suggestion]
packages/admin/src/components/TaxonomySidebar.tsx:626-636The same
createTermMutationpowers both term-creation paths. Hierarchical creation failures are rendered inline at line 617, butTagInputdoes not receive the error, so flat-tag creation failures leave no visible feedback in the UI.Add a
createErrorprop toTagInput, pass the mutation error, and render it below the autocomplete so both paths surface failures consistently.<TagInput terms={terms} selectedIds={selectedIds} onAdd={handleAdd} onRemove={handleRemove} onCreate={(label) => createTermMutation.mutate(label)} isCreating={createTermMutation.isPending} createError={createTermMutation.error} label={taxonomy.label} entryLocale={resolvedEntryLocale} canCreate={canManageTaxonomies} />Also update the
TagInputcomponent's props to acceptcreateError?: Error | nulland render the message under the autocomplete.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 8287038 | Aug 16 2026, 05:38 PM |
|
Addressed the latest EmDashBot suggestion in acae411: |
There was a problem hiding this comment.
This is the right fix for #2451 and the re-review found no new blocking issues. Assignments stay on translation groups, read-time resolution prefers the entry locale and falls back to the configured default, the admin surfaces fallback badges and unresolved assignments with a translation CTA, and the public loader folds the fallback logic into the existing hydration subquery so logged-out routes do not pay an extra round trip.
The one item raised in the previous review is now addressed: TagInput accepts a createError prop and renders the mutation error below the autocomplete, matching the hierarchical category-creation path. A test was added to cover that behavior.
I checked the changed source and tests statically (no test/lint/build runs):
- SQL in
selectEntryTermRowsand the loader fold uses validated identifiers and parameterized values; no injection path. - Authorization is consistent: the server still requires
taxonomies:manage(EDITOR) to create terms, and the admin now gates creation UI withROLE_EDITOR, so the UI and API line up. - API responses remain
apiSuccess({ data: ... })shaped. - User-visible strings are Lingui-wrapped.
- Tailwind uses logical properties (e.g.
marginInlineStartin the checkbox tree). - Locale filtering on content tables is preserved; the pivot join uses
content.translation_groupso assignments span variants. - Changeset is present for both affected packages.
No further findings to report.
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. |
acae411 to
dab936f
Compare
What does this PR do?
Keeps taxonomy assignments locale-neutral while resolving their displayed term deterministically: the entry locale first, then the configured default locale. The resolved term retains its actual locale so the editor can label fallback values. Assignments with neither variant remain visible with their available locales and an inline action to create the entry-locale translation; no arbitrary-locale display fallback is used.
The content editor now shows the entry's stored content locale separately from the admin interface language and warns when EmDash is using its implicit English default. Public hydration applies the same fallback inside the existing folded query, preserving logged-out query counts.
Closes #2451
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
pnpm typecheckpnpm lintpnpm lint:json | jq '.diagnostics | length'→0dir="rtl",lang="ar", locale row and implicit-English warning verified visually, with no page errorsTry this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
codex/fix-taxonomy-locale-fallback. Updated automatically when the playground redeploys.