Skip to content

fix(core): keep term translations under their term's parent - #3368

Merged
ascorbic merged 1 commit into
emdash-cms:mainfrom
danielmlr:fix/term-translation-parent
Sep 24, 2026
Merged

ascorbic merged 1 commit into
emdash-cms:mainfrom
danielmlr:fix/term-translation-parent

Conversation

@danielmlr

Copy link
Copy Markdown
Contributor

What does this PR do?

Keeps a translated term under the same parent as the term it translates. Creating a translation stored whatever parent the caller passed, so one created without a parent became a root term in its locale while the term stayed nested elsewhere. The REST API, MCP, seed apply and the WordPress import all create translations that way.

A translation created without a parent now takes its term's parent and position. One created under another parent moves the term in every locale, as a parent update already does. An update naming the parent its row already has now also moves translations left elsewhere, so existing drift can be repaired from any locale. The admin already creates translations under the source's parent, so that path is unchanged. Logged-out routes run no new query.

Part of #2525. Independent of #3360, which covers taxonomy definitions; either can merge first.

Type of change

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

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. (n/a: no admin UI change)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (n/a: bug fix)
  • I have included screenshots below if this PR changes the UI (n/a: no UI change)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5.5

Screenshots / test output

Not applicable (no UI change). Four of the five new tests in term-reorder.test.ts and apply.test.ts failed before the change; the fifth pins the cycle check that a translation's move now relies on. The term-reorder.test.ts ones run on SQLite and PostgreSQL.

A term's parent and position belong to its translation group: updating
the parent through any locale already moves every locale. Creating a
translation did not follow that rule. `TaxonomyRepository.create()` with
`translationOf` stored whatever parent the caller passed, so a
translation created without one became a root term in its locale while
the term stayed nested in the others. The REST API, MCP, seed apply and
the WordPress import all reach that path without a parent.

A translation created without a parent now takes the group's parent and
position. One created under a different parent moves the whole group
there, as an update does, so the tested case of translating a term
straight under another parent keeps working and no longer splits the
term across parents.

An update also skipped the group write when the addressed row already
had the requested parent, so a locale left under another parent could
not be repaired from the locale that was right. It now writes the group
whenever any row differs.
@changeset-bot

changeset-bot Bot commented Sep 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d94e447

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 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

@pkg-pr-new

pkg-pr-new Bot commented Sep 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-test

npm i https://pkg.pr.new/@emdash-cms/plugin-test@3368

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-loader

npm i https://pkg.pr.new/@emdash-cms/registry-loader@3368

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3368

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: d94e447

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

@danielmlr
danielmlr marked this pull request as ready for review September 24, 2026 01:08
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Sep 24, 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 a real data-integrity problem: translations and their source term should share one parent/position, and the previous code let a translation become a root term (or land anywhere the caller specified). The change centralizes the invariant in TaxonomyRepository — a translation with no parentId inherits the source’s parent and sort order, an explicit different parent moves the whole translation group, and an update that names the current parent now repairs drifted siblings. The cycle check is correctly extended to create-with-translation so the move-the-group behavior can’t introduce cycles. All four code paths called out in the description (REST, MCP, plugin API, seed, WordPress import) flow through TaxonomyRepository.create/handleTermCreate and inherit the fix; no per-caller changes were needed.

I read the diff and the changed files end-to-end, traced the callers through the REST route, MCP tool, plugin context, seed apply engine, and WordPress WXR importer, and checked the new tests. The implementation uses parameterized Kysely queries throughout, adds no new SELECT on logged-out routes (the only new read, groupHasOtherParent, runs on authenticated updates), and invalidates the taxonomy caches on writes. The regression tests exercise the create-inherit, create-move-group, update repair-drift, and translation-under-descendant rejection behaviors, including one seed-specific case.

I found no logic bugs, no SQL-injection or auth regressions, no missing locale filters, no logged-out query-count increase, and no comment or changeset issues that would mislead a reader. The prose changes in the public docs and changeset accurately describe the new behavior.

@emdashbot emdashbot Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Sep 24, 2026
@ascorbic
ascorbic merged commit 4a69cc6 into emdash-cms:main Sep 24, 2026
53 checks passed
@emdashbot emdashbot Bot mentioned this pull request Sep 24, 2026
@danielmlr
danielmlr deleted the fix/term-translation-parent branch September 24, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants