Getting started
Install the plugin, configure it, and sync content between Payload and Crowdin.
Table of contents:
- Install
- Database changes
- Options
- Environment variables
- Sync translations
- Delete documents
- Further documentation
Install
Requirements:
- Payload 3
- A Crowdin project and a personal access token with access to it
# npm
npm install payload-crowdin-sync
# yarn
yarn add payload-crowdin-sync
Add the plugin to your Payload configuration.
import { crowdinSync } from 'payload-crowdin-sync';
export default buildConfig({
plugins: [
crowdinSync({
projectId: 323731,
token: process.env.CROWDIN_TOKEN,
localeMap: {
de_DE: {
crowdinId: 'de',
},
fr_FR: {
crowdinId: 'fr',
},
},
sourceLocale: 'en',
}),
],
// The rest of your config goes here
});
Database changes
This plugin adds three collections to your database:
crowdin-filescrowdin-article-directoriescrowdin-collection-directories
Enabled documents also get these fields, which aren't stored in your database:
syncTranslationsandsyncAllTranslationscheckboxes, which load translations on save (see virtual fields).crowdinArticleDirectory, a relationship to the document's Crowdin folder, looked up when the document is read.
For details, see how documents map to Crowdin.
Options
projectId (required)
Your Crowdin project ID.
{
projectId: 323731,
}
localeMap (required)
Map your Payload locales to Crowdin locale ids.
{
localeMap: {
de_DE: {
crowdinId: 'de',
}
}
}
sourceLocale (required)
The Payload locale that syncs to source translations (files) on Crowdin.
{
sourceLocale: 'en',
}
token
Your Crowdin API token: a personal access token on crowdin.com, or an Enterprise token. If empty, the plugin doesn't upload or delete anything in Crowdin.
{
token: process.env.CROWDIN_TOKEN,
}
organization
Your Crowdin Enterprise organization domain, for example acme for acme.crowdin.com. Leave it empty if you use crowdin.com.
{
organization: process.env.CROWDIN_ORGANIZATION,
}
directoryId
Crowdin directory ID to store translations. To get the directory ID without making an API call, inspect the page source of your folder in Sources > Files.
{
directoryId: 1169,
}
collections
Define an array of collection slugs for which the plugin is active.
{
collections: ['posts', 'categories'],
}
If undefined, the plugin will detect localized fields on all collections.
{
collections: undefined,
}
Use an empty array to disable all collections.
{
collections: [],
}
Use an object to define a condition that activates Crowdin based on the document data.
{
collections: [
'posts',
{
slug: 'categories',
condition: ({ doc }) => doc.translateWithCrowdin,
},
];
}
globals
Define an array of global slugs for which the plugin is active.
{
globals: ['nav'],
}
If undefined, the plugin will detect localized fields on all globals.
{
globals: undefined,
}
Use an empty array to disable all globals.
{
globals: [],
}
Use an object to define a condition that activates Crowdin based on the document data.
{
globals: [
{
slug: 'nav',
condition: ({ doc }) => doc.translateWithCrowdin,
},
],
}
slateToHtmlConfig
Controls how Payload Slate richText values are converted to HTML before being uploaded to Crowdin.
- Default behavior: if you do not provide this option, the plugin uses
payloadSlateToHtmlConfigfrom@slate-serializers/html(a Payload-oriented preset). - When to customize: if you have custom Slate node types/marks (or want to tweak table/link/image output).
- More docs & examples: see slate-serializers — docs & demos.
If you provide slateToHtmlConfig, it fully replaces the default preset (so you’ll typically want to start from the Payload preset and extend it).
{
slateToHtmlConfig: undefined,
}
Example: extend the default Payload preset to add/override element mappings.
import { payloadSlateToHtmlConfig } from '@slate-serializers/html'
crowdinSync({
// ...
slateToHtmlConfig: {
...payloadSlateToHtmlConfig,
elementMap: {
...payloadSlateToHtmlConfig.elementMap,
// example customization:
['table-row']: 'tr',
},
},
})
htmlToSlateConfig
Controls how translated HTML downloaded from Crowdin is converted back into Payload Slate richText JSON.
- Default behavior: if you do not provide this option, the plugin uses
payloadHtmlToSlateConfigfrom@slate-serializers/html. - When to customize: if you emit custom HTML from your
slateToHtmlConfig(or need custom parsing for attributes/styles). - More docs & examples: see slate-serializers — docs & demos.
{
htmlToSlateConfig: undefined,
}
Example: extend the default Payload preset to add/override tag handling.
import { payloadHtmlToSlateConfig } from '@slate-serializers/html'
crowdinSync({
// ...
htmlToSlateConfig: {
...payloadHtmlToSlateConfig,
elementTags: {
...payloadHtmlToSlateConfig.elementTags,
// example customization:
h1: () => ({ type: 'heading-one' }),
},
},
})
Serializer config reference (condensed)
The plugin config surface is intentionally small: you can override slateToHtmlConfig and htmlToSlateConfig, but the underlying serializer libraries have many knobs.
If you need to go deeper (custom tags/attributes/styles, whitespace filtering, DOM transforms), start here:
- Full docs & runnable examples: slate-serializers — docs & demos
Common places to look in the slate-serializers docs:
slateToDom(used under the hood byslateToHtml):elementMap,elementTransforms,markMap,markTransformshtmlToSlate:elementTags,elementStyleMap,htmlPreProcessString,filterWhitespaceNodes
These options only affect Slate fields. See serializer configuration for a worked example.
pluginCollectionAccess
access collection config to pass to all the Crowdin collections created by this plugin.
{
pluginCollectionAccess: undefined,
}
pluginCollectionAdmin
admin collection config to pass to all the Crowdin collections created by this plugin.
{
pluginCollectionAdmin: {
hidden: ({ user }) => !userIsAdmin({ user });
}
}
tabbedUI
Appends Crowdin tab onto your config using Payload's Tabs Field. If your collection is not already tab-enabled, meaning the first field in your config is not of type tabs, then one will be created for you called Content.
{
tabbedUI: true,
}
lexicalBlockFolderPrefix
Default lex.. Used as a prefix when constructing directory names for Lexical block fields in Crowdin.
{
lexicalBlockFolderPrefix: `blocks-`,
}
disableSelfClean
Default false. The plugin keeps records of the files and folders it creates in Crowdin. If someone deletes one of them in Crowdin, the plugin notices and repairs its records:
- When loading translations, a file that returns 404 has its
crowdin-filesrecord deleted. The file is uploaded again on the next save. - When saving, the plugin checks that the collection and document folders still exist in Crowdin. If a folder is missing, the stale record is deleted and the folder is created again.
The folder check costs one extra Crowdin API call per folder on each save. Set disableSelfClean: true to skip the checks and keep all records as they are.
{
disableSelfClean: true,
}
legacyArticleDirectoryLookup
Default false. Deprecated: use this only until you have run the backfill, then turn it off again. It will be removed in a future major version.
New directories are linked to their document when they are created (collectionDocument or globalSlug). Directories from earlier versions may have no link. With this option off, the plugin only finds a directory through that link.
Set legacyArticleDirectoryLookup: true if you still have unlinked directories and have not run backfillArticleDirectoryPolymorphicLinks. The plugin then also looks at a stored crowdinArticleDirectory id on the document, and at name within the collection's directory.
If you save a document whose directory is still unlinked while this option is off, the plugin throws instead of creating a second folder on Crowdin. The error names this option and the backfill.
{
legacyArticleDirectoryLookup: true,
}
deleteCrowdinFiles
Default false. When a document is deleted, or a localized field is emptied, the plugin always deletes its own records for the affected files and folders. Set deleteCrowdinFiles: true to delete the source files and folders in Crowdin as well.
This is off by default because deleting a source file in Crowdin also deletes its translations.
{
deleteCrowdinFiles: true,
}
Environment variables
| Variable | Effect |
|---|---|
PAYLOAD_CROWDIN_SYNC_ALWAYS_UPDATE=true | Upload all localized fields on every save, not only the ones that changed. |
PAYLOAD_CROWDIN_SYNC_USE_JOBS | Any non-empty value queues translation syncs as Payload jobs instead of running them during save. See virtual fields. |
PAYLOAD_CROWDIN_SYNC_VERBOSE | Any non-empty value logs details of translation syncs to the console, for debugging. |
By default, the plugin only uploads what changed:
- Any change to a localized
textortextareafield uploads the document'sfields.jsonagain. - A
richTextfield is uploaded only if its content changed. Each one has its own file in Crowdin.
PAYLOAD_CROWDIN_SYNC_ALWAYS_UPDATE is useful when you add the plugin to an existing site: saving a document uploads all of its content without you having to edit every field.
Sync translations
Upload source translations
On save draft or publish, content from localized fields in Collections and/or globals is organised into directories and files in your Crowdin project as configured in options.
See supported fields for which fields are sent, how nested fields are handled, and how to exclude fields.
Download translations
To load translations into Payload CMS, use either:
- virtual fields added to each localized document (convenient); or
- endpoints added to the API (can do a dry run of changes).
Virtual fields
When in a locale other than the source locale:
- Check the
Sync all translationscheckbox on a given collection document/global and save draft (loads translations as draft) or publish. - Check the
Sync translationscheckbox to synchronise for the current locale only.
The checkboxes appear once the document has been uploaded to Crowdin.
Loading translations for many locales can make saving slow. Set PAYLOAD_CROWDIN_SYNC_USE_JOBS to a non-empty value (for example true) to queue the work as Payload jobs instead. The plugin registers a crowdinSyncTranslations task and queues one job per locale. You need to run the job queue yourself; see Queues in the Payload docs.
Endpoints
API endpoints are added to the crowdin-article-directories collection.
Review (dry run)
To review translations, visit:
<payload-base-url>/api/crowdin-article-directories/<article-id>/review
e.g. https://my-payload-app.com/api/crowdin-article-directories/64a880bb87ef685285a4d9dc/review
A JSON object is returned that allows you to review what will be updated in the database. The JSON object will contain the following keys:
draftindicates that on update, a draft will be created rather than a published version. See Drafts | Payload CMS.sourcereview the source document. e.g. for theenlocale.translations<locale>e.g.es_EScurrentTranslationsall current localized fields and values.latestTranslationslocalized fields populated with values from Crowdin.changedboolean to indicate whether any changes have been made in Crowdin.
Update
To update translations, visit:
<payload-base-url>/api/crowdin-article-directories/<article-id>/update
e.g. https://my-payload-app.com/api/crowdin-article-directories/64a880bb87ef685285a4d9dc/update
The document will be updated and the same report will be generated as for a review.
Notes
- Pass the
draft=truequery parameter to update as a draft rather than a published version. - Pass a
localeparameter to perform a review/update for one locale only. e.g.locale=fr_FR. - The source locale (e.g.
en) is not affected. - Use the
excludeLocalesfield on documents in thecrowdin-article-directoriescollection to prevent some locales from being included in the review/update operation. - If supplied translations do not contain required fields, translation updates will not be applied and validation errors will be returned in the API response.
Delete documents
When you delete a document, the plugin deletes its crowdin-files records and its crowdin-article-directories record. Files in Crowdin are kept unless you set deleteCrowdinFiles.
If the document was never uploaded, or its Crowdin records are already gone, there is nothing to clean up and the delete goes ahead. An error deleting one file is logged and doesn't stop the rest.
Further documentation
- Supported fields
- How documents map to Crowdin
- Serializer configuration
- Development
- Engineering decisions
Note: This plugin is still in development. Planned features are listed in repo/planned-features.md.