Hi @drew,
Thank you for this detailed question and sorry about the unclear docs! I’ll update them ASAP. In the meantime, though, here is some sample code that will hopefully help.
Given a record like this:
This code should let you add a German locale while keeping the English contents intact:
import {buildBlockRecord, buildClient, LogLevel} from "@datocms/cma-client";
import type {Document as StructuredTextDocument} from 'datocms-structured-text-utils';
import type {SimpleSchemaTypes} from "@datocms/cma-client-node";
// The actual update code
(async () => {
const client = buildClient({
apiToken: process.env.DATO_API_TOKEN,
logLevel: LogLevel.BODY // Shows you the HTTP bodies being sent and received, to make it easier to understand
})
const recordId = 'YtTI-MSMS2246DqXHet0jA' // The specific record ID you want to update
const blockItemTypeId = 'OI-LIDMeQ4a7KkgFO7DIUQ'; // The block's item TYPE id, which you can get from its URL or schema
const currentRecord: ExampleRecord = await client.items.find(recordId, {
nested: true // IMPORTANT: Retrieves existing block CONTENTS instead of just their IDs
}
)
// console.log(currentRecord)
/* Returns something like:
{
id: 'YtTI-MSMS2246DqXHet0jA',
type: 'item',
title: { en: 'English title' },
modular_content_field: { en: [ [Object], [Object] ] },
structured_text_field: { en: { schema: 'dast', document: [Object] } },
item_type: { id: 'IdfCc2k-RCWrlGVfbwYpBA', type: 'item_type' },
...
} */
const newBlockContent: ExampleBlockContents = {
item_type: {type: 'item_type', id: blockItemTypeId},
paragraph_field_inside_block: 'Beispiel-Block auf Deutsch'
}
const newTranslatedBlock = buildBlockRecord( newBlockContent)
const newTranslatedStructuredText: StructuredTextDocument = {
"schema": "dast",
"document": {
"type": "root",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "span",
"value": "Deutsch in Strukturiertem Text"
}
]
},
{
type: "block",
item: newTranslatedBlock as unknown as string, // This is a bug in our types, I think... it should accept a new block record instead of an ID
}
]
}
}
const updatedRecordContent: ExampleRecord = {
...currentRecord,
title: {
...currentRecord.title, // Destructure & copy existing locales
de: 'Beispieltext auf Deutsch' // Add new German string
},
modular_content_field: {
...currentRecord.modular_content_field, // Destructure & copy existing locales
de: [newTranslatedBlock]
},
structured_text_field: {
...currentRecord.structured_text_field,
de: newTranslatedStructuredText
}
}
const updatedRecord = await client.items.update(recordId, updatedRecordContent);
console.log(JSON.stringify(updatedRecord, null, 2))
})()
// Example types you can use if you want
type Localized<T> = {
en?: T;
de?: T;
[locale: string]: T | undefined; // For other locales, or you can explicitly specify them like 'en' and 'de'
};
type ExampleBlockContents = SimpleSchemaTypes.ItemUpdateSchema & {
paragraph_field_inside_block: string;
}
type ExampleRecord = SimpleSchemaTypes.ItemUpdateSchema & {
title?: Localized<string>;
modular_content_field?: Localized<SimpleSchemaTypes.ItemUpdateSchema[]>;
structured_text_field?: Localized<StructuredTextDocument>;
}
Running it adds the German text:
Important notes:
- To keep the existing locales, you have to first fetch the record’s current state
- Then you use ES6 destructuring to pass along the existing data to the new record you’re building
- You can use the
nested: true
param on a record to fetch all its block contents, instead of having to recursively look up its blocks individually
- When constructing a new block, use the
buildBlockRecord()
helper function and do NOT provide an existing ID. You only use an existing ID to reference an existing block, not to build a new one.
Additional Details
This is what the CMA JS client is sending over the wire (click to expand):
{
"data": {
"id": "YtTI-MSMS2246DqXHet0jA",
"type": "item",
"attributes": {
"title": {
"de": "Beispieltext auf Deutsch",
"en": "English title"
},
"modular_content_field": {
"de": [
{
"type": "item",
"attributes": {
"paragraph_field_inside_block": "Beispiel-Block auf Deutsch"
},
"relationships": {
"item_type": {
"data": {
"id": "OI-LIDMeQ4a7KkgFO7DIUQ",
"type": "item_type"
}
}
}
}
],
"en": [
{
"type": "item",
"attributes": {
"paragraph_field_inside_block": "English paragraph inside a modular content field"
},
"relationships": {
"item_type": {
"data": {
"id": "OI-LIDMeQ4a7KkgFO7DIUQ",
"type": "item_type"
}
}
},
"id": "cU57fLeASIaIRetgG96vpw"
},
{
"type": "item",
"attributes": {
"paragraph_field_inside_block": "Another English paragraph inside a modular content field"
},
"relationships": {
"item_type": {
"data": {
"id": "OI-LIDMeQ4a7KkgFO7DIUQ",
"type": "item_type"
}
}
},
"id": "Hz1n0jbLS-ikG3yUL3XwbQ"
}
]
},
"structured_text_field": {
"de": {
"schema": "dast",
"document": {
"type": "root",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "span",
"value": "Deutsch in Strukturiertem Text"
}
]
},
{
"type": "block",
"item": {
"type": "item",
"attributes": {
"paragraph_field_inside_block": "Beispiel-Block auf Deutsch"
},
"relationships": {
"item_type": {
"data": {
"id": "OI-LIDMeQ4a7KkgFO7DIUQ",
"type": "item_type"
}
}
}
}
}
]
}
},
"en": {
"schema": "dast",
"document": {
"children": [
{
"children": [
{
"type": "span",
"value": "English structured text"
}
],
"type": "paragraph"
},
{
"item": {
"type": "item",
"attributes": {
"paragraph_field_inside_block": "English paragraph inside structured text"
},
"relationships": {
"item_type": {
"data": {
"id": "OI-LIDMeQ4a7KkgFO7DIUQ",
"type": "item_type"
}
}
},
"id": "a8RTQCc7RQGq3LI_TcITow"
},
"type": "block"
},
{
"children": [
{
"type": "span",
"value": ""
}
],
"type": "paragraph"
}
],
"type": "root"
}
}
}
},
"relationships": {
"item_type": {
"data": {
"id": "IdfCc2k-RCWrlGVfbwYpBA",
"type": "item_type"
}
},
"creator": {
"data": {
"id": "627975",
"type": "organization"
}
}
},
"meta": {
"created_at": "2025-03-25T17:54:16.208+00:00",
"updated_at": "2025-03-25T19:15:35.522+00:00",
"published_at": "2025-03-25T19:15:35.574+00:00",
"publication_scheduled_at": null,
"unpublishing_scheduled_at": null,
"first_published_at": "2025-03-25T17:54:18.322+00:00",
"is_valid": true,
"is_current_version_valid": true,
"is_published_version_valid": true,
"status": "published",
"current_version": "e98-Yf0LQrK6yD-JM2ihqw",
"stage": null
}
}
}
Sorry, I know none of this is super-clear right now. I’ll update the docs and add a better example soon, but I hope this code helps a bit in the interim!
EDIT: In case anyone else comes across this later, a small clarification: You technically don’t need nested: true
if you just want to preserve the existing English content. Without that param, existing blocks will be returned as an array of IDs, which you can pass verbatim back into an update call in order to preserve the existing content. That means passing an array of existing IDs is functionally equivalent to passing back an array of their nested contents (which would generate new blocks with new IDs, with the exact same content). However, because later in this thread the OP also asks about how to copy the English content over to other locales, keeping their structures intact but translating their strings, it is clearer to just keep the nested: true
param intact so that the block contents, not just their IDs, can be used to mirror the content into other locales.