Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions apps/docs/content/docs/dev/content-engine/caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,29 @@ contentPublicSlugTag("example.article", "hello-world");
// "content:example.article:slug:hello-world"
```

Format: `content:{contentTypeId}:{scope}[:{key}]`. No plugin id - a content type
id is already globally unique (`validateContentTypes` enforces it) and already
namespaced, as in `example.article`.
Format: `content:{contentTypeId}:{scope}[:{locale}][:{key}]`. No plugin id - a
content type id is already globally unique (`validateContentTypes` enforces it)
and already namespaced, as in `example.article`.

On a [localized](/docs/dev/content-engine/localized-public-api) content type every
builder takes a locale, and the segment sits **after** the scope so the two forms
can never collide - `content:x:list` is three segments and `content:x:list:pl` is
four, whatever the locale happens to spell:

```ts
contentPublicListTag("example.article", "pl");
// "content:example.article:list:pl"

contentPublicSlugTag("example.article", "witaj", "pl");
// "content:example.article:slug:pl:witaj"
```

The locale is load-bearing on the slug tag rather than merely tidy: two languages
routinely answer to the *same* slug (`/en/about` and `/pl/about`), so a
locale-less slug tag would make one language's edit expire the other's page.

A content type that is not localized produces exactly the tags it always did,
byte for byte, so nothing existing has to be re-tagged.

These are pure strings and they are **public API**. Tag your own `fetch` calls
and your own `"use cache"` functions with them and your pages get expired at the
Expand Down Expand Up @@ -305,6 +325,29 @@ enforces rather than by a second, drifting rule:
isContentPubliclyVisible({ publishedAt: row.publishedAt, status: row.status });
```

`isContentTranslationPubliclyVisible` is the localized counterpart, and it is
stated as an `&&` of that same predicate rather than as a second set of clauses -
so the two cannot drift into disagreeing about what "published" means:

```ts
isContentTranslationPubliclyVisible({ base: row, translation });
```

### Which locales a mutation reaches

`contentLocaleInvalidations` answers that, and it is pure:

| What changed | Reaches |
| --- | --- |
| A **shared** field, or the record's publication state | **every** locale |
| A translation in a **non-default** locale | that locale |
| A translation in the **default** locale, `fallback: "default"` | that locale, plus every locale with no translation of its own |
| A translation in the **default** locale, `fallback: "none"` | that locale |

Nothing falls back to Polish, whatever the fallback setting is - so a Polish edit
never throws away the English cache. See
[Localized public API](/docs/dev/content-engine/localized-public-api#caching).

## Where the Next imports live

Exactly one place: `@vitnode/core/content/next`.
Expand Down
8 changes: 5 additions & 3 deletions apps/docs/content/docs/dev/content-engine/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ Four more declarations, each opt-in:

Publication alone exposes nothing. Public exposure requires both of the first
two; `editorial` works with or without either. `localization` combines with
`publication` and `editorial`, and is still
[exclusive of `publicApi` and `search`](/docs/dev/content-engine/localization#stage-5b-boundaries) -
both arrive in a later stage.
`publication`, `editorial` and `publicApi` - a public read then
[resolves one language](/docs/dev/content-engine/localized-public-api) - and is
still
[exclusive of `search`](/docs/dev/content-engine/localization#stage-5c-boundaries),
which arrives in a later stage.

## What it is not

Expand Down
33 changes: 20 additions & 13 deletions apps/docs/content/docs/dev/content-engine/limitations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,29 @@ whose *reading* half is not built yet:

| Combination | Refused until |
| --- | --- |
| `localization` + `publicApi` | Stage 5C |
| `localization` + `search` | Stage 5D |

Each is a definition-time error naming the stage. A localized content type that
silently ran Stage 1-4 logic against its base table while ignoring its localized
fields would be worse than one that refuses to be declared.
It is a definition-time error naming the stage. A localized content type that
silently indexed one language while ranking every other one as a miss would be
worse than one that refuses to be declared.

Because preview projects through `publicApi.fields`, a **locale preview link
cannot be minted** until Stage 5C either - the
[token format](/docs/dev/content-engine/translation-preview) is in place and
tested, but the routes that mint and read one are not.
Stage 5C lifted the `publicApi` refusal, so
[locale-aware public reads](/docs/dev/content-engine/localized-public-api),
fallback resolution, strict-locale slugs, locale-aware cache tags and
[locale preview links](/docs/dev/content-engine/translation-preview) all work now.

Also outside Stage 5B: fallback resolution, locale-aware cache tags, per-locale
search documents, an `Outdated` badge (its honest definition needs a comparison
two timestamps cannot make), a locale selector on the AdminCP *list*, and
locale-specific scheduling.
Still outside Stage 5C: per-locale search documents, an `Outdated` badge (its
honest definition needs a comparison two timestamps cannot make), a locale
selector on the AdminCP *list*, and locale-specific scheduling.

## A public list cannot be ordered by a localized field

`publicApi.orderableFields` refuses one at definition time. A list ordered by a
localized title would reshuffle itself for every language, and a fallback set
would interleave two collations - so one cursor would mean two different
positions depending on the language. Order by a column the record has one of.
`filterableFields` and `searchableFields` *may* name a localized field: both are
evaluated against the single translation the reader is being served.

## Localized field names cannot appear on base-table surfaces

Expand All @@ -79,7 +86,7 @@ six are compile errors and runtime errors.
`admin.titleField` therefore falls back to `null` on a content type whose only
text fields are localized. The locale tabs show the localized title inside each
tab; the list still has no locale-aware title column, which arrives with the
locale selector in Stage 5C.
locale selector in Stage 5D.

## Foreign key names on a long translation table are truncated by Postgres

Expand Down
33 changes: 16 additions & 17 deletions apps/docs/content/docs/dev/content-engine/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ A content type without the block generates the same single table, the same
routes and the same wire shapes it always did.

<Callout type="warn" title="Two combinations are still refused">
Localization works with `publication` and `editorial` from Stage 5B on, but
Localization works with `publication`, `editorial` and `publicApi`, but
cannot yet be combined with `publicApi` or `search` - each is refused at
definition time with a message naming the stage that lifts it. See [Stage 5B
boundaries](#stage-5b-boundaries).
definition time with a message naming the stage that lifts it. See [Stage 5C
boundaries](#stage-5c-boundaries).
</Callout>

## This is not UI translation
Expand Down Expand Up @@ -102,7 +102,7 @@ localization: {
| --- | --- | --- | --- |
| `enabled` | `true` | - | Literal `true`. Omit the block to stay non-localized |
| `defaultLocale` | `string` | - | The locale every record is created in, and the one translation it can never lose |
| `fallback` | `"none" \| "default"` | `"none"` | Reserved for Stage 5C. Resolved now so the configuration is stable before anything reads through it |
| `fallback` | `"none" \| "default"` | `"none"` | What a [public read](/docs/dev/content-engine/localized-public-api) does for a locale with no published translation |

`defaultLocale` is checked twice. Its **shape** is checked at definition time -
it has to look like a locale code and fit `varchar(32)`. Whether it names a real,
Expand All @@ -111,7 +111,7 @@ checked [once at boot](#the-boot-check) instead.

`fallback` does nothing yet, and this page will not pretend otherwise. Nothing in
Stage 5A reads through it; it exists so that a content type declared today does
not change public behaviour the moment Stage 5C lands. `"none"` is the default
not change public behaviour the moment the public read layer landed. `"none"` is the default
because it is the only answer that cannot silently publish the wrong language.

## Which fields can be localized
Expand Down Expand Up @@ -274,25 +274,24 @@ offender at once rather than failing on the first, and it is skipped entirely
when no content type is localized - an install with none never touches the
languages table because of it.

## Stage 5B boundaries
## Stage 5C boundaries

Stage 5A landed the infrastructure; Stage 5B landed the editorial layer on top of
it. What is still missing is everything that reads *outwards*, and the honest
failure for that is a refused definition rather than a content type that quietly
runs Stage 1-4 logic against the base table while pretending its localized fields
do not exist.
Stage 5A landed the infrastructure, Stage 5B the editorial layer and Stage 5C the
[public read](/docs/dev/content-engine/localized-public-api). One thing still
reads outwards without knowing about languages, and the honest failure for that is
a refused definition rather than a content type that quietly indexes one language
and ranks every other one as a miss.

| Combination | Refused until | Why |
| --- | --- | --- |
| `localization` + `publicApi` | Stage 5C | A public read has to resolve a locale and decide what to do when a translation is missing |
| `localization` + `search` | Stage 5D | One document per record would index a single language and rank every other one as a miss |

Each is a `ContentEngineError` at definition time, with the stage in the message.
It is a `ContentEngineError` at definition time, with the stage in the message.

Because `editorial.preview` requires `publicApi`, a **locale preview link cannot
be minted yet** either - the token format is in place and tested, and the routes
land with Stage 5C. See
[Locale preview](/docs/dev/content-engine/translation-preview).
`localization` + `publicApi` is **no longer refused**: locale precedence, fallback,
strict-locale slugs, locale-aware cache tags and the
[locale preview link](/docs/dev/content-engine/translation-preview) all landed with
Stage 5C.

## The roadmap

Expand Down
Loading
Loading