Description
content_taxonomies links the two ends of a content↔term assignment with inconsistent locale semantics, and the asymmetry causes per-locale tag drift.
The junction row is (collection, entry_id, taxonomy_id):
taxonomy_id is locale-agnostic. Since migration 036_i18n_menus_and_taxonomies, it stores the term's translation_group (not a taxonomies row id). The old FK was deliberately dropped so one junction row covers every locale translation of the term. Reads confirm it — TaxonomyRepository joins taxonomies.translation_group = content_taxonomies.taxonomy_id.
entry_id is per-locale. It stores a single content row's id. Because content tables are row-per-locale (migration 019_i18n), each translation of a post is a distinct row with its own id, so a junction row attaches a term to one locale's entry only, not to the content translation_group.
So a content_taxonomies row links one locale's entry to a locale-collapsed term concept. The two ends disagree on what "the same thing across locales" means.
Why this is a bug (observable consequence): term assignments are only synced across content translations once, at creation time, via TaxonomyRepository.copyEntryTerms ("Used when creating a translation of a post so the new translation inherits the source's term assignments"). It's a one-time snapshot. After that the locale entries drift:
- Create an English post, tag it
News. Create its Arabic translation → it inherits News (copy at creation). Good so far.
- Later, edit the English post and add
Sports. The Arabic translation does not receive Sports — its entry_id differs and nothing re-syncs.
- Symmetrically, removing a tag from one locale leaves it on the sibling locale.
The result is that "the same content in another language" can carry a divergent set of terms with no UI/runtime affordance reconciling them, even though the term side was specifically designed to be locale-agnostic. Either both ends should be group-keyed (assignments shared across the content translation_group), or the divergence should be an explicit, surfaced feature rather than silent drift from a creation-time copy.
This is a design/consistency bug rather than data loss — flagging for a decision on intended semantics.
Steps to reproduce
- Enable a second locale (e.g.
ar) and create a posts collection with a taxonomy (e.g. Category).
- Create an English post, assign term
News.
- Create an Arabic translation of that post (inherits
News via copyEntryTerms).
- Edit the English post; add term
Sports and save.
- Fetch terms for the Arabic translation (
getTermsForEntry(collection, arEntryId)).
- Observed: Arabic translation still has only
News; Sports never propagated.
Expected (or to be decided): assignments stay consistent across translations of the same content, mirroring the locale-agnostic term side — or per-locale divergence is an explicit, documented capability.
Environment
- emdash version: main (current
packages/core)
- Runtime: Node + SQLite and Cloudflare D1 (schema-level, dialect-independent)
- Relevant code:
packages/core/src/database/migrations/036_i18n_menus_and_taxonomies.ts (taxonomy_id → translation_group)
packages/core/src/database/repositories/taxonomy.ts (getTermsForEntry, setTermsForEntry, copyEntryTerms)
packages/core/src/database/migrations/019_i18n.ts (row-per-locale content)
Logs / error output
N/A — behavioral/design inconsistency, no error thrown.
Description
content_taxonomieslinks the two ends of a content↔term assignment with inconsistent locale semantics, and the asymmetry causes per-locale tag drift.The junction row is
(collection, entry_id, taxonomy_id):taxonomy_idis locale-agnostic. Since migration036_i18n_menus_and_taxonomies, it stores the term'stranslation_group(not ataxonomiesrow id). The old FK was deliberately dropped so one junction row covers every locale translation of the term. Reads confirm it —TaxonomyRepositoryjoinstaxonomies.translation_group = content_taxonomies.taxonomy_id.entry_idis per-locale. It stores a single content row's id. Because content tables are row-per-locale (migration019_i18n), each translation of a post is a distinct row with its own id, so a junction row attaches a term to one locale's entry only, not to the contenttranslation_group.So a
content_taxonomiesrow links one locale's entry to a locale-collapsed term concept. The two ends disagree on what "the same thing across locales" means.Why this is a bug (observable consequence): term assignments are only synced across content translations once, at creation time, via
TaxonomyRepository.copyEntryTerms("Used when creating a translation of a post so the new translation inherits the source's term assignments"). It's a one-time snapshot. After that the locale entries drift:News. Create its Arabic translation → it inheritsNews(copy at creation). Good so far.Sports. The Arabic translation does not receiveSports— itsentry_iddiffers and nothing re-syncs.The result is that "the same content in another language" can carry a divergent set of terms with no UI/runtime affordance reconciling them, even though the term side was specifically designed to be locale-agnostic. Either both ends should be group-keyed (assignments shared across the content translation_group), or the divergence should be an explicit, surfaced feature rather than silent drift from a creation-time copy.
This is a design/consistency bug rather than data loss — flagging for a decision on intended semantics.
Steps to reproduce
ar) and create apostscollection with a taxonomy (e.g.Category).News.NewsviacopyEntryTerms).Sportsand save.getTermsForEntry(collection, arEntryId)).News;Sportsnever propagated.Expected (or to be decided): assignments stay consistent across translations of the same content, mirroring the locale-agnostic term side — or per-locale divergence is an explicit, documented capability.
Environment
packages/core)packages/core/src/database/migrations/036_i18n_menus_and_taxonomies.ts(taxonomy_id→translation_group)packages/core/src/database/repositories/taxonomy.ts(getTermsForEntry,setTermsForEntry,copyEntryTerms)packages/core/src/database/migrations/019_i18n.ts(row-per-locale content)Logs / error output
N/A — behavioral/design inconsistency, no error thrown.