Minor Changes
-
#1146
11b3001Thanks @MohamedH1998! - Adds first-class i18n support for bylines, mirroring the row-per-locale model already used by menus and taxonomies (PR #916, migrations 036).Schema (migration 040)
_emdash_bylinesgains two columns:locale—TEXT NOT NULL DEFAULT 'en'. Every row now belongs to exactly one locale.translation_group—TEXT NOT NULL. Shared across every locale variant of a single byline identity. The anchor row'stranslation_groupequals itsid; siblings inherit it.
A partial unique index
idx_bylines_group_locale_uniqueenforces one row per(translation_group, locale). The pre-existing(slug)unique index becomes(slug, locale)to allow the same slug across locales.Existing rows are backfilled to the configured
defaultLocale(or'en'if i18n isn't configured) withtranslation_group = id. Monolingual sites see no functional change; multilingual sites continue rendering the same byline data at the default locale until editors create translations.Credit hydration: strict per-locale
_emdash_content_bylines.byline_idnow stores the byline'stranslation_group, not its row id. When an entry is rendered, credits are filtered by joining the junction against the byline sibling whoselocalematches the entry'slocale. If no sibling exists at the entry's locale, the credit hydrates as empty — there is no fallback to other locales' bios.Author-inferred bylines (where an entry has no explicit credits but its author is linked to a byline) still fall back per-locale and respect the strictness gate: an entry with explicit credits at any locale will not infer from the author even if the explicit credits don't resolve at the rendering locale.
This is a deliberate behavior change for multilingual sites. The motivation is correctness: chain-walking credits across locales renders the wrong-language bio on translated entries.
The "explicit credit suppresses author fallback" check reads
primary_byline_iddirectly from the content row — set bysetContentBylinesiff junction rows exist, backfilled by migration 040 for pre-existing rows. No separate probe against_emdash_content_bylinesis needed at hydration time; the column is folded into the single per-entry context fetch (author_id+primary_byline_idin one query). Both monolingual and multilingual sites get the same query count.Identity lookups: chain-walk
getBylineBySlug(slug, { locale })walks the configured fallback chain (resolveLocaleChain), likegetMenuandgetTerm. Author pages for un-translated bylines still render an identity rather than 404'ing. This is conceptually distinct from credit hydration and runs throughrequestCachedfor per-render dedupe.Admin
- TranslationsPanel in the bylines editor lists every configured locale with Edit / Translate buttons. The Translate action POSTs to the new
POST /_emdash/api/admin/bylines/:id/translationsendpoint. - LocaleSwitcher on
/bylinesfilters the list strictly to one locale. Cross-locale navigation via TranslationsPanel routes through/bylines?locale=…. - The byline picker on the content editor is locale-pinned to the entry's locale. Editors only see bylines that will actually hydrate at the entry's locale.
- The byline credit empty state on a locale with no bylines yet shows a CTA linking to
/bylines?locale=…for inline creation. - Translating an entry (
POST /content/:collectionwithtranslationOf) callscopyContentBylinesto inherit the source's credits — these resolve at the new entry's locale via the strict-hydration model, so credits "follow" the content across translations once sibling bylines exist.
API additions
GET /_emdash/api/admin/bylines/:id/translations— list every sibling row sharing a translation_group.POST /_emdash/api/admin/bylines/:id/translations— create a sibling at a target locale. Body defaults (slug, displayName, websiteUrl, avatar) inherit from the source.POST /_emdash/api/admin/bylinesacceptstranslationOf+localeto create a sibling in one call.GET /_emdash/api/admin/bylines?locale=…filters strictly.BylineSummarygainslocale: stringandtranslationGroup: string | null(additive — existing consumers ignore the new fields).
Permissions
Two new entries on
@emdash-cms/auth:bylines:read— minimumSUBSCRIBER.bylines:manage— minimumEDITOR.
All byline routes (list, get, update, delete, translations) now check these instead of
content:read/Role.EDITOR. Role thresholds are unchanged, so existing users see no permission differences. Custom RBAC configurations that bind to the old strings should add the new permission names.Repository
BylineRepositoryis strict per-locale:findMany,findBySlug,findByIdaccept an optionallocaleand return rows matching that locale (or all locales when omitted, for the manager view).- New methods:
listTranslations(id),findByTranslationGroup(group),copyContentBylines(collection, fromId, toId). setContentBylinesdeduplicates bytranslation_groupafter resolving wire row ids, so passing two sibling row ids of the same identity collapses to one credit row.deleteis sibling-aware: removing one locale variant leaves siblings standing.
Notable trade-offs
- Strict hydration over chain-walking for credits. Chain-walking would render mismatched-language bios on translated content. The honest answer is to show nothing rather than the wrong thing; the picker tells editors which bylines will resolve at the entry's locale, and the empty-state CTA makes creating a sibling a one-click flow.
- Schema is row-per-locale, not a separate
byline_translationsside-table. Matches the existing content / menu / taxonomy convention so query patterns and indexes are consistent across the codebase.
Patch Changes
-
#1139
88f544dThanks @ask-bonk! - Upgradeskyselyto^0.29.0(was^0.27.0) to resolve three high-severity advisories fixed in>=0.28.17:- GHSA-wmrf-hv6w-mr66 – SQL injection via unsanitized JSON path keys
- GHSA-pv5w-4p9q-p3v2 – JSON-path traversal injection via
JSONPathBuilder.key()/.at() - GHSA-8cpq-38p9-67gx – MySQL SQL injection via
sql.lit(string)
Also updates import paths for
MigratorandMigrationtypes tokysely/migrationto comply with kysely 0.29 export changes.