Skip to content

fix: preserve taxonomy assignments across locale fallbacks - #2508

Merged
ascorbic merged 6 commits into
mainfrom
codex/fix-taxonomy-locale-fallback
Aug 17, 2026
Merged

fix: preserve taxonomy assignments across locale fallbacks#2508
ascorbic merged 6 commits into
mainfrom
codex/fix-taxonomy-locale-fallback

Conversation

@ascorbic

@ascorbic ascorbic commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

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

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Codex (GPT-5)

Screenshots / test output

  • pnpm typecheck
  • pnpm lint
  • pnpm lint:json | jq '.diagnostics | length'0
  • Core affected suite: 18 files, 163 tests passed
  • Admin affected suites: taxonomy/sidebar, settings panel, editor, router, and cache coverage passed
  • SQLite and D1 query-count harnesses: all logged-out route counts unchanged; SQL-text snapshots updated for the intentional folded-query change
  • Arabic browser QA: dir="rtl", lang="ar", locale row and implicit-English warning verified visually, with no page errors

Try 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.

Copilot AI lite review requested due to automatic review settings August 16, 2026 14:34
@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dab936f

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke 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/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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 16, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache dab936f Aug 17 2026, 09:26 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 16, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground dab936f Aug 17 2026, 09:28 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 16, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do dab936f Aug 17 2026, 09:30 AM

@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 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.

Comment thread packages/admin/src/components/TaxonomySidebar.tsx

Copilot AI 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.

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 contentLocale metadata 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.

Comment thread packages/core/src/taxonomies/index.ts
Comment thread packages/core/src/loader.ts Outdated
Comment thread packages/core/src/api/handlers/taxonomies.ts
@github-actions github-actions Bot added the review/awaiting-author Reviewed; waiting on the author to respond label Aug 16, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2508

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: dab936f

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This 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.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@github-actions github-actions Bot added the review/needs-rereview Author pushed changes since the last review label Aug 16, 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 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:

  1. Unused locale query paramfetchEntryTerms and setEntryTerms in TaxonomySidebar no longer append a locale query parameter to the entry-terms route; the route resolves the entry's locale server-side from the stored content row.
  2. Fallback-locale badges in tag suggestionsTermLocaleBadge now 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.

Comment thread packages/admin/src/components/TaxonomySidebar.tsx
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@github-actions github-actions Bot added needs-rebase and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 16, 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 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.

Comment thread packages/admin/src/components/TaxonomySidebar.tsx
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@github-actions github-actions Bot removed the review/needs-rereview Author pushed changes since the last review label Aug 16, 2026
@ascorbic
ascorbic force-pushed the codex/fix-taxonomy-locale-fallback branch from 8b8a428 to 113f1ba Compare August 16, 2026 17:11
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 16, 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 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-637

    The same createTermMutation powers both term creation paths. A failure in the hierarchical branch is rendered inline below the “Add new category” button, but TagInput does not receive the error, so flat-tag creation failures leave no visible feedback.

    Pass the mutation error into TagInput and render it below the autocomplete, or add an onError toast to createTermMutation so 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}
    				/>
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 16, 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 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-636

    The same createTermMutation powers both term-creation paths. Hierarchical creation failures are rendered inline at line 617, but TagInput does not receive the error, so flat-tag creation failures leave no visible feedback in the UI.

    Add a createError prop to TagInput, 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 TagInput component's props to accept createError?: Error | null and render the message under the autocomplete.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 16, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 8287038 Aug 16 2026, 05:38 PM

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed needs-rebase labels Aug 16, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Addressed the latest EmDashBot suggestion in acae411: TagInput now receives createTermMutation.error and renders flat-tag creation failures below the Kumo autocomplete, matching hierarchical creation feedback. Added a failing-response browser regression test; sidebar/cache suites pass 22/22, admin typecheck and full lint pass, and final JSON lint is 0.

@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 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 selectEntryTermRows and 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 with ROLE_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. marginInlineStart in the checkbox tree).
  • Locale filtering on content tables is preserved; the pivot join uses content.translation_group so assignments span variants.
  • Changeset is present for both affected packages.

No further findings to report.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 16, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
ascorbic force-pushed the codex/fix-taxonomy-locale-fallback branch from acae411 to dab936f Compare August 17, 2026 09:23
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Aug 17, 2026
@ascorbic
ascorbic enabled auto-merge (squash) August 17, 2026 09:27
@ascorbic
ascorbic merged commit 5c216c2 into main Aug 17, 2026
47 checks passed
@ascorbic
ascorbic deleted the codex/fix-taxonomy-locale-fallback branch August 17, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Taxonomy assignments silently disappear when content and terms use different locales

2 participants