Skip to main content

JavaScript Script Step

The script step lets you run custom JavaScript (ES5) on each Part flowing through the pipeline. It uses the goja ES5 runtime -- a pure Go JavaScript engine with no CGo dependency.

How it works

The script tool receives each Part one at a time. For each Part, your script runs with access to a part object and three control functions: emit(), skip(), and log(). If your script calls neither emit() nor skip(), the Part passes through unchanged.

You can write your logic in either of two equivalent forms: as top-level code against the implicit part global (used throughout the examples below), or as a function process(part) that the tool calls once per Part (see Function form). Both have identical capabilities; the function form adds a return-value convenience.

The JavaScript API

The part object

part.type; // "block", "data", "media", "layer-start", "layer-end",
// "group-start", "group-end"

For block parts, the block property provides access to translatable content:

part.block.id; // Block ID string
part.block.translatable; // boolean

// Source runs (flat array)
part.block.source[0].content.text; // text of the first run

// Target runs by locale (object)
part.block.targets["fr"][0].content.text; // French target's first run text

emit(part)

Emit a modified (or new) Part to the output channel. If you call emit(), the original Part is not forwarded automatically -- only what you emit reaches downstream tools.

// Modify a target translation and emit
if (part.block.targets["fr"]) {
var seg = part.block.targets["fr"][0];
seg.content.text = seg.content.text.toUpperCase();
}
emit(part);

By default the script may read the source but only target edits are read back — the source is read-only (see Configuration reference).

skip()

Drop the current Part entirely. It will not reach downstream tools or the writer.

if (part.type === "block" && part.block.source[0].content.text === "") {
skip();
}

log(message)

Write a message to stderr for debugging.

log("Processing block: " + part.block.id);

Control flow summary

Script behaviorResult
No emit() or skip() calledPart passes through unchanged
emit(part) calledOnly emitted parts are forwarded
skip() calledPart is dropped
emit() called multiple timesAll emitted parts are forwarded (one-to-many)

Function form

Instead of writing top-level code against the implicit part global, you can define a process function. The tool calls it once per Part, passing the part as its argument:

function process(part) {
if (part.type === "block" && part.block.targets["fr"]) {
var seg = part.block.targets["fr"][0];
seg.content.text = seg.content.text.toUpperCase();
}
return part; // emit the (modified) part
}

Inside process, emit(), skip(), and log() behave exactly as in the implicit-globals form. As a convenience, the function's return value is also honored — but only when you have not already called emit() or skip() (an explicit call always wins):

process(part) returnsResult
a part objectthat part is emitted
an array of partsall are emitted (one-to-many)
nullthe part is dropped (equivalent to skip())
nothing (undefined)the part passes through unchanged

The function form works identically with --code, --script-file, and the YAML code/scriptFile config. It reads naturally for transforms that compute and return a result; the implicit-globals form is terser for simple filters. Source edits still require allowSourceMutation (see Configuration reference) in either form.

CLI usage

Inline code

kapi script -i input.xliff --code 'if (part.type === "block") {
var text = part.block.source[0].content.text;
if (text.length > 100) { skip(); }
}'

Script file

kapi script -i input.xliff --script-file filter.js

Where filter.js contains:

if (part.type === "block") {
var text = part.block.source[0].content.text;
if (text.length <= 5) {
skip();
}
}

YAML flow usage

Use the script step inline in a flow definition:

steps:
- tool: script
label: Filter short segments
config:
code: |
if (part.type === 'block') {
var text = part.block.source[0].content.text;
if (text.length < 3) {
skip();
}
}

- tool: pseudo-translate
config:
targetLocale: fr

Or reference an external file:

steps:
- tool: script
config:
scriptFile: ./scripts/filter.js

- tool: pseudo-translate
config:
targetLocale: fr

Examples

Filter by source text length

Skip blocks where the source text is shorter than a threshold:

if (part.type === "block") {
var text = part.block.source[0].content.text;
if (text.length < 10) {
skip();
}
}

Modify target text

Append a marker to all French translations:

if (part.type === "block" && part.block.targets["fr"]) {
var seg = part.block.targets["fr"][0];
seg.content.text = seg.content.text + " [REVIEW]";
emit(part);
}

Conditional routing

Only pass translatable blocks through to downstream tools:

if (part.type !== "block") {
// Let structural parts (layers, data) pass through
emit(part);
} else if (part.block.translatable) {
emit(part);
} else {
skip();
}

Transform source text

Normalize whitespace in the source before translation. Source edits are ignored by default — the source is read-only to the script (immutability contract). Opt in with allowSourceMutation: true, and place the step ahead of the steps that should observe the rewritten source — typically first (see flow authoring):

steps:
- tool: script
config:
allowSourceMutation: true
code: |
if (part.type === 'block') {
var text = part.block.source[0].content.text;
text = text.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
part.block.source[0].content.text = text;
emit(part);
}
- tool: ai-translate

Log and pass through

Inspect the pipeline without changing anything:

if (part.type === "block") {
log("Block " + part.block.id + ": " + part.block.source[0].content.text);
}
// No emit() or skip() -- part passes through unchanged

Configuration reference

PropertyTypeDescription
sourcestringMode selector: inline (default) or file
codestringInline JavaScript code (ES5)
scriptFilestringPath to a .js file
allowSourceMutationbooleanPermit the script to modify the source text. Off by default — the source is read-only and source edits are ignored unless this is set.

Provide either code or scriptFile. The optional source field selects the mode explicitly (inline or file) for UI and validation; when omitted, the mode is inferred from whichever of code/scriptFile is set.

Notes

  • The runtime is ES5 only (no let, const, arrow functions, or template literals). Use var for variable declarations.
  • Each tool instance gets its own goja runtime, so there is no shared state between parallel pipeline branches.
  • The script runs synchronously for each Part. Long-running scripts will block the pipeline.
  • Target text edits on block parts are read back. Source edits are read back only when allowSourceMutation: true; otherwise the source is read-only. Changes to other Part types are not persisted.