Skip to content

feat: Add content engine Stage 5C — Public Localization - #739

Open
aXenDeveloper wants to merge 1 commit into
feat/Universal-Content-Engine-mvp-5bfrom
feat/Universal-Content-Engine-mvp-5c
Open

feat: Add content engine Stage 5C — Public Localization#739
aXenDeveloper wants to merge 1 commit into
feat/Universal-Content-Engine-mvp-5bfrom
feat/Universal-Content-Engine-mvp-5c

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Lifts the localization + publicApi boundary. A public localized response is one base row joined to one translation, and both halves have to be published; fallback chooses which translation that predicate runs against and never relaxes it.

Locale resolution

  • content/locale.ts: Accept-Language parsing with quality values, two-pass negotiation (exact before prefix), and the precedence explicit -> negotiated -> default. An explicit locale that names no served language is null, which the routes answer as the same 404 a missing record gets; a negotiated one falls through, because a preference is not an instruction.

The read layer

  • contentPublicCondition states subordination once, in SQL, as an and of the existing publishedCondition over both pairs of columns.
  • createContentLocalizedPublicService joins the translation being served and evaluates visibility twice in the same statement - as an EXISTS in the WHERE, so the paginator's COUNT agrees with the joined read, and as the join's ON. Filters and searches on localized fields go through the same test, so they can never match a language the reader will not be shown.
  • findBySlug is strict-locale on both fallback settings: a URL belongs to a language, and two languages routinely share a slug.
  • The response carries the locale it resolved to, plus Content-Language and - only when the header decided - Vary: Accept-Language.

Caching

  • Every tag gains a locale segment after the scope, so a non-localized content type's tags are byte-identical to what they were.
  • contentLocaleInvalidations is the fan-out rule: a shared change reaches every locale, a translation reaches its own - plus, for the default locale under fallback: "default", every locale served by it.
  • GET /{id}/public-locales evaluates "which languages have a page" on the server, where the language registry lives; the AdminCP diffs two snapshots rather than reimplementing the rule in the browser.
  • Scheduled transitions fan out the same way, over the revalidation bridge.

Preview

  • POST /{id}/translations/{locale}/preview freezes the shared revision and that locale's translation revision together, and puts ?locale= in the link. The public preview route resolves its locale the same way every other public read does and refuses a mismatch in either direction.

Definition-time rules

  • publicApi.orderableFields refuses a localized field: a list ordered by one reshuffles per language, and a cursor would mean two positions across a fallback set.
  • A localized content type may not expose a field called locale.

No migration: publicApi adds no column.

Improving Documentation

Description

What?

Why?

Lifts the `localization` + `publicApi` boundary. A public localized response is
one base row joined to one translation, and both halves have to be published;
fallback chooses *which* translation that predicate runs against and never
relaxes it.

Locale resolution
- `content/locale.ts`: `Accept-Language` parsing with quality values, two-pass
  negotiation (exact before prefix), and the precedence explicit -> negotiated ->
  default. An explicit locale that names no served language is `null`, which the
  routes answer as the same 404 a missing record gets; a negotiated one falls
  through, because a preference is not an instruction.

The read layer
- `contentPublicCondition` states subordination once, in SQL, as an `and` of the
  existing `publishedCondition` over both pairs of columns.
- `createContentLocalizedPublicService` joins the translation being served and
  evaluates visibility twice in the same statement - as an `EXISTS` in the
  `WHERE`, so the paginator's `COUNT` agrees with the joined read, and as the
  join's `ON`. Filters and searches on localized fields go through the same
  test, so they can never match a language the reader will not be shown.
- `findBySlug` is strict-locale on both fallback settings: a URL belongs to a
  language, and two languages routinely share a slug.
- The response carries the locale it resolved to, plus `Content-Language` and -
  only when the header decided - `Vary: Accept-Language`.

Caching
- Every tag gains a locale segment after the scope, so a non-localized content
  type's tags are byte-identical to what they were.
- `contentLocaleInvalidations` is the fan-out rule: a shared change reaches every
  locale, a translation reaches its own - plus, for the default locale under
  `fallback: "default"`, every locale served by it.
- `GET /{id}/public-locales` evaluates "which languages have a page" on the
  server, where the language registry lives; the AdminCP diffs two snapshots
  rather than reimplementing the rule in the browser.
- Scheduled transitions fan out the same way, over the revalidation bridge.

Preview
- `POST /{id}/translations/{locale}/preview` freezes the shared revision and that
  locale's translation revision together, and puts `?locale=` in the link. The
  public preview route resolves its locale the same way every other public read
  does and refuses a mismatch in either direction.

Definition-time rules
- `publicApi.orderableFields` refuses a localized field: a list ordered by one
  reshuffles per language, and a cursor would mean two positions across a
  fallback set.
- A localized content type may not expose a field called `locale`.

No migration: `publicApi` adds no column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vitnode-docs Ready Ready Preview Aug 7, 2026 7:54pm

@github-actions github-actions Bot added the 💡 Feature A new feature label Aug 7, 2026
@aXenDeveloper
aXenDeveloper marked this pull request as ready for review August 7, 2026 19:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21560059ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


return {
"Content-Language": locale,
...(source === "negotiated" ? { Vary: "Accept-Language" } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Vary all negotiable responses on Accept-Language

When a localized request has no explicit ?locale=, the representation can depend on Accept-Language even if the current header is absent or does not match. Such a request is labeled source: "default" here and receives no Vary, so a browser or shared cache can reuse an English default response for a later Polish request to the same URL. Omit Vary only for explicit-locale URLs; default and negotiated responses must both vary on the header.

Useful? React with 👍 / 👎.

Comment on lines +104 to +106
for (const tag of wanted) {
const exact = byNormalized.get(tag);
if (exact !== undefined) return exact;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve quality order while applying locale fallback

Scanning every requested tag for an exact match before considering any primary-tag fallback defeats the quality ranking produced by parseAcceptLanguage. For example, fr;q=1, en;q=0.1 with available locales fr-FR and en resolves to English, even though French is the user's strongest preference and the function explicitly supports resolving fr to fr-FR. Process each ranked tag by trying its exact and primary-language matches before moving to the next tag.

Useful? React with 👍 / 👎.

Comment on lines +424 to +425
const translated = localized
? await readPreviewTranslation(c, payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Select only shared fields for live localized previews

When a localized preview token has r: 0—the supported case for records that predate editorial revisions—readPreviewRow calls contentPublicSelection(definition, model.columns). The base model's column map contains only shared fields, while this change permits localized fields in publicApi.fields, so that selection contains undefined columns and Drizzle fails before this translation merge runs. Build the live base selection from exposed shared fields plus the cursor id so legacy localized preview links do not return a server error.

Useful? React with 👍 / 👎.

@aXenDeveloper aXenDeveloper changed the title feat(content): add the locale-aware public read layer feat: Add content engine Stage 5C — Public Localization Aug 7, 2026
@github-actions github-actions Bot added 💡 Feature A new feature and removed 💡 Feature A new feature labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant