Skip to content
This repository was archived by the owner on Jan 14, 2026. It is now read-only.

n8n UI Customisations & Translation Infrastructure#6

Open
apl-intento wants to merge 52 commits intomasterfrom
RND-97-n-8-n-ui-customisations
Open

n8n UI Customisations & Translation Infrastructure#6
apl-intento wants to merge 52 commits intomasterfrom
RND-97-n-8-n-ui-customisations

Conversation

@apl-intento
Copy link
Collaborator

@apl-intento apl-intento commented Jan 13, 2026

📋 Overview

Major feature addition implementing a complete translation infrastructure for n8n with custom Intento packages, including agent-based architecture, supplier pattern, segmentation capabilities, and comprehensive test coverage.

🎯 Key Features

1. Agent-Based Architecture (@intento/agents)

  • TranslationAgent: Multi-step translation orchestration with supplier management
  • ✅ Request/Response patterns with correlation IDs and latency tracking
  • ✅ Error handling with HTTP status codes (408 timeout, 499 abort, 500 error)
  • ✅ Immutable data structures with Object.freeze()

2. Core Infrastructure (@intento/core)

  • AgentBase: Abstract base class for multi-step workflow execution
  • SupplierBase: Retriable supplier pattern with exponential backoff
  • ContextFactory: Decorator-based context creation with @mapTo
  • ExecutionContext: n8n execution context integration with IExecuteFunctions
  • Tracer: Structured logging and observability

3. Segmentation Package (@intento/segmentation)

  • EchoGarden Supplier: ICU-based text segmentation using echogarden library
  • ✅ Locale-aware boundary detection (word, sentence, grapheme, line)
  • ✅ Batch processing with configurable suppression patterns

4. Translation Package (@intento/translation)

  • DeepL Supplier: DeepL API integration with authentication
  • TranslationContext: Language pair configuration with Text union type
  • DelayContext: Rate limiting with noDelay/fixedDelay/randomDelay modes
  • ✅ Validation with throwIfInvalid() pattern
  • ✅ PII protection in logs (counts not content)

5. n8n Node Integration (@intento/nodes-base)

  • TranslationAgentV1.node.ts: n8n workflow node for translation
  • DryRunProvider: Testing/development supplier with configurable modes
  • ✅ Connection types for supplier chaining
  • ✅ UI configuration with INodeProperties

Note

Introduces Intento translation stack and streamlines CI/tooling.

  • Adds packages/@intento/core (agent framework: AgentBase, AgentRequest/Response, AgentError, ContextFactory with @mapTo, ExecutionContext, Tracer) with extensive unit tests and Jest/ESLint configs
  • Adds packages/@intento/agents (initial TranslationAgent + request/response/descriptor wiring) and project scaffolding
  • Updates CI workflows to ubuntu-latest runners; minor Docker manifest cleanup; Playwright/unit/lint jobs aligned; Codecov remains
  • Tooling tweaks: widen Prettier/Biome printWidth to 140, add .vscode launch, gitignore logs/, and start:dev script

Written by Cursor Bugbot for commit 33e2bdd. This will update automatically on new commits. Configure here.

delete condition for building only amd64
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive translation infrastructure for n8n with custom Intento packages. The implementation includes agent-based architecture, supplier pattern, ICU-based segmentation, DeepL translation integration, and extensive test coverage.

Changes:

  • Added translation infrastructure with @intento/translation package including contexts, suppliers (DeepL, DryRun), and request/response patterns
  • Implemented segmentation infrastructure with @intento/segmentation package featuring EchoGarden ICU-based text splitting
  • Created n8n node integration layer in @intento/nodes-base with tools for translation and segmentation
  • Added comprehensive test suites with 95%+ coverage targets
  • Fixed test infrastructure issues in @n8n/node-cli including environment variable handling and file cleanup delays

Reviewed changes

Copilot reviewed 197 out of 321 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/@n8n/nodes-langchain/nodes/ToolExecutor/ToolExecutor.node.json Updated subcategory from "Helpers" to "Primitives"
packages/@n8n/node-cli/src/utils/package-manager.test.ts Added missing tmpdir fixture parameter
packages/@n8n/node-cli/src/test-utils/temp-fs.ts Added 100ms delay before cleanup to prevent file handle issues
packages/@n8n/node-cli/src/commands/*.test.ts Set npm_config_user_agent for package manager detection
packages/@n8n/eslint-config/tsconfig.json Removed empty files array
packages/@intento/translation/* Complete translation package with contexts, suppliers, types
packages/@intento/segmentation/* Complete segmentation package with ICU-based text splitting
packages/@intento/nodes-base/* n8n node implementations for translation and segmentation tools

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

jobs:
build-arm64:
runs-on: ubuntu-22.04-arm
runs-on: ubuntu-latest
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARM64 build job runs on x86_64 runner

High Severity

The build-arm64 job's runner was changed from ubuntu-22.04-arm to ubuntu-latest. The job is explicitly named "build-arm64" with a step called "Setup and Build ARM64", but ubuntu-latest is an x86_64 runner, not ARM64. This means the job will not produce ARM64 artifacts as intended. The publish-to-docker-hub job depends on this build completing successfully, so release builds expecting ARM64 binaries will either fail or receive x86_64 binaries incorrectly.

Fix in Cursor Fix in Web

this.dryRunContext = ContextFactory.read<DryRunContext>(DryRunContext, this.functions, this.tracer);
this.splitContext = ContextFactory.read<SplitContext>(SplitContext, this.functions, this.tracer);
this.descriptor.batchLimit = this.splitContext.batchSize;
this.descriptor.segmentLimit = this.splitContext.segmentSize;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared descriptor object mutated causing cross-instance interference

Medium Severity

The DryRunSupplier constructor mutates this.descriptor.batchLimit and this.descriptor.segmentLimit directly. Since DryRunDescriptor is a shared exported constant and TranslationSupplierBase stores the descriptor reference without copying (this.descriptor = descriptor), all DryRunSupplier instances share the same descriptor object. When multiple instances are created with different splitContext configurations, each instance overwrites the shared descriptor's values, causing all instances to use the last-configured values instead of their own. This leads to incorrect batching behavior when multiple DryRunSuppliers are used in the same workflow.

Additional Locations (1)

Fix in Cursor Fix in Web

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good comment by the way

description: "Docker image to use (for docker-pull mode)"
required: false
default: "n8nio/n8n:nightly"
default: "n8nio/n8n:nightly"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate YAML keys break workflow input definitions

High Severity

Multiple workflow input fields have duplicate YAML keys for description and default values. For example, branch, test-mode, test-command, shards, and docker-image inputs each have their description and default lines appearing twice. Additionally, runs-on: ubuntu-latest appears twice at lines 70-71. YAML parsers may fail or silently use only the last value, causing unexpected behavior in CI.

Additional Locations (1)

Fix in Cursor Fix in Web

needs: determine-build-context
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
timeout-minutes: 30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate timeout-minutes YAML key in workflow

Medium Severity

The build-and-push-docker job has timeout-minutes: 30 specified twice on consecutive lines (86 and 87). Duplicate YAML keys are invalid and may cause parsing issues or unpredictable behavior depending on the YAML parser used.

Fix in Cursor Fix in Web

this.to = context.to;
this.from = context.from;

Object.freeze(this);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that is neccesarry

Comment on lines +42 to +43
const splitResult = await this.splitText(request, signal);
if (splitResult instanceof SupplyError) return new AgentError(request, splitResult.code, splitResult.reason);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a common pattern to return an error instead of throwing an exception?

Comment on lines +14 to +15
private segmentation?: SegmentsSupplierBase;
private translation?: TranslationSupplierBase[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename them to something more precise, like translationTools and segmentationTools

}

protected async execute(request: TranslationAgentRequest, signal: AbortSignal): Promise<TranslationAgentResponse | AgentError> {
signal.throwIfAborted();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to add event listener once then check aborted it or not synchoniusly in each function many times

if (result instanceof TranslationResponse) return result;
if (i >= this.translation!.length - 1) return result;
}
throw new Error('No translation suppliers were found.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be that translation just failed on each tool

* @template TS - Response type extending AgentResponseBase with latency tracking
*/
export abstract class AgentBase<TI extends AgentRequestBase, TS extends AgentResponseBase> {
private _initialize: Promise<void> | null = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of having this field? I do not see any use of it

try {
tracer.debug(`Fetching node parameter '${key}' for context '${context}' ...`);
// NOTE: extractValue resolves expressions and extracts from collection/fixedCollection parameters
const value = functions.getNodeParameter(key, 0, undefined, { extractValue: true });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 0 here?

// NOTE: Base delay scaled so exponential growth reaches maxDelayMs at final retry
const baseDelay = this.maxDelayMs / 2 ** (this.maxAttempts - 1);
// NOTE: Jitter uses uniform random variance to prevent thundering herd
const jitter = this.maxJitter * (Math.random() - 0.5) * 2 * baseDelay;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jitter can be negative. Is it expected?

*/
protected onError(request: TI, error: Error): SupplyError {
// NOTE: TimeoutError from AbortSignal.timeout() - execution exceeded configured limit
if (error instanceof DOMException && error.name === 'TimeoutError') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't DOMException related to the frontend part? If not, what is under the DOM abbreviation?

Comment on lines +38 to +44
constructor(descriptor: IDescriptor, connection: IntentoConnectionType, functions: IFunctions) {
this.connection = connection;
this.functions = functions;
this.tracer = new Tracer(descriptor, functions);
this.descriptor = descriptor;
this.executionContext = ContextFactory.read<ExecutionContext>(ExecutionContext, functions, this.tracer);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``

Suggested change
constructor(descriptor: IDescriptor, connection: IntentoConnectionType, functions: IFunctions) {
this.connection = connection;
this.functions = functions;
this.tracer = new Tracer(descriptor, functions);
this.descriptor = descriptor;
this.executionContext = ContextFactory.read<ExecutionContext>(ExecutionContext, functions, this.tracer);
}
constructor(
readonly descriptor: IDescriptor,
protected readonly connection: IntentoConnectionType,
protected readonly functions: IFunctions) {
this.tracer = new Tracer(descriptor, functions);
this.executionContext = ContextFactory.read<ExecutionContext>(ExecutionContext, functions, this.tracer);
}

And you can delete definition in 22-24 rows

* @param reason - Human-readable error description for n8n display
* @param isRetriable - Whether operation can be retried (false for permanent failures)
*/
constructor(request: SupplyRequestBase, code: number, reason: string, isRetriable: boolean) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplify class definition how I show in supplier-base.ts file comment

if (!this.agentRequestId || this.agentRequestId.trim() === '') throw new Error('"agentRequestId" is required');
if (!this.supplyRequestId || this.supplyRequestId.trim() === '') throw new Error('"supplyRequestId" is required');
if (!this.reason || this.reason.trim() === '') throw new Error('"reason" is required');
if (this.code < 100 || this.code > 599) throw new RangeError('"code" must be a valid HTTP status code (100-599)');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code 100 :D

* Used by @mapTo decorator to attach parameter key names to constructor parameters
* via reflect-metadata, enabling automatic extraction from n8n node parameters.
*/
export const CONTEXT_PARAMETER = Symbol('intento:context_parameter');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


signal?.throwIfAborted();

return await new Promise((resolve, reject) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return await make sense only in try catch blocks. You can just return promise and function will become sync

* RegExpValidator.isValidPattern("//") // false - empty pattern
* RegExpValidator.isValidPattern("/[/") // false - invalid regex
*/
static isValidPattern(pattern: string): boolean {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this function, there are valid patterns but they will fail match check.

protected async execute(request: TranslationRequest, signal: AbortSignal): Promise<TranslationResponse | SupplyError> {
signal.throwIfAborted();

const from = !request.from || request.from === '' ? 'auto' : request.from.toLowerCase();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const from = !request.from || request.from === '' ? 'auto' : request.from.toLowerCase();
const from = request.from?.toLowerCase() || 'auto'

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants