|
Documentation
Book a DemoPlatform
PlatformMCPCLIAPIWorkflows
Guides
Changelog

Localization

  • Overview
  • Translation API
  • Web App Localization
  • Mobile App Localization
  • iOS with String Catalogs
  • Android with strings.xml
  • Emails Localization
  • Static Content (e.g. .md, .json)
  • Next.js with Markdoc
  • Rails with i18n

Workflows

  • Engine Setup with MCP
  • Jira Triage
  • Zendesk Help Center
  • CI/CD
  • Payload CMS

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.

The Payload integration is enabled per organization. If you don't see it under Settings -> Integrations, reach out and we'll turn it on for you.

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 Settings -> Integrations and click Connect under Payload CMS.

2

Enter your instance details

FieldWhat to enter
Connection nameA label such as Production or Staging
Payload Base URLThe root URL of your instance, e.g. https://cms.example.com. HTTPS only
Auth Collection SlugThe 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

Lingo.dev verifies the key against your instance before moving on.

3

Choose what to translate

SettingWhat it does
Collections and GlobalsTick the ones to translate. A row marked No read + update stays disabled until the service user gets access to it
Source LocaleThe locale your editors write in. Use your Payload defaultLocale
Target LocalesThe locales to translate into
EngineThe localization engine that translates this content
Translate draft savesOff translates only published changes. On translates draft saves too and keeps the translations as drafts
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,
    }),
  ],
});

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 three tabs: Collections, Globals, and Runs.

Scope is set per collection and per global. Every document in a selected collection is covered. To change scope, locales, engine, or the draft setting, click Edit configuration in the page header. Changes apply to the next run without a redeploy.

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 Translate draft saves is on), and content outside your scope are ignored.

Manual runs. Every collection, global, and document row has two buttons:

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 the row again, from scratchAfter changing your engine's glossary, brand voice, or rules

Open a collection to reach its documents and sync one at a time. Both tabs show when each item was last synced.

Nothing is translated on connect. To translate what you already have, click Sync on each collection and global. 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. While a row is covered by a queued or running run, its buttons read Syncing....

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 Runs tab lists every run with its status, trigger (Webhook or Manual, sync or retranslate), start time, and duration. A queued or running run can be cancelled from the list.

Open a run to see the stage it is in (reading from Payload, translating, writing back), overall progress, progress per target locale, and the documents, collections, and globals it covers. Each item links to the Payload admin.

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 at the top of the run detail
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#

With Translate draft saves off, the default, 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.

With it on, 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.

Managing the Connection#

Rotate the webhook URL#

Open the menu in the connection page header and choose Regenerate webhook URL. The old URL stops working immediately. Update LINGO_WEBHOOK_URL and redeploy. Editing the connection keeps the URL.

Disconnect#

Disconnect from Settings -> Integrations -> Payload CMS. 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
Base URLHTTPS 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 reopen the configuration.

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.

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 connection shows "Couldn't reach this Payload instance". Check that the instance is up, the key is still valid, and any gateway headers still work. Update the connection in Settings -> Integrations.

Next Steps#

Localization Engines
Configure the engine that translates your Payload content
Glossaries
Lock product names and terms across every collection
Brand Voices
Set the tone your content should keep per language

Was this page helpful?

Max PrilutskiyMax Prilutskiy·Updated 17 minutes ago·8 min read