Payload CMS

Updated: 3 weeks ago · 9 min read

Connect your Payload CMS to a localization engine, pick the collections and globals to cover, and Lingo.dev translates their localized fields into your target locales and writes them back to Payload under each locale.

Works with Payload 3 projects that have localization enabled and use the Lexical rich text editor. The older Slate editor is not supported. Localized text, textarea, and richText fields are translated. Everything else in the document is left as it is.

Payload CMS comes with the Enterprise plan. If the Connect page says Payload CMS is not on your plan, reach out and we'll sort it out.

Before You Start#

You need three things:

  • Payload 3 with localization configured. Make sure your source and target locales match the locales in your Payload config.
  • A service user with an API key. Set useAPIKey: true on your auth collection (usually users), create a user for Lingo.dev, and generate an API key for it in the Payload admin. The user needs read and update access to every collection and global you want translated.
  • A localization engine. Its glossary, brand voice, and rules shape the translations.

Locale codes must match your Payload config

The locales you pick in Lingo.dev must match the codes under localization.locales exactly. If Payload has en and de, pick English and German, not English (United States): en-US and en are different locales. Use your Payload defaultLocale as the source locale, because that is the locale the plugin watches for changes.

Connect Your Payload Instance#

1

Open the integration

Go to Studio -> All integrations and click Connect on the Payload CMS card.

2

Enter your instance details

FieldWhat to enter
AddressThe root URL of your instance, e.g. https://cms.example.com. HTTPS only
Auth collectionThe collection your service API key belongs to, usually users
API keyThe service user's API key
Custom headersOptional. Sent on every request to your instance. Click Add header for each one, or paste a Name: value line to fill a row

Click Verify instance. Lingo.dev checks the key against your instance before moving on.

3

Choose what to translate

SettingWhat it does
NameWhat to call this connection, such as Production or Staging, so you can tell your instances apart
Collections and GlobalsClick the ones to translate. One the service user cannot read and update is struck through and cannot be picked until it gets access
EngineThe localization engine whose glossary, rules, and brand voice these translations follow
Source localeThe locale your editors write in. Use your Payload defaultLocale
Target localesThe locales to translate into
ModePublish translates only published changes and publishes the translations. Draft translates draft saves too and writes the translations as drafts. See Drafts vs Published below

Click Connect.

4

Install the plugin

The last step shows your Webhook URL. Copy it now. It is shown only once. Store it as LINGO_WEBHOOK_URL in your Payload environment, then install the plugin and add it to your config:

bash
pnpm add @lingo.dev/payloadcms
ts
import { buildConfig } from "payload";
import { lingo } from "@lingo.dev/payloadcms";

export default buildConfig({
  // ...your collections, globals, and localization config
  plugins: [
    lingo({
      webhookUrl: process.env.LINGO_WEBHOOK_URL,
    }),
  ],
});

Click Done, then redeploy Payload. From now on, every change published in the source locale is sent to Lingo.dev for translation.

What the plugin does

It adds a GET /api/lingo/schema endpoint that tells Lingo.dev which of your fields are localized text, and a hook that pings Lingo.dev when a document or global changes. Scope, locales, and engine live in the dashboard, so you can change them without redeploying. Leave out webhookUrl to skip the hook and start every translation from the dashboard.

Choose What Gets Translated#

The connection page has four tabs: Collections, Globals, Workflow runs, and Settings.

Scope is set per collection and per global. The Collections and Globals tabs list everything your instance offers, one row each, with when it was last synced. Each row has a Sync switch: turn it on and every document in that collection, or that global, is covered from then on. The change is saved the moment you flip it, and applies to the next run without a redeploy. The switch in the table header turns Sync on or off for everything in the list.

A row marked no read + update is locked: the service user cannot read and update it, so it cannot be translated. A row marked not localized yet has Sync off.

Locales, engine, and mode are changed on the Settings tab.

Inside a document, your Payload field config decides what is translated:

FieldTranslated
text, textarea, and richText fields marked localized: trueYes
The same field types inside a localized group, array, blocks, or tabsYes
Blocks and inline blocks embedded in rich textYes, their text fields by the same rules
select, radio, checkbox, number, date, relationship, upload, json, code, email, pointNo
Fields that are not localized and have no localized parentNo
id, blockType, blockNameNo

Rich text is translated as a Lexical tree. Formatting, links, uploads, and block structure are preserved, and only the text inside is replaced. A sentence broken up by bold text or a link is translated as one sentence.

To bring a field into scope, mark it localized: true in Payload and redeploy. The next run picks it up.

Sync and Retranslate#

Automatic runs. With webhookUrl set in the plugin, every save of a document or global in the source locale notifies Lingo.dev. Saves made within a short time window are debounced into one run. Saves in other locales, draft saves (unless the connection is in Draft mode), and content outside your scope are ignored.

Manual runs. Two buttons, two different jobs:

ButtonWhat it doesWhen to use it
SyncTranslates only what changed since the last runBackfilling content after connecting, or retrying after a failure
RetranslateTranslates everything in scope again, from scratchAfter changing your engine's glossary, brand voice, or rules

Both live in the page header, where they cover everything with Sync on. Retranslate from the header asks you to confirm first. To run just some collections or globals, tick their rows: Queue sync (N) and Queue retranslation (N) appear and cover just those rows. A row can be ticked only once its Sync switch is on.

Nothing is translated on connect. To translate what you already have, click Sync in the page header. Adding a target locale later works the same way: the next Sync fills it.

Only one run is in flight per connection. Further requests queue and start in order.

Retranslate overwrites hand edits

Retranslate regenerates every translated field in its scope, including translations your team edited by hand in Payload. Sync only regenerates fields whose source text changed, so hand edits elsewhere survive.

Watch a Run#

The Workflow runs tab lists every run: when it started, what triggered it (Webhook or Manual, then Sync or Retranslate), which locales it covered, how long it took, and its status. A queued or running run has a Stop button. Open a row to see the result per locale.

StatusMeaning
QueuedWaiting for the run ahead of it
RunningIn progress
CompletedEvery translation was written back
Up to dateNothing in scope changed since the last run. Not a failure
FailedThe run stopped. The reason is shown on the run's row
CancelledStopped by someone on your team

A failed run keeps what it already wrote. The error message lists the documents that were not written, and the next Sync retries them. A document an editor saved mid-run is skipped and picked up by the next run.

Where Translations Land#

Each translation is written to the same document or global under its target locale, Payload's own localization model. Only translated fields are written. Every other field stays untouched. Write-backs run as the service user and do not trigger a new run.

Existing translations are kept. On the first sync of a document, whatever a target locale already holds stays in place and only the missing fields are translated. A field still holding its Payload default value counts as missing. Use Retranslate to replace existing translations.

Drafts vs Published#

In Publish mode, only published changes start a run, and translations are published as soon as they are written. Payload publishes the whole document, so any unpublished draft edits on it go live with the translation.

In Draft mode, draft saves start runs too. Lingo.dev reads the latest draft of the source and writes each translation as a draft. Nothing changes for your readers until someone publishes the translation in Payload. Use this while evaluating translation quality or when translations go through review.

You can change the mode later on the Settings tab.

Managing the Connection#

Settings#

The Settings tab shows the instance the connection reads, which collections and globals have Sync on, and whether the connection is Working or Cannot be reached. It lets you change the name, the source locale, the target locales, the engine, and the mode. Each change keeps the webhook URL, so no redeploy is needed.

The address and the API key are set when the connection is made and cannot be changed afterwards. To point Lingo.dev at a different address or key, disconnect and connect again.

Disconnect#

Disconnect at the bottom of the Settings tab stops Lingo.dev reading from and writing to the instance. This removes the connection, its run history, and the record of what has been translated. Translations already written stay in Payload. Remove LINGO_WEBHOOK_URL or the plugin from your config afterwards.

Reconnecting means a new webhook URL

A new connection gets a new webhook URL, so update LINGO_WEBHOOK_URL and redeploy before automatic runs work again. The first sync re-reads every document in scope, keeps the translations Payload already has, and fills the gaps.

Limits#

LimitDetail
Payload versionPayload 3 with localization configured
Field typestext, textarea, and richText (Lexical only) marked localized
ScopeWhole collections and globals. No per-field selection
ConnectionsSeveral per organization, one per Payload instance
Concurrent runsOne per connection
AddressHTTPS only

Troubleshooting#

Connecting fails with "Payload rejected the API key". Check the key, the auth collection slug, and that useAPIKey is enabled on that collection.

A collection or global shows "no read + update". Grant the service user read and update access in that collection's access config, then reload the connection page. The row's Sync switch unlocks.

The first run fails with "The Lingo plugin isn't installed". Add @lingo.dev/payloadcms to your Payload config's plugins and redeploy. Connecting works without the plugin; syncing does not.

Publishing in Payload does not start a run. Check that LINGO_WEBHOOK_URL is set, localization is configured, the collection or global is in scope, the save was in the source locale, and it was a publish rather than a draft, unless the connection is in Draft mode.

A field is not translated. It has no localized: true on it or on a parent, or it is not a text, textarea, or richText field.

The Settings tab shows the connection as Cannot be reached. Check that the instance is up, the key is still valid, and any gateway headers still work. The address and key cannot be edited on an existing connection, so if one of them changed, disconnect, connect again, and update LINGO_WEBHOOK_URL with the new webhook URL.

Next Steps#