Building Multilingual Bot Flows¶
How to make one bot flow answer each customer in their own language — without duplicating the whole flow per language.
Backward compatibility: Everything here is opt-in and additive. Flows built before multilingual support keep working exactly as-is. The localized fields are optional; when a node has none, the engine uses its single base text just like before.
1. How language selection works¶
Every running flow has a per-conversation bag of session variables. The engine picks the language from a variable named lang (or language) at send time:
{{lang}}=ar→ Arabic content is sent{{lang}}=fr→ French content is sent- variable missing / no matching translation → falls back to the Default (base) content, then to
en
Language resolution happens automatically on the server. You never configure it directly — you just make sure a lang variable exists in the session.
Language codes are lowercase ISO-639 (en, ar, fr, es, de, pt, …). en_other is normalized to en.
2. Step-by-step¶
Step 1 — Ask the user their language (once)¶
Add a Quick Reply node near the start of the flow as a language picker:
| Field | Value |
|---|---|
| Message Body | Choose your language / اختر لغتك / Choisissez votre langue |
| Collect answer into a variable | On |
| Variable name | lang |
| Save | Button payload (fallback to title) |
Buttons (set the payload to the language code, the title to the display name):
| Title | Payload |
|---|---|
| English | en |
| العربية | ar |
| Français | fr |
Because Save = payload, tapping "العربية" stores lang = "ar" in the session. Every node after this can now localize itself.
Prefer to auto-detect instead of asking? Set
langwith a Set Variable node from any source you trust — e.g.{{contact.language}}, a CRM field, or the result of a Classify Intent / AI Response node that detects language. The only requirement is that alang(orlanguage) variable is present before localized nodes run.
Step 2 — Author localized Quick Replies¶
Open any Quick Reply node. At the top you'll see Content language:
- Leave it on Default (fallback) and write your base message body + buttons (this is what older flows already had — and what any unconfigured language falls back to).
- Type a code (e.g.
ar) into Add language → Add. - The selector switches to AR. Now the Message Body field and each button title edit the Arabic text. Leave any field blank to fall back to Default for that one string.
- Repeat for
fr,es, … - Save.
Button payloads stay shared across languages (they're codes, not display text), so your branching/Switch logic doesn't change between languages. Only the visible labels are translated.
At send time the node automatically renders the body + button labels in {{lang}}, falling back to Default per-string when a translation is missing.
Step 3 — Inject dynamic values¶
Localized text supports {{variable}} interpolation, in both the body and the button labels:
Default: Hi {{contact.first_name}}, your order {{order_id}} is ready.
AR: مرحبًا {{contact.first_name}}، طلبك {{order_id}} جاهز.
{{contact.first_name}} and {{order_id}} resolve from session/contact variables regardless of language. (If a variable is missing, the {{token}} is left untouched — never blanked.)
3. Localizing plain Send Message nodes¶
The Send Message node has its own localization for WhatsApp templates / freeform smart-send:
localizedBodies— per-language freeform bodies (used when inside the 24-hour window)templateNameBase— a Meta template base name; the engine appends the language suffix (__ar,__fr) and falls back to__en
So for template-driven sends, create your Meta templates as myflow_welcome__en, myflow_welcome__ar, … and set the base name; the right one is chosen by {{lang}}.
4. When you still need to branch on language¶
Native localization covers message bodies and button labels. If a different path must run per language (different nodes, not just different text — e.g. Arabic routes to an Arabic-speaking agent), branch explicitly:
- After the language picker, add a Switch node with
inputVariable = {{lang}}. - Add cases
en,ar,fr, … each leading to its own sub-path. - Use Jump To at the end of each path to converge back to a shared continuation, avoiding duplicate tails.
Use localization for content, branching for flow shape. Most flows only need localization.
5. Reference — what is / isn't localizable today¶
| Element | Localizable | How |
|---|---|---|
| Quick Reply message body | ✅ | Per-language editor (localizedMessage) |
| Quick Reply button labels | ✅ | Per-language editor (localizedTitles per button) |
| Quick Reply button payloads | — (shared) | Codes, not display text — intentionally shared |
| Send Message (template/freeform) | ✅ | localizedBodies + templateNameBase (__lang suffix) |
Any {{variable}} in text |
✅ | Interpolated in every language |
| Quick Reply header/footer text | ❌ (base only) | Use the message body for localized content |
| Collect Input prompt, AI prompts, etc. | ➖ | Use {{variable}} + branch on {{lang}} if needed |
6. Developer notes (data shape & resolution)¶
Stored on the Quick Reply node's data (all optional — absent on legacy nodes):
{
"message": "Choose an option", // base / fallback body
"localizedMessage": { "ar": "اختر خيارًا", "fr": "Choisissez" },
"languages": ["ar", "fr"], // configured locales (editor only)
"buttons": [
{
"id": "btn-1",
"title": "Yes", // base / fallback label
"payload": "yes", // shared across languages
"localizedTitles": { "ar": "نعم", "fr": "Oui" }
}
]
}
Resolution order at send time (for example, for a quick-reply message):
qrLang = resolveFlowLanguage("", flowVars)→ readsflowVars.lang ?? flowVars.language, elseen.- Body =
resolveLocalizedBody(localized_message, qrLang) ?? message, theninterpolateFlowString(...). - Each button title =
resolveLocalizedBody(button.localizedTitles, qrLang) ?? button.title, then interpolated.
resolveLocalizedBody returns null when there's no map → legacy/non-localized nodes take the exact same path as before (base text, no behavior change). The deploy step (n8n-deploy-flow, quickReply case) only includes localized_message in the callback payload when it is present, so non-localized nodes produce a byte-identical payload to pre-feature builds.
See also¶
- Bot Builder — Complete Guide — every node, trigger, and scenario
- Bot Flows — feature overview
- Templates — WhatsApp template management
Last Updated: June 2026