Skip to content

feat: bylines i18n support#1146

Merged
ascorbic merged 3 commits into
emdash-cms:mainfrom
MohamedH1998:feat/bylines-i18n
May 23, 2026
Merged

feat: bylines i18n support#1146
ascorbic merged 3 commits into
emdash-cms:mainfrom
MohamedH1998:feat/bylines-i18n

Conversation

@MohamedH1998
Copy link
Copy Markdown
Contributor

@MohamedH1998 MohamedH1998 commented May 22, 2026

What does this PR do?

Adds first-class i18n support to bylines, mirroring the row-per-locale model already in place for content (migration 019), and menus + taxonomies (migration 036, PR #916). Each _emdash_bylines row carries locale and translation_group; siblings sharing a translation_group are the same byline identity in different languages. _emdash_content_bylines.byline_id is remapped to store the referenced byline's translation_group, so a single credit survives content translations and resolves against the active locale at runtime.

Highlights

  • Migration 040_byline_i18n — adds locale + translation_group to _emdash_bylines. Idempotent, backfills via configured defaultLocale, partial unique on (translation_group, locale), widens (slug) to (slug, locale).
  • Strict per-locale credit hydration — hydrateBylines joins the junction against the sibling matching the entry's locale. No chain-walk for credits. Explicit credit at any locale suppresses author-inferred fallback (gated by hasContentBylines).
  • Identity lookups chain-walk — getBylineBySlug walks resolveLocaleChain via requestCached. Author pages for un-translated bylines render the default-locale identity. Deliberately different from credit hydration.
  • BylineRepository — strict per-locale (findMany, findBySlug, findById take locale). New: listTranslations, findByTranslationGroup, copyContentBylines, hasContentBylines / hasContentBylinesMany. setContentBylines dedupes by translation_group. delete is sibling-aware.
  • REST API — two new endpoints, requireDb on all routes, new bylines:read / bylines:manage permissions (same role thresholds as before):
    • GET /_emdash/api/admin/bylines/:id/translations
    • POST /_emdash/api/admin/bylines/:id/translations
    • GET /_emdash/api/admin/bylines?locale=xx filters strictly
    • POST /_emdash/api/admin/bylines accepts locale + translationOf
  • Admin UI
    • TranslationsPanel + LocaleSwitcher on the bylines manager — strict locale filtering, bookmarkable via ?locale=.
    • Byline picker on the content editor is locale-pinned to the entry's locale.
    • Empty state CTA links to /bylines?locale=… for inline creation in the right locale.
    • ContentEditor only sends bylines when the user touched the editor (bylinesTouched, mirrors slugTouched). Prevents silent wipe of copied credits on translation entries.
    • loadMoreMutation snapshots filters at click-time, discards stale in-flight results.
  • Locale validation — handlers validate locale against getI18nConfig()?.locales ("zz" → 400). Schemas reject empty-string locales (z.string().min(1).optional()).
    Closes #

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.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: i18n: Translatable site title, tagline, collection names, etc. #403

AI-generated code disclosure

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

Screenshots / test output

Screenshot 2026-05-22 at 15 47 51

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: 662cb36

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

This PR includes changesets to release 13 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/cloudflare Minor
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@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

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

This PR changes 4,431 lines across 30 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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 662cb36

ascorbic
ascorbic previously approved these changes May 22, 2026
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!!

@ascorbic
Copy link
Copy Markdown
Collaborator

/review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work — the strict-per-locale credit hydration + chain-walk identity lookup distinction is well-reasoned and the test coverage (migration up/down, D1-like FK semantics, dedupe-after-resolve, copy-on-translation, no-fallback-when-explicit-exists) is thorough. The migration backfill, idempotency, and rollback guards are exactly the kind of belt-and-braces I'd want for a row-per-locale schema change on tables that already hold production data.

A few minor things noted inline — none are blockers:

  1. Extra per-entry query on every author-fallback path (hydrateBylines): the new hasContentBylines round-trip fires whenever the strict-locale getContentBylines returns empty. On monolingual installs (the majority case) this is a strict regression — one extra query per entry that has no bylines configured, for no functional benefit. Worth re-running pnpm query-counts and either short-circuiting for monolingual sites or folding the "any junction exists" signal into the first query.
  2. Inconsistent error response shape in the new translations route (raw JSON.stringify instead of apiError). Also a redundant source-not-found check that duplicates the handler's own check.
  3. BylineRepository.update() (unmodified in this PR but worth noting) doesn't validate slug or locale conflicts. An editor renaming the FR sibling of jane to a colliding (slug, locale) will fall through to the raw DB constraint and surface as a generic 500. The create path mirrors a careful CONFLICT response — would be nice to match in a follow-up.
  4. Possible empty-state flash before the picker query loads (gated only on bylines.length === 0, not on a loading flag).

Nothing structural. Once the query-count diff is reviewed, this looks ready.

Comment thread packages/core/src/api/handlers/content.ts Outdated
Comment thread packages/admin/src/components/ContentEditor.tsx Outdated
},
};
}
sourceGroup = source.translationGroup ?? source.id;
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.

sourceGroup = source.translationGroup ?? source.id is correct given the migration backfills translation_group = id for every existing row, but it relies on that invariant holding indefinitely. The type translationGroup: string | null in BylineSummary and the comment in types.ts ("Nullable in storage for backwards compatibility") leave the door open.

If a row ever has translation_group = NULL, the findByTranslationGroup(source.id) lookup at the next line would only return the source row itself (real siblings would have a non-null group), and the (translation_group, locale) conflict check would silently miss.

Not something to act on now — but worth considering either (a) making the column NOT NULL in a follow-up migration once all installs are post-040, or (b) treating a null translationGroup as a data-integrity bug rather than a fallback case in code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will skip this for now, should be addressed in a separate ticket/discussion

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review submitted successfully. The review was created with state "COMMENTED" and includes the summary body plus 4 inline comments anchored on:

  • packages/core/src/api/handlers/content.ts:140 — extra query round-trip on monolingual sites
  • packages/core/src/astro/routes/api/admin/bylines/[id]/translations.ts:67-71 — inline JSON.stringify vs apiError, plus redundant source check (with suggestion)
  • packages/admin/src/components/ContentEditor.tsx:2000 — empty-state flash before picker query loads
  • packages/core/src/api/handlers/bylines.ts:114translationGroup ?? source.id fallback could hide future data-integrity bugs

Review URL: #1146 (review)

github run

@ascorbic ascorbic dismissed their stale review May 22, 2026 16:12

Look at the bot reviews

@ascorbic
Copy link
Copy Markdown
Collaborator

@MohamedH1998 some valid review feedback there that's worth addressing

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

@MohamedH1998
Copy link
Copy Markdown
Contributor Author

@MohamedH1998 some valid review feedback there that's worth addressing

@ascorbic i've addressed all the comments

Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ascorbic ascorbic merged commit 11b3001 into emdash-cms:main May 23, 2026
36 checks passed
@emdashbot emdashbot Bot mentioned this pull request May 23, 2026
@MohamedH1998 MohamedH1998 mentioned this pull request Jun 1, 2026
18 tasks
@emdashbot emdashbot Bot mentioned this pull request Jun 3, 2026
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