Skip to content

Commit 8bcaa92

Browse files
authored
feat[plugin][smartling]: ENG-9736 visual context upload, exclude blocks, edit/add/delete string instructions (#4218)
## Description This PR has 3 new features 1. Uploading Visual context to smartling. This PR has the settings part of it 2. Ability to exclude/include blocks from translations into Smartling 3. Add/Edit/Delete string instructions to blocks **Loom** For features 1 and 2 -> https://www.loom.com/share/d6d8c880667c4453b43c7c8b7af4de0a For feature 3 -> https://www.loom.com/share/060f9094fc3c4865972cef2f4624e1d7 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > - **Smartling plugin** > - New `enableVisualContextCapture` setting in plugin settings. > - Context menu expanded: exclude/include any block from future translations; new actions to `Add/Edit/Delete` per-string `instructions` stored on `meta`. > - Minor job actions logic refinements; version bumped and `@builder.io/utils` dependency updated to `1.1.29`. > > - **Utils (`translation-helpers.ts`)** > - Translation extraction/application now respects nested `meta.excludeFromTranslation` via depth tracking; skips all descendant nodes within excluded blocks. > - Applies exclusion uniformly to `Text`, `Core:Button`, `Symbol`, and custom components with `localizedTextInputs`. > - Snapshots updated to reflect button text handling and additional fields; package version bumped to `1.1.29`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 91b1cb6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5f25224 commit 8bcaa92

File tree

7 files changed

+175
-49
lines changed

7 files changed

+175
-49
lines changed

packages/utils/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@builder.io/utils",
3-
"version": "1.1.27",
3+
"version": "1.1.29",
44
"description": "Utils for working with Builder.io content",
55
"main": "./dist/index.js",
66
"scripts": {

packages/utils/src/__snapshots__/translation-helpers.test.ts.snap

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,31 @@ Object {
322322
}
323323
`;
324324

325+
exports[`getTranslateableFields from carousel content and custom component with subFields to match snapshot 1`] = `
326+
Object {
327+
"blocks.builder-06d3f6da74fb4054ad311afc1dda3675#text": Object {
328+
"instructions": "Visit https://builder.io/fiddle/... for more details",
329+
"value": "<p>I am slide second</p>",
330+
},
331+
"blocks.builder-0a10ae48b6314221bbb7d06d068d623d#text": Object {
332+
"instructions": "Visit https://builder.io/fiddle/... for more details",
333+
"value": "<p>I am slide 1</p>",
334+
},
335+
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#listItems.0.field1": Object {
336+
"instructions": "Visit https://builder.io/fiddle/... for more details",
337+
"value": "<p>text 1 value</p>",
338+
},
339+
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#listItems.1.field1": Object {
340+
"instructions": "Visit https://builder.io/fiddle/... for more details",
341+
"value": "<p>text 1.1 value</p>",
342+
},
343+
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#title.text": Object {
344+
"instructions": "Visit https://builder.io/fiddle/... for more details",
345+
"value": "<p>custom component subField input</p>",
346+
},
347+
}
348+
`;
349+
325350
exports[`getTranslateableFields from content to match snapshot 1`] = `
326351
Object {
327352
"blocks.block-id#text": Object {
@@ -360,6 +385,14 @@ Object {
360385
"instructions": "Visit https://builder.io/fiddle/... for more details",
361386
"value": "en-us headings!",
362387
},
388+
"blocks.button-localized-text-id#text": Object {
389+
"instructions": "Button with pre-localized text",
390+
"value": "Click Me!",
391+
},
392+
"blocks.button-plain-text-id#text": Object {
393+
"instructions": "Button with plain text",
394+
"value": "Cute Baby",
395+
},
363396
"metadata.seo": Object {
364397
"instructions": "Visit https://builder.io/fiddle/... for more details",
365398
"value": Object {
@@ -386,28 +419,3 @@ Object {
386419
},
387420
}
388421
`;
389-
390-
exports[`getTranslateableFields from carousel content and custom component with subFields to match snapshot 1`] = `
391-
Object {
392-
"blocks.builder-06d3f6da74fb4054ad311afc1dda3675#text": Object {
393-
"instructions": "Visit https://builder.io/fiddle/... for more details",
394-
"value": "<p>I am slide second</p>",
395-
},
396-
"blocks.builder-0a10ae48b6314221bbb7d06d068d623d#text": Object {
397-
"instructions": "Visit https://builder.io/fiddle/... for more details",
398-
"value": "<p>I am slide 1</p>",
399-
},
400-
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#listItems.0.field1": Object {
401-
"instructions": "Visit https://builder.io/fiddle/... for more details",
402-
"value": "<p>text 1 value</p>",
403-
},
404-
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#listItems.1.field1": Object {
405-
"instructions": "Visit https://builder.io/fiddle/... for more details",
406-
"value": "<p>text 1.1 value</p>",
407-
},
408-
"blocks.builder-23e0618256ab40799ecf6504bc57fa1c#title.text": Object {
409-
"instructions": "Visit https://builder.io/fiddle/... for more details",
410-
"value": "<p>custom component subField input</p>",
411-
},
412-
}
413-
`;

packages/utils/src/translation-helpers.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,27 @@ export function getTranslateableFields(
220220

221221
// blocks
222222
if (blocks) {
223+
// Track how many levels deep we are inside excluded blocks
224+
let excludedDepth = 0;
225+
223226
traverse(blocks).forEach(function (el) {
224-
if (this.key && el && el.meta?.localizedTextInputs) {
227+
// Check if this element starts an exclusion zone
228+
const startsExclusion = el && el.meta?.excludeFromTranslation;
229+
if (startsExclusion) {
230+
excludedDepth++;
231+
}
232+
233+
// After processing children, decrement the counter if this element started an exclusion
234+
this.after(function () {
235+
if (startsExclusion) {
236+
excludedDepth--;
237+
}
238+
});
239+
240+
// Skip translation if we're inside an excluded block (excludedDepth > 0)
241+
const isExcluded = excludedDepth > 0;
242+
243+
if (this.key && el && el.meta?.localizedTextInputs && !isExcluded) {
225244
const localizedTextInputs = el.meta.localizedTextInputs as string[];
226245
if (localizedTextInputs && Array.isArray(localizedTextInputs)) {
227246
localizedTextInputs
@@ -238,7 +257,7 @@ export function getTranslateableFields(
238257
});
239258
}
240259
}
241-
if (el && el.id && el.component?.name === 'Text' && !el.meta?.excludeFromTranslation) {
260+
if (el && el.id && el.component?.name === 'Text' && !isExcluded) {
242261
const componentText = el.component.options.text;
243262
results[`blocks.${el.id}#text`] = {
244263
value:
@@ -249,7 +268,7 @@ export function getTranslateableFields(
249268
};
250269
}
251270

252-
if (el && el.id && el.component?.name === 'Core:Button' && !el.meta?.excludeFromTranslation) {
271+
if (el && el.id && el.component?.name === 'Core:Button' && !isExcluded) {
253272
const componentText = el.component.options?.text;
254273
if (componentText) {
255274
const textValue = typeof componentText === 'string'
@@ -264,7 +283,7 @@ export function getTranslateableFields(
264283
}
265284
}
266285

267-
if (el && el.id && el.component?.name === 'Symbol') {
286+
if (el && el.id && el.component?.name === 'Symbol'&& !isExcluded) {
268287
const symbolInputs = Object.entries(el.component?.options?.symbol?.data) || [];
269288
if (symbolInputs.length) {
270289
const basePath = `blocks.${el.id}.symbolInput`;
@@ -316,8 +335,27 @@ export function applyTranslation(
316335
}
317336

318337
if (blocks) {
338+
// Track how many levels deep we are inside excluded blocks
339+
let excludedDepth = 0;
340+
319341
traverse(blocks).forEach(function (el) {
320-
if (el && el.id && el.component?.name === 'Symbol') {
342+
// Check if this element starts an exclusion zone
343+
const startsExclusion = el && el.meta?.excludeFromTranslation;
344+
if (startsExclusion) {
345+
excludedDepth++;
346+
}
347+
348+
// After processing children, decrement the counter if this element started an exclusion
349+
this.after(function () {
350+
if (startsExclusion) {
351+
excludedDepth--;
352+
}
353+
});
354+
355+
// Skip translation if we're inside an excluded block (excludedDepth > 0)
356+
const isExcluded = excludedDepth > 0;
357+
358+
if (el && el.id && el.component?.name === 'Symbol' && !isExcluded) {
321359
const symbolInputs = Object.entries(el.component?.options?.symbol?.data) || [];
322360
if (symbolInputs.length) {
323361
const transformedMeta = {};
@@ -368,7 +406,7 @@ export function applyTranslation(
368406
el &&
369407
el.id &&
370408
el.component?.name === 'Text' &&
371-
!el.meta?.excludeFromTranslation &&
409+
!isExcluded &&
372410
translation[`blocks.${el.id}#text`]
373411
) {
374412
const localizedValues =
@@ -405,7 +443,7 @@ export function applyTranslation(
405443
el &&
406444
el.id &&
407445
el.component?.name === 'Core:Button' &&
408-
!el.meta?.excludeFromTranslation &&
446+
!isExcluded &&
409447
translation[`blocks.${el.id}#text`]
410448
) {
411449
const localizedValues =
@@ -440,7 +478,7 @@ export function applyTranslation(
440478
}
441479

442480
// custom components
443-
if (el && el.id && el.meta?.localizedTextInputs) {
481+
if (el && el.id && el.meta?.localizedTextInputs && !isExcluded) {
444482
// there's a localized input
445483
const keys = el.meta?.localizedTextInputs as string[];
446484
let options = el.component.options;

plugins/smartling/package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/smartling/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@builder.io/plugin-smartling",
3-
"version": "0.0.23-11",
3+
"version": "0.0.23-13",
44
"description": "",
55
"keywords": [],
66
"main": "dist/plugin.system.js",
@@ -125,7 +125,7 @@
125125
"typescript": "^3.0.3"
126126
},
127127
"dependencies": {
128-
"@builder.io/utils": "1.1.26",
128+
"@builder.io/utils": "1.1.29",
129129
"fast-json-stable-stringify": "^2.1.0",
130130
"lodash": "^4.17.21",
131131
"object-hash": "^3.0.0",

plugins/smartling/src/plugin.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ Builder.register('plugin', {
163163
advanced: false,
164164
requiredPermissions: ['admin'],
165165
},
166+
{
167+
name: 'enableVisualContextCapture',
168+
friendlyName: 'Enable Visual Context Capture',
169+
type: 'boolean',
170+
defaultValue: false,
171+
helperText: 'Enable automatic visual context capture for translations',
172+
requiredPermissions: ['admin'],
173+
},
166174
],
167175
onSave: async actions => {
168176
const pluginPrivateKey = await appState.globalState.getPluginPrivateKey(pkg.name);
@@ -460,7 +468,7 @@ const initializeSmartlingPlugin = async () => {
460468
}
461469
const element = selectedElements[0];
462470
const isExcluded = element.meta?.get(transcludedMetaKey);
463-
return element.component?.name === 'Text' && !isExcluded;
471+
return !isExcluded;
464472
},
465473
onClick(elements) {
466474
elements.forEach(el => el.meta.set('excludeFromTranslation', true));
@@ -476,13 +484,85 @@ const initializeSmartlingPlugin = async () => {
476484
}
477485
const element = selectedElements[0];
478486
const isExcluded = element.meta?.get(transcludedMetaKey);
479-
return element.component?.name === 'Text' && isExcluded;
487+
return isExcluded;
480488
},
481489
onClick(elements) {
482490
elements.forEach(el => el.meta.set('excludeFromTranslation', false));
483491
},
484492
});
485493

494+
495+
registerContextMenuAction({
496+
label: 'Add String Instructions',
497+
showIf(selectedElements) {
498+
if (selectedElements.length !== 1) {
499+
// todo maybe apply for multiple
500+
return false;
501+
}
502+
const element = selectedElements[0];
503+
return element.meta?.get('instructions') === undefined;
504+
},
505+
async onClick(elements) {
506+
if (elements.length !== 1) {
507+
// todo maybe apply for multiple
508+
return false;
509+
}
510+
const instructions = await appState.dialogs.prompt({
511+
placeholderText: 'Enter string instructions for translation',
512+
});
513+
if (instructions) {
514+
elements[0].meta.set('instructions', instructions);
515+
appState.snackBar.show('String instructions added to content');
516+
}
517+
},
518+
});
519+
520+
registerContextMenuAction({
521+
label: 'Edit String Instructions',
522+
showIf(selectedElements) {
523+
if (selectedElements.length !== 1) {
524+
// todo maybe apply for multiple
525+
return false;
526+
}
527+
const element = selectedElements[0];
528+
return element.meta?.get('instructions') !== undefined;
529+
},
530+
async onClick(elements) {
531+
if (elements.length !== 1) {
532+
// todo maybe apply for multiple
533+
return false;
534+
}
535+
const element = elements[0];
536+
const instructions = element.meta?.get('instructions');
537+
if (instructions !== undefined) {
538+
const newInstructions = await appState.dialogs.prompt({
539+
placeholderText: 'Enter new string instructions for translation',
540+
defaultValue: instructions,
541+
});
542+
if (newInstructions) {
543+
element.meta.set('instructions', newInstructions);
544+
appState.snackBar.show('String instructions updated');
545+
}
546+
}
547+
},
548+
});
549+
550+
registerContextMenuAction({
551+
label: 'Delete String Instructions',
552+
showIf(selectedElements) {
553+
if (selectedElements.length !== 1) {
554+
// todo maybe apply for multiple
555+
return false;
556+
}
557+
const element = selectedElements[0];
558+
return element.meta?.get('instructions') !== undefined;
559+
},
560+
onClick(elements) {
561+
elements[0].meta.delete('instructions');
562+
appState.snackBar.show('String instructions deleted');
563+
},
564+
});
565+
486566
registerContentAction({
487567
label: 'Add to translation job',
488568
showIf(content, model) {

0 commit comments

Comments
 (0)