Payload CMS
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: trueon your auth collection (usuallyusers), 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#
Open the integration
Go to Studio -> All integrations and click Connect on the Payload CMS card.
Enter your instance details
| Field | What to enter |
|---|---|
| Address | The root URL of your instance, e.g. https://cms.example.com. HTTPS only |
| Auth collection | The collection your service API key belongs to, usually users |
| API key | The service user's API key |
| Custom headers | Optional. 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.
Choose what to translate
| Setting | What it does |
|---|---|
| Name | What to call this connection, such as Production or Staging, so you can tell your instances apart |
| Collections and Globals | Click the ones to translate. One the service user cannot read and update is struck through and cannot be picked until it gets access |
| Engine | The localization engine whose glossary, rules, and brand voice these translations follow |
| Source locale | The locale your editors write in. Use your Payload defaultLocale |
| Target locales | The locales to translate into |
| Mode | Publish 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.
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:
pnpm add @lingo.dev/payloadcmsimport { 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:
| Field | Translated |
|---|---|
text, textarea, and richText fields marked localized: true | Yes |
The same field types inside a localized group, array, blocks, or tabs | Yes |
| Blocks and inline blocks embedded in rich text | Yes, their text fields by the same rules |
select, radio, checkbox, number, date, relationship, upload, json, code, email, point | No |
| Fields that are not localized and have no localized parent | No |
id, blockType, blockName | No |
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:
| Button | What it does | When to use it |
|---|---|---|
| Sync | Translates only what changed since the last run | Backfilling content after connecting, or retrying after a failure |
| Retranslate | Translates everything in scope again, from scratch | After 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.
| Status | Meaning |
|---|---|
| Queued | Waiting for the run ahead of it |
| Running | In progress |
| Completed | Every translation was written back |
| Up to date | Nothing in scope changed since the last run. Not a failure |
| Failed | The run stopped. The reason is shown on the run's row |
| Cancelled | Stopped 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#
| Limit | Detail |
|---|---|
| Payload version | Payload 3 with localization configured |
| Field types | text, textarea, and richText (Lexical only) marked localized |
| Scope | Whole collections and globals. No per-field selection |
| Connections | Several per organization, one per Payload instance |
| Concurrent runs | One per connection |
| Address | HTTPS 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.