From 8354f56e394ad69df4b4fae5cca845dbf2800ccd Mon Sep 17 00:00:00 2001 From: Steven Thompson <44806974+thompsonsj@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:25:58 +0000 Subject: [PATCH] feat(plugin): deep merge source lexical block values into translation (#221) * feat(plugin): deep merge source lexical block values into translation * style(plugin): cleanup * test(lexicaleditorwithmultipleblocks): test nested fields lex blocks --- .../fields/lexicalEditorWithBlocks.ts | 28 +++-- ...al-editor-with-blocks-inside-array.test.ts | 1 - ...cal-editor-with-multiple-blocks.fixture.ts | 9 +- ...exical-editor-with-multiple-blocks.test.ts | 44 ++++--- .../tests/plugin/getLocalizedFields.test.ts | 86 +++++++++---- docs/engineering.md | 117 ++++++++++++++++++ .../payload-crowdin-sync/files/document.ts | 8 +- .../api/payload-crowdin-sync/translations.ts | 44 ++++--- plugin/src/lib/utilities/lexical/index.ts | 12 +- 9 files changed, 271 insertions(+), 78 deletions(-) diff --git a/dev/src/lib/collections/fields/lexicalEditorWithBlocks.ts b/dev/src/lib/collections/fields/lexicalEditorWithBlocks.ts index d088ce5..2c69c58 100644 --- a/dev/src/lib/collections/fields/lexicalEditorWithBlocks.ts +++ b/dev/src/lib/collections/fields/lexicalEditorWithBlocks.ts @@ -60,15 +60,29 @@ export const lexicalEditorWithBlocks: RichTextField = { imageAltText: "CTA", fields: [ { - name: "text", - type: "text", - }, - { - name: "href", - type: "text", + name: "link", + type: "group", + fields: [ + { + name: "text", + type: "text", + }, + { + name: "href", + type: "text", + }, + { + name: "type", + type: "select", + options: [ + "internal", + "external", + ] + } + ] }, { - name: "type", + name: "style", type: "select", options: [ "primary", diff --git a/dev/src/lib/tests/collections/lexical-editor-with-blocks-inside-array.test.ts b/dev/src/lib/tests/collections/lexical-editor-with-blocks-inside-array.test.ts index 5a62942..0e30b1f 100644 --- a/dev/src/lib/tests/collections/lexical-editor-with-blocks-inside-array.test.ts +++ b/dev/src/lib/tests/collections/lexical-editor-with-blocks-inside-array.test.ts @@ -1347,7 +1347,6 @@ describe('Lexical editor with multiple blocks', () => { }, { "fields": { - "blockName": "", "blockType": "highlight", "color": "yellow", "content": { diff --git a/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.fixture.ts b/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.fixture.ts index ad39847..c4555b9 100644 --- a/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.fixture.ts +++ b/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.fixture.ts @@ -33,9 +33,12 @@ export const fixture = { "id": "65d67d2591c92e447e7472f7", "blockName": "", "blockType": "cta", - "text": "Download payload-crowdin-sync on npm!", - "href": "https://www.npmjs.com/package/payload-crowdin-sync", - "type": "primary" + "link": { + "text": "Download payload-crowdin-sync on npm!", + "href": "https://www.npmjs.com/package/payload-crowdin-sync", + "type": "external", + }, + "select": "primary" } }, { diff --git a/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.test.ts b/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.test.ts index 4c73988..d3e13e5 100644 --- a/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.test.ts +++ b/dev/src/lib/tests/fields/lexical-editor-with-multiple-blocks.test.ts @@ -48,10 +48,13 @@ describe('Lexical editor with multiple blocks', () => { { "blockName": "", "blockType": "cta", - "href": "https://www.npmjs.com/package/payload-crowdin-sync", "id": "65d67d2591c92e447e7472f7", - "text": "Download payload-crowdin-sync on npm!", - "type": "primary", + "link": { + "href": "https://www.npmjs.com/package/payload-crowdin-sync", + "text": "Download payload-crowdin-sync on npm!", + "type": "external", + }, + "select": "primary", }, { "blockName": "", @@ -172,10 +175,13 @@ describe('Lexical editor with multiple blocks', () => { "fields": { "blockName": "", "blockType": "cta", - "href": "https://www.npmjs.com/package/payload-crowdin-sync", "id": "65d67d2591c92e447e7472f7", - "text": "Download payload-crowdin-sync on npm!", - "type": "primary", + "link": { + "href": "https://www.npmjs.com/package/payload-crowdin-sync", + "text": "Download payload-crowdin-sync on npm!", + "type": "external", + }, + "select": "primary", }, "format": "", "type": "block", @@ -448,10 +454,13 @@ describe('Lexical editor with multiple blocks', () => { "fields": { "blockName": "", "blockType": "cta", - "href": "https://www.npmjs.com/package/payload-crowdin-sync", "id": "65d67d2591c92e447e7472f7", - "text": "Download payload-crowdin-sync on npm!", - "type": "primary", + "link": { + "href": "https://www.npmjs.com/package/payload-crowdin-sync", + "text": "Download payload-crowdin-sync on npm!", + "type": "external", + }, + "select": "primary", }, "format": "", "type": "block", @@ -1020,8 +1029,10 @@ describe('Lexical editor with multiple blocks', () => { blocks: { '65d67d2591c92e447e7472f7': { cta: { - text: 'Téléchargez payload-crowdin-sync sur npm!', - href: 'https://www.npmjs.com/package/payload-crowdin-sync', + link: { + text: 'Téléchargez payload-crowdin-sync sur npm!', + href: 'https://www.npmjs.com/package/payload-crowdin-sync', + }, }, }, '65d67d8191c92e447e7472f8': { @@ -1184,12 +1195,13 @@ describe('Lexical editor with multiple blocks', () => { }, { "fields": { - "blockName": "", "blockType": "cta", - "href": "https://www.npmjs.com/package/payload-crowdin-sync", "id": "65d67d2591c92e447e7472f7", - "text": "Téléchargez payload-crowdin-sync sur npm!", - "type": "primary", + "link": { + "href": "https://www.npmjs.com/package/payload-crowdin-sync", + "text": "Téléchargez payload-crowdin-sync sur npm!", + "type": "external", + }, }, "format": "", "type": "block", @@ -1265,7 +1277,6 @@ describe('Lexical editor with multiple blocks', () => { }, { "fields": { - "blockName": "", "blockType": "highlight", "color": "green", "content": { @@ -1309,7 +1320,6 @@ describe('Lexical editor with multiple blocks', () => { }, { "fields": { - "blockName": "", "blockType": "imageText", "id": "65d67e2291c92e447e7472f9", "image": "65d67e6a7fb7e9426b3f9f5f", diff --git a/dev/src/lib/tests/plugin/getLocalizedFields.test.ts b/dev/src/lib/tests/plugin/getLocalizedFields.test.ts index d134634..0177a80 100644 --- a/dev/src/lib/tests/plugin/getLocalizedFields.test.ts +++ b/dev/src/lib/tests/plugin/getLocalizedFields.test.ts @@ -378,15 +378,29 @@ describe('payload-crowdin-sync: getLexicalBlockFields', () => { { "fields": [ { - "name": "text", - "type": "text", - }, - { - "name": "href", - "type": "text", + "fields": [ + { + "name": "text", + "type": "text", + }, + { + "name": "href", + "type": "text", + }, + { + "name": "type", + "options": [ + "internal", + "external", + ], + "type": "select", + }, + ], + "name": "link", + "type": "group", }, { - "name": "type", + "name": "style", "options": [ "primary", "secondary", @@ -476,6 +490,8 @@ describe('payload-crowdin-sync: getLexicalBlockFields', () => { "slug": "imageText", }, ], + "name": "blocks", + "type": "blocks", } `); }); @@ -1813,15 +1829,29 @@ describe('payload-crowdin-sync: getLocalizedFields', () => { { "fields": [ { - "name": "text", - "type": "text", - }, - { - "name": "href", - "type": "text", + "fields": [ + { + "name": "text", + "type": "text", + }, + { + "name": "href", + "type": "text", + }, + { + "name": "type", + "options": [ + "internal", + "external", + ], + "type": "select", + }, + ], + "name": "link", + "type": "group", }, { - "name": "type", + "name": "style", "options": [ "primary", "secondary", @@ -4635,15 +4665,29 @@ describe('payload-crowdin-sync: getLocalizedFields', () => { { "fields": [ { - "name": "text", - "type": "text", - }, - { - "name": "href", - "type": "text", + "fields": [ + { + "name": "text", + "type": "text", + }, + { + "name": "href", + "type": "text", + }, + { + "name": "type", + "options": [ + "internal", + "external", + ], + "type": "select", + }, + ], + "name": "link", + "type": "group", }, { - "name": "type", + "name": "style", "options": [ "primary", "secondary", diff --git a/docs/engineering.md b/docs/engineering.md index 8fd7233..db50ca3 100644 --- a/docs/engineering.md +++ b/docs/engineering.md @@ -14,3 +14,120 @@ Rationale: - Organise Lexical block fields into a folder (easier to review) - Remove the need for richTextBlockFieldNameSeparator. the use of this led to field names that don't exist. - Logical - fields in blocks are easier to treat as a new 'document' rather than continuing to name them with increasing long field names to describe where they are nested. + +### Source Lexical block content + +Lexical block content from the source locale (e.g. `en`) is merged back into Lexical block translations. + +#### Source content + +Consider the following source content taken from a Lexical field block. + +```json +{ + "id": "668579b65fbcb419a79ccb4c", + "blockName": "", + "blockType": "highlight", + "color": "yellow", + "title": "Highlight block title", + "content": { + "root": { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "Highlight block rich text content.", + "type": "text", + "version": 1 + } + ] + } + ] + } + } +} +``` + +Note: Whether or not Lexical block fields are localized is ignored - the Lexical field itself is localized making localization status for block fields redundant. + +#### Send to Crowdin + +The following is sent for translation (the plugin treats Lexical field blocks as an imaginary collection containing a single `blocks` field): + +`blocks.json` + +```json +{ + "blocks": { + "668579b65fbcb419a79ccb4c": { + "title": "Highlight block title", + } + } +} +``` + +`blocks.668579b65fbcb419a79ccb4c.content.html` + +```html +
Highlight block rich text content.
+``` + +#### Receive from Crowdin + +`blocks.json` + +```json +{ + "blocks": { + "668579b65fbcb419a79ccb4c": { + "title": "Titre du bloc « Surligner »", + } + } +} +``` + +`blocks.668579b65fbcb419a79ccb4c.content.html` + +```html +« Surligner » bloque le contenu en texte enrichi.
+``` + +#### Build blocks for Payload update + +Note that **non-rich text** fields are restored from the source content when translated Lexical blocks are added. + +This is because a given block configuration likely contains fields that are not sent to Crowdin such as `select` fields which may be important for block field values. Lexical blocks are 'isolated', so these fields should be merged into translations based on source values. + +Note that source block values are stored in a `sourceBlocks` field on the `CrowdinFiles` collection for convenient access. + +```json +{ + "id": "668579b65fbcb419a79ccb4c", + "blockType": "highlight", + "color": "yellow", + "title": "Titre du bloc « Surligner »", + "content": { + "root": { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "« Surligner » bloque le contenu en texte enrichi.", + "type": "text", + "version": 1 + } + ] + } + ] + } + } +} +``` \ No newline at end of file diff --git a/plugin/src/lib/api/payload-crowdin-sync/files/document.ts b/plugin/src/lib/api/payload-crowdin-sync/files/document.ts index 0ea3144..b9faea7 100644 --- a/plugin/src/lib/api/payload-crowdin-sync/files/document.ts +++ b/plugin/src/lib/api/payload-crowdin-sync/files/document.ts @@ -1,7 +1,7 @@ import { PluginOptions } from '../../../index'; import { payloadCrowdinSyncFilesApi } from "."; import { CrowdinArticleDirectory, CrowdinFile } from './../../../payload-types'; -import { CollectionConfig, Document, GlobalConfig, PayloadRequest, RichTextField } from 'payload/types'; +import { BlockField, CollectionConfig, Document, GlobalConfig, PayloadRequest, RichTextField } from 'payload/types'; import { Config } from 'payload/config'; import { isEmpty } from 'lodash'; @@ -234,7 +234,7 @@ export class payloadCrowdinSyncDocumentFilesApi extends payloadCrowdinSyncFilesA collection: CollectionConfig | GlobalConfig; }) { // brittle check for Lexical value - improve this detection. Type check? Anything from Payload to indicate the type? - let html, blockContent + let html, blockContent, blockConfig: BlockField | undefined const isLexical = Object.prototype.hasOwnProperty.call(value, "root") if (isLexical) { const field = findField({ @@ -248,7 +248,7 @@ export class payloadCrowdinSyncDocumentFilesApi extends payloadCrowdinSyncFilesA html = await convertLexicalToHtml(value, editorConfig) // no need to detect change - this has already been done on the field's JSON object blockContent = value && extractLexicalBlockContent(value.root) - const blockConfig = editorConfig && getLexicalBlockFields(editorConfig) + blockConfig = editorConfig && getLexicalBlockFields(editorConfig) if (blockContent && blockContent.length > 0 && blockConfig) { // directory name must be unique from file names - Crowdin API const folderName = `${this.pluginOptions.lexicalBlockFolderPrefix}${name}` @@ -316,7 +316,7 @@ export class payloadCrowdinSyncDocumentFilesApi extends payloadCrowdinSyncFilesA { name: fieldName, type: 'blocks', - blocks: blockConfig.blocks, + blocks: blockConfig ? blockConfig.blocks : [], } ], }, diff --git a/plugin/src/lib/api/payload-crowdin-sync/translations.ts b/plugin/src/lib/api/payload-crowdin-sync/translations.ts index c800e01..8f53810 100644 --- a/plugin/src/lib/api/payload-crowdin-sync/translations.ts +++ b/plugin/src/lib/api/payload-crowdin-sync/translations.ts @@ -7,7 +7,7 @@ import { Payload } from "payload"; import { CrowdinHtmlObject, PluginOptions } from "../../types"; import deepEqual from "deep-equal"; import { - Block, + BlockField, CollectionConfig, Field, GlobalConfig, @@ -31,7 +31,7 @@ import { Config, CrowdinFile } from "../../payload-types"; import { getCollectionConfig, getFile, getFileByDocumentID, getFiles, getFilesByDocumentID, getLexicalFieldArticleDirectory } from "../helpers"; import { getLexicalBlockFields, getLexicalEditorConfig } from "../../utilities/lexical"; import { getRelationshipId } from "../../utilities/payload"; -import { assign, isEmpty } from "lodash"; +import { isEmpty, merge } from "lodash"; interface IgetLatestDocumentTranslation { collection: string; @@ -508,9 +508,7 @@ export class payloadCrowdinSyncTranslationsApi { file, crowdinArticleDirectoryId, }: { - blockConfig: { - blocks: Block[] - }, + blockConfig: BlockField | undefined, file: CrowdinFile, locale: string, crowdinArticleDirectoryId: string, @@ -519,12 +517,11 @@ export class payloadCrowdinSyncTranslationsApi { const fieldName = `blocks` // find a way to `getTranslation` or getPayloadTranslation` for the subfolder. // add ability to pass `fields` and `crowdinArticleDirectory` to `getTranslation`. That will do it. + if (!blockConfig) { + return + } const fields: Field[] = [ - { - name: fieldName, - type: 'blocks', - blocks: blockConfig.blocks, - } + blockConfig, ] let docTranslations: { [key: string]: any } = {}; // add json fields @@ -561,16 +558,25 @@ export class payloadCrowdinSyncTranslationsApi { // merge non-localized fields back in if (!isEmpty(file.fileData?.sourceBlocks)) { const sourceBlocks = JSON.parse(`${file.fileData?.sourceBlocks}`) || [] - const processed = (docTranslations['blocks'] || []).map((translatedBlock: {id: string}) => { - const sourceBlock = sourceBlocks.find((block: any) => block.id === translatedBlock.id) - if (sourceBlock) { - return assign(sourceBlock, translatedBlock) - } - return translatedBlock + + // build a source translation crowdinJsonObject + const sourceCrowdinJsonObject = buildCrowdinJsonObject({ + doc: { + blocks: sourceBlocks, + }, + fields, + isLocalized: (field) => !!(field), }) - return { - blocks: processed - } + // convert source translation crowdinJsonObject to payloadUpdateObject + // we are only interested in merging back json fields - otherwise html fields will deep merge creating hybrid rich text content containing both translations + // this is why a 'sourceCrowdinHtmlObject' is not built + const sourcePayloadUpdateObject = buildPayloadUpdateObject({ + crowdinJsonObject: sourceCrowdinJsonObject, + fields, + isLocalized: (field) => !!(field), + }) + + return merge({}, sourcePayloadUpdateObject, docTranslations) } return docTranslations diff --git a/plugin/src/lib/utilities/lexical/index.ts b/plugin/src/lib/utilities/lexical/index.ts index bef8af5..d798ac7 100644 --- a/plugin/src/lib/utilities/lexical/index.ts +++ b/plugin/src/lib/utilities/lexical/index.ts @@ -1,5 +1,5 @@ import { LexicalRichTextAdapter, SanitizedEditorConfig } from "@payloadcms/richtext-lexical"; -import { Block, RichTextField } from "payload/types"; +import { BlockField, RichTextField } from "payload/types"; import { SerializedRootNode, SerializedLexicalNode } from "lexical" import { SerializedBlockNode } from "@payloadcms/richtext-lexical"; @@ -18,13 +18,13 @@ export const getLexicalEditorConfig = (field: RichTextField) => { return undefined } -export const getLexicalBlockFields = (editorConfig: SanitizedEditorConfig): { - blocks: Block[] -} | undefined => { +export const getLexicalBlockFields = (editorConfig: SanitizedEditorConfig): BlockField | undefined => { const blocks = editorConfig.resolvedFeatureMap.get('blocks') if (blocks) { - return blocks.props as { - blocks: Block[] + return { + blocks: (blocks.props as any).blocks, + name: "blocks", + type: "blocks", } } return undefined