Skip to content

feat(translator): tell the systemPrompt builder what it is translating - #180

Merged
SearheiParkhamchuk merged 2 commits into
mainfrom
feat/translator-prompt-context
Oct 22, 2026
Merged

SearheiParkhamchuk merged 2 commits into
mainfrom
feat/translator-prompt-context

Conversation

@SearheiParkhamchuk

Copy link
Copy Markdown
Contributor

Closes #109.

A systemPrompt builder could not read anything from the CMS, and could not know which collection it was working on. Both seams were blind, and together they made a per-collection prompt impossible.

The reporter wanted to inject a glossary that lives in the CMS. Unable to read it from a builder, they stopped using systemPrompt altogether and wrapped the whole provider in their own async translate that rebuilds the OpenAI provider on every call with the terms in a closure — a wrapper whose only purpose was to get an await. Scoping a term to one collection stayed impossible at any price, because neither seam knew which collection it was.

What changed

The builder may answer with a promise. SystemPromptBuilder returns string | Promise<string>. The call site already awaited it through a helper already typed to accept one, so this was a type away — and it fixes a live defect, not only a gap: an async builder's result was previously concatenated unawaited, putting [object Promise] into the prompt sent to the model.

The builder is told what it is translating. A new DocumentRef — { collection, id } — travels from the handler to the provider and into SystemPromptContext. Both surfaces name it: the document route and the field route pass the same pair, so a prompt scoped to a collection applies however the editor started the translation.

Shape, and why it is this one

collection and documentId began as two loose optional fields beside inlineMarks. That was wrong twice over, and both corrections came from review rather than from the first draft:

  • They are not options. inlineMarks is a switch about how to translate; these say what is being translated. One name now holds them, so "a collection with no document" stops being expressible.
  • They are not optional. Every real caller has both. The only producer of the request description is TranslationStage, and it always sets the document — so document is required inside it. The parameter holding it stays optional, because a provider called on its own is asked to translate strings and that is a complete request; what is not allowed is describing a request halfway.

No payload handle is passed, though the issue asked for one. The only channel from the handler to the provider runs through src/core, so a handle would put a Payload reference in the kernel — which neither src/core nor src/translation-providers has today. The builder is awaitable now, so a consumer resolves its own.

Compatibility

Declarations were built on main and on this branch and diffed: index.d.ts is byte-identical. Two caveats belong in the release note rather than being left to be discovered:

  • The fourth argument now reaches a provider on nearly every call, where its presence used to imply inline marks. A provider branching on if (options) instead of options.inlineMarks === true will see a difference.
  • A consumer who concatenates their own builder's result — `${base(ctx)}\n…` — now silently interpolates [object Promise], because a template literal accepts a promise and the compiler says nothing. Same class as the defect fixed here.

Requiring document inside the request description is breaking for anyone constructing one by hand without it. Nobody does; the plugin is the sole producer.

Scheduled, not done

translate(input, sourceLng, targetLng, options?) keeps two adjacent same-typed strings that can be swapped silently, and a trailing bag that mixes what with how. Recorded as translate-positional-languages in docs/DEPRECATIONS.md, status planned, to land with the kernel extraction rather than forcing an early major. The path the README recommends — createTranslationProvider({ complete }) — is unaffected: CompletionFn already takes a single object.

A deeper question surfaced in review and is now open question 5 in docs/plans/2026-08-21-translation-mechanism-kernel-extraction.md: document is carried by three functions that never read it, and the only reader is the consumer's own systemPrompt. Moving prompt assembly into the plugin was considered and rejected — systemPrompt belongs to a language-model provider, and a GoogleTranslate or DeepL provider has no system prompt at all. The open question is narrower: once the provider port is reshaped anyway, does buildSystemPrompt stay part of the provider, or become something a provider declares it accepts, the way capabilities.inlineMarks already does.

Verification

package 2110 tests / 151 files · types 0 · lint 45 warnings, 0 errors
apps/dev types 0
integration SQLite 237 · PostgreSQL 242 · MongoDB 235
gates sp-diff-checks clean · sp-lint-delta introduced nothing

New coverage: the builder awaited and its text appended to; the document named at the unit, provider and pipeline levels; the field surface naming the same document as the document surface; and an end-to-end case on three databases asserting what the provider was actually handed. Each guard proven by a mutation that reddened it alone and was reverted.

Two tests changed that are worth naming rather than hiding: one asserted the old three-argument call and now asserts four — that edit is the behaviour change — and one that passed before the implementation existed was removed as a tautology.

A separate first commit regenerates apps/dev/src/payload-types.ts: Payload now defaults auth.forgotPassword.minRequestInterval, which adds a resetPasswordRequestedAt field. Machine-written, nothing in this repo configures it, kept out of the hand-written commit.

Payload now defaults `auth.forgotPassword.minRequestInterval` to 15000
(collections/config/defaults.js), and a non-zero interval makes it add a
`resetPasswordRequestedAt` field to every auth collection
(auth/getAuthFields.js). Nothing in this repo configures either; the generated
file had simply not been regenerated since the version that introduced the
default.

Committed on its own so the machine-written change does not ride along with
hand-written work and reappear in every branch's diff.
A `systemPrompt` builder could not read anything from the CMS and could not
know which collection it was working on. Both seams were blind, and together
they made a per-collection prompt impossible.

`SystemPromptBuilder` was synchronous, so a builder had nowhere to await a
lookup. The provider saw `translate(input, sourceLng, targetLng)` — numbered
strings and two language codes, nothing about the document. The reporter of
#109 wanted to inject a glossary that lives in the CMS: unable to read it from
a builder, they stopped using `systemPrompt` altogether and wrapped the whole
provider in their own async `translate` that rebuilds the OpenAI provider per
call with the terms in a closure — a wrapper whose only purpose was to get an
await. Scoping a term to one collection stayed impossible at any price,
because neither seam knew which collection it was.

Now:

- `SystemPromptBuilder` may answer with a promise. The call site already
  awaited it through a helper already typed to accept one, so this was a type
  away. It also fixes a live defect: an async builder's result was previously
  concatenated unawaited, putting "[object Promise]" into the prompt.
- a new `DocumentRef` — `{ collection, id }` — says what is being translated.
  It is required on the pipeline's own types, because every translation has a
  subject, and it rides to the provider inside the existing
  `TranslationRequestOptions`, the type added in 0.13.0 for exactly this.
- both surfaces name it. The document route and the field route pass the same
  pair, so a prompt scoped to a collection applies however the editor started
  the translation.

Identity, not a switch: the two halves are one named type rather than two
loose fields, so "a collection with no document" stops being expressible, and
`document` is required inside the request description even though the
description itself may be omitted — a provider called on its own is asked to
translate strings, but a half-description is not allowed.

No `payload` handle is passed. The only channel from the handler to the
provider runs through src/core, so a handle would put a Payload reference in
the kernel; the builder is awaitable now, so a consumer resolves its own.

Behaviour note for the release: the fourth argument now reaches a provider on
nearly every call, where its presence used to imply inline marks. A provider
branching on `if (options)` rather than `options.inlineMarks === true` will see
a difference.

Closes #109
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
ideal-cms Ready Ready Preview Oct 22, 2026 2:16pm UTC

Request Review

@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.18.0-next.1 🎉

The release is available on npm package (@major/translator dist-tag)

Your semantic-release bot 📦🚀

@SearheiParkhamchuk
SearheiParkhamchuk merged commit 313bbc1 into main Oct 22, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.18.0 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

SearheiParkhamchuk added a commit that referenced this pull request Oct 22, 2026
main released 0.18.0, carrying the prompt-context work (#180, closing #109).
The same feature branch was already merged here as 0.18.0-next.1, so the only
real conflict was the bookkeeping: the version field keeps 1.0.0-next.4,
because the prerelease line is ahead and semantic-release owns that field, and
the changelog keeps both headings over the one body they describe.

Recording main as an ancestor is what the eventual major/translator to main
merge needs.
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.10.2 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0-next.5 🎉

The release is available on npm package (@next dist-tag)

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.1.1 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.4.0 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 7848a589 Deployed Oct 15, 2026 by vercel[bot]
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.

translator: an async systemPrompt builder with collection, document and run context

1 participant