Skip to main content

Payload Crowdin Sync Plugin

Automatically upload/sync localized fields from the default locale to Crowdin. Load translations from Crowdin into Payload CMS.

Table of contents:

Install

  • Payload version 3.0 or higher is required
#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,
organization: process.env.CROWDIN_ORGANIZATION,
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

Localized documents have an extra field added to them - crowdinArticleDirectory.

For details, see crowdin.md.

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. If empty, changes to files are disabled.

{
token: 'xxxxxxx',
}

organizationId (required)

Your Crowdin organization ID.

{
organizationId: 200000000,
}

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

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-`,
}

Environment variables

Set PAYLOAD_CROWDIN_SYNC_ALWAYS_UPDATE=true to update all localized fields in Crowdin when an article is created/updated.

By default, updates will only be sent to Crowdin in the following scenarios.

  • At least one of the localized text fields has changed: any change to a localized text field updates the compiled fields.json that is sent to Crowdin.
  • A richText field is changed. Individual richText fields will only be updated on Crowdin if the content has changed - each field has its own file on Crowdin.

It is useful to have a convenient way of forcing all localized fields to update at once. For example, if the plugin is activated on an existing install, it is convenient to trigger all updates on Crowdin for a given article without having to change every richText field or one of the text fields.

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

Exclude fields

In some cases, you may wish to localize fields but prevent them being synced to Crowdin. e.g. a slug field that autogenerates based on title.

There are two ways to indicate to the plugin that a field should be ignored. In your field config:

  • add { custom: { crowdinSync: { disable: true } }} (preferred); or
  • include the string Not sent to Crowdin. Localize in the CMS. in admin.description (may be removed in a future version).

Example:

import type { Field } from 'payload';

const field: Field = {
name: 'textLocalizedField',
type: 'text',
localized: true,
custom: {
crowdinSync: {
disable: true,
},
},
};

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

Set a PAYLOAD_CROWDIN_SYNC_USE_JOBS environment variable to a non-empty value (e.g. true) to add translation sync operations as jobs. This is a useful way to prevent hooks from running slowly. You'll need to execute the job queue seperately. See Queues | Docs | Payload CMS.

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.

Further documentation

Note: This plugin is still in development. A todo list is maintained at development.md.