Three months ago I had no knowledge of application programming whatsoever. I’ve been in Spain for 8 months and I came here with an idea in my head. With the support of Gemini AI from Antigravity, I started to materialize my idea of a language translation application to communicate with some friends from Germany. My workflow is composed of 4 nodes (Webhook1 + HTTP REQUEST + JavaScript + Respond to Webhook). When I click to execute the workflow, it runs in less than 3 seconds. But my application, which is still incomplete and is currently web-based, which is activated from an index file that is hosted locally on my desktop, fails to translate to the target language and ends up giving me the original message every time. I don’t know what else to do anymore.
Your workflow running in under 3 seconds means the automation is executing, but it does not prove the translation output is being passed correctly. The issue is probably not “translation” itself; it is likely a data-mapping problem between your web page, the webhook, the HTTP request, the JavaScript node, and the final response.
I would debug it node by node:
-
Check the Webhook node output. Confirm it receives both the original text and the target language.
-
Check the HTTP Request node input. Confirm it sends the correct text and target language to the translation API.
-
Check the HTTP Request node output. Confirm the API actually returns translated text.
-
Check the JavaScript node. It may be extracting the wrong JSON field or falling back to the original message.
-
Check the Respond to Webhook node. Confirm it returns
translatedText, not the original text. -
Check the frontend JavaScript. Confirm it displays the translated field from the webhook response.
Since your app always returns the original text, I suspect there is fallback logic like translatedText || originalText, or the response node/frontend is displaying the original text variable. Remove fallback during debugging so the workflow fails clearly when no translation is found.
Translation itself should be straightforward. We have already built a transcription-style pipeline, and translating transcript text is usually just another processing step. The harder part is making sure the correct variable moves through the chain. If you share the Webhook output, HTTP Request output, JavaScript node code, and frontend fetch code, we can probably identify the exact break point.
It is quite impressive to go from zero programming knowledge to a working webhook pipeline so you shouldn’t give up, you are closer than you think.
The reply above covers all of the debugging steps well though one thing worth adding: the most common cause of “returns the original message” in this setup is the JavaScript node grabbing the wrong field from the API response.
You should try adding a console.log or using n8n’s built-in output preview to see exactly what the HTTP Request node returns. Try specifically looking at the JSON structure. The translated text is usually nested a level deeper than expected, so something like response.data.translations[0].translatedText depending on which API you are using.
If you share which translation API you are hitting and paste in the JavaScript code that was used, someone can probably spot it within 2 minutes.
"Thank you so much @pratham_gupta and @AnthonyAtXRay for taking the time to respond! Your debugging points are excellent.
To give you a bit more context: at first, when I was doing initial testing, the entire flow seemed to respond well. However, the real issues in the browser started showing up more forcefully recently. After analyzing it thoroughly, we suspect that the internal logic of n8n is fine, but the failure occurs in the final communication: I’m running the web application locally by opening the index.html file directly from my computer (file://). Everything points to Google Chrome or local network policies blocking the response coming from the n8n IP due to CORS.
Besides, to top it off, I just ran out of prepaid credits for the Google API, so the workflow got paused.
To validate whether the variable mapping is as precise as you suggest, I’m sharing below the structure of my JavaScript node and the general flow (without credentials). Do you think my theory that the file:// format and CORS are to blame for the browser block makes sense, or do you see some logic error in the variables?"
{
“nodes”: [
{
“parameters”: {
“respondWith”: “json”,
“responseBody”: “={{ $json }}”,
“options”: {
“responseCode”: 200,
“responseHeaders”: {
“entries”: [
{
“name”: “Content-Type”,
“value”: “application/json”
},
{
“name”: “Access-Control-Allow-Origin”,
“value”: “"
},
{
“name”: “Access-Control-Allow-Headers”,
“value”: "”
}
]
}
}
},
“type”: “n8n-nodes-base.respondToWebhook”,
“typeVersion”: 1.5,
“position”: [
752,
112
],
“id”: “7eb0364c-e218-47a7-ba7e-f9d8fe8bb8f8”,
“name”: “Respond to Webhook”
},
{
“parameters”: {
“httpMethod”: “POST”,
“path”: “98bd0d2c-fcf4-49b7-b501-72c81c5d9d44”,
“responseMode”: “responseNode”,
“options”: {
“allowedOrigins”: “*”
}
},
“type”: “n8n-nodes-base.webhook”,
“typeVersion”: 2.1,
“position”: [
80,
112
],
“id”: “0af6f18b-0e1a-4d5c-bca6-7573f4a31cb9”,
“name”: “Webhook1”,
“webhookId”: “c1603781-563f-4655-9b6d-1170e847c6f0”
},
{
“parameters”: {
“method”: “POST”,
“url”: “https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key=TU_API_KEY_AQUÍ”,
“sendBody”: true,
“specifyBody”: “json”,
“jsonBody”: “={{ { “contents”: [{ “parts”: [{ “text”: $json.body.text + " - Translate this to " + $json.body.target_lang }] }] } }}”,
“options”: {}
},
“id”: “06da87ea-d639-41fa-a859-50ae26c35224”,
“name”: “HTTP Request”,
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.4,
“position”: [
304,
112
]
},
{
“parameters”: {
“jsCode”: “const geminiResponse = $input.first().json;\nconst translatedText = geminiResponse.candidates[0].content.parts[0].text.trim();\n\nreturn [\n {\n json: {\n status: “success”,\n original_text: $(‘Webhook1’).item.json.body.text,\n translated_text: translatedText,\n audio_base64: “”\n }\n }\n];”
},
“id”: “576fa8a4-896f-4d33-ad1a-8cfe5d204ec8”,
“name”: “Code in JavaScript”,
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
528,
112
]
}
],
“connections”: {
“Webhook1”: {
“main”: [
[
{
“node”: “HTTP Request”,
“type”: “main”,
“index”: 0
}
]
]
},
“HTTP Request”: {
“main”: [
[
{
“node”: “Code in JavaScript”,
“type”: “main”,
“index”: 0
}
]
]
},
“Code in JavaScript”: {
“main”: [
[
{
“node”: “Respond to Webhook”,
“type”: “main”,
“index”: 0
}
]
]
}
],
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “ANONYMOUS_INSTANCE_ID”
}
}
Freddy, the screenshots would help, but based on what you described, I would narrow this down to one specific question:
At which point does the translated text disappear?
Your n8n workflow may be running correctly, but the web app may still be reading the wrong field.
I would test it in this order:
- Webhook node
Confirm the incoming data contains something like:
{
"text": "hello",
"targetLanguage": "German"
}
- HTTP Request node
Open the node execution output and check whether the translation API returns the translated text.
For example, something like:
{
"translatedText": "Hallo"
}
or sometimes it may be nested deeper, like:
{
"data": {
"translation": "Hallo"
}
}
- JavaScript node
This is a common break point. The code may be reading the wrong field.
For debugging, do not return the original message as fallback. Return an obvious error instead:
const output = $json.translatedText;
if (!output) {
throw new Error("No translated text found. Check HTTP Request output field name.");
}
return [
{
json: {
translatedText: output
}
}
];
If the translated text is nested, you need to adjust the field path based on the real HTTP Request output.
- Respond to Webhook node
Make sure it returns the translated value, not the original input.
Example response:
{
"translatedText": "={{$json.translatedText}}"
}
- Frontend fetch code
Your local web page may be doing something like this:
resultBox.innerText = data.text;
when it should be:
resultBox.innerText = data.translatedText;
Because your app always gives back the original message, my strongest guess is one of these:
-
the JavaScript node is falling back to the original text
-
the Respond to Webhook node is mapped to the original field
-
the frontend is displaying the original field instead of the translated field
-
the HTTP Request node is returning translation in a nested JSON path, but the JS node is reading the wrong path
Best debugging move: temporarily return the full HTTP Request output directly from Respond to Webhook. Then test from the web app and check what your browser actually receives. Once you see the real JSON structure, mapping the translated field should be easy.
Your theory about CORS is exactly right, your workflow’s JSON exposes the specific issue.
Your Webhook node has “allowedOrigins” set to "asterisk"which is what it should be. But your Respond to Webhook node has the “Access-Control-Allow-Origin” set to an empty string. That will need to be changed to “asterisk” as well, otherwise you have Chrome block the response even when the request gets through.
The next issue is the file:// protocol itself. Chrome ends up blocking the fetch requests from file:// pages to external URLs regardless of the CORS headers. It’s a simple fix- instead of opening index.html directly, you serve it from a local server.
If you have Python installed: python -m http.server 8080 in the folder containing your index.html, and then open up http://localhost:8080
As far as the Google API credits go, once you fill those up, the JavaScript node logic itself looks correct. And the variable mapping to candidates[0].content.parts[0].text is the right path for Gemini responses.