Skip to main content

Getting started

Install the plugin, configure it, and sync content between Payload and Crowdin.

Table of contents:

Install​

Requirements:

# 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-files
  • crowdin-article-directories
  • crowdin-collection-directories

Enabled documents also get these fields, which aren't stored in your database:

  • syncTranslations and syncAllTranslations checkboxes, 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 payloadSlateToHtmlConfig from @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 payloadHtmlToSlateConfig from @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:

Common places to look in the slate-serializers docs:

  • slateToDom (used under the hood by slateToHtml): elementMap, elementTransforms, markMap, markTransforms
  • htmlToSlate: 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-files record 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​

VariableEffect
PAYLOAD_CROWDIN_SYNC_ALWAYS_UPDATE=trueUpload all localized fields on every save, not only the ones that changed.
PAYLOAD_CROWDIN_SYNC_USE_JOBSAny non-empty value queues translation syncs as Payload jobs instead of running them during save. See virtual fields.
PAYLOAD_CROWDIN_SYNC_VERBOSEAny 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 text or textarea field uploads the document's fields.json again.
  • A richText field 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.

Screenshot 2024-02-06 at 22 02 38

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 translations checkbox on a given collection document/global and save draft (loads translations as draft) or publish.
  • Check the Sync translations checkbox to synchronise for the current locale only.
Screenshot 2024-02-06 at 22 08 48

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:

  • draft indicates that on update, a draft will be created rather than a published version. See Drafts | Payload CMS.
  • source review the source document. e.g. for the en locale.
  • translations
    • <locale> e.g. es_ES
      • currentTranslations all current localized fields and values.
      • latestTranslations localized fields populated with values from Crowdin.
      • changed boolean 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=true query parameter to update as a draft rather than a published version.
  • Pass a locale parameter 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 excludeLocales field on documents in the crowdin-article-directories collection 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​

Note: This plugin is still in development. Planned features are listed in repo/planned-features.md.