OOctwin Platform Guides
Answering in Their Language

Octwin · Conversational engine

Your agent answers in the language they wrote to you in.

No menu. No press 1 for English. No extra AI call to work it out. The language is read from the customer's own words, on the message that arrives — and it can only ever be a language your pack actually speaks.

en Good morning. How can you help me today? 0.895
ar السلام عليكم، صباح الخير. كيف يمكنكم مساعدتي اليوم؟ 0.987
ur السلام علیکم، صبح بخیر۔ آپ میری کیا مدد کر سکتے ہیں؟ 0.750
Three sentences. One agent, one pack, no configuration. Scores are the detector's real output on these exact strings.

What the customer sees

The same hub, three times.

Below is one shipped pack — Binaa Building Supplies, a Riyadh building-materials wholesaler — answering three customers who each said good morning in their own language. Nobody chose a language. Nobody was asked.

English en · 0.895
The Binaa Building Supplies chat answering in English, with the How can we help? menu expanded to five rows: Price list, Cement, Steel, Request a quote, My requests.
“Hello there, good morning. How can you help me today?” language_source = detected
العربية ar · 0.987
The same chat answering in Arabic, right-to-left, with the same five-row menu translated: قائمة الأسعار, أسمنت, حديد, اطلب عرض سعر, طلباتي.
«السلام عليكم، صباح الخير. كيف يمكنكم مساعدتي اليوم؟» language_source = detected
اردو ur · 0.750
The same chat answering in Urdu, right-to-left, with the same five-row menu translated: قیمتوں کی فہرست, سیمنٹ, سریا, قیمت کی درخواست دیں, میری درخواستیں.
«السلام علیکم، صبح بخیر۔ آپ میری کیا مدد کر سکتے ہیں؟» language_source = detected

Look at what moved, not just the words. The agent's own sentence is translated — but so is the menu underneath it, both lines of every row, and the direction of the whole bubble. That is the platform drawing the same list three ways, not the model improvising a translation.

Worth knowing what this pack's default is: Binaa is configured default_settings.locale: ar. The English pane is therefore not the pack falling back — it is detection overriding the pack's own default, on the first message, before the customer had done anything but say hello.

How it decides

Evidence, then a roster it cannot argue with.

Every inbound message runs through the same two questions: what language is this?, and then is that a language this pack can actually serve? The second question is the one that keeps the feature honest.

The detector is a local statistical model — not a call to a large language model, and not an extra round-trip. It returns a language and a real probability, and anything at or above the gate counts as confident. It is then clamped to your pack's roster: a language you ship no words for is discarded rather than stored, because storing it would point every translated string at copy that does not exist.

The detector, on four real strings

Hello there, good morning. How can you help me today?
en 0.895 — over the gate, and a language this pack ships. Adopted.
السلام عليكم، صباح الخير. كيف يمكنكم مساعدتي اليوم؟
ar 0.987 — adopted.
مرحبا
fa 0.396 — Persian. This pack ships ar, en, ur, so the answer is discarded and nothing is stored.
Hi
en 0.125 — over the 0.1 gate by a hair. Two letters is almost no evidence.

Those last two rows are the honest part of this page. A bare greeting is weak evidence in any script. مرحبا is five letters and the model called it Persian; had this pack shipped Persian, it would have answered in Persian. A greeting is a coin-flip; a sentence is not. Everything in the next section exists because of that.

One more rule, easy to miss and load-bearing: a button tap is never evidence. Button and list titles are text your bot wrote, in the language it already chose — detecting on one would make the current language confirm itself forever. Only the customer's own prose is ever read.

When it changes its mind

Answer immediately. Remember slowly.

A wrong switch is visible to the customer; a decline is not. So the platform is quick to answer in a new language and deliberately slow to record one.

What is stored on the contact What happens on a confident detection
nothing yet Adopt it at once — there is nothing to contradict. This is every pane above.
the same language Confirmed. Any half-formed candidate is cleared.
a different language,
seen once before
This is the second sighting. Adopt it — the customer really has changed language.
a different language,
seen for the first time
Answer this turn in the new language, but do not store it yet. One mis-read costs a turn's wording and nothing permanent.
pinned by a person Detection is skipped entirely. See below.

That fourth row is the whole design in one line. An English product name inside an Arabic sentence, an order reference like ACC-2291, a one-word ok — none of them can flip a customer who has been speaking Arabic for a week.

“Twice” means twice among confident readings, not twice in a row. A tap, a photo, or an unreadable reply between two Arabic sentences does not break the streak, because those turns never reach the detector at all. Most readers assume the opposite.

The customer stays in control

Three words beat a language menu.

/lang is built into the platform. It works on every pack, with no YAML and no redeploy — including on a pack somebody else published.

/lang
Reports the current setting, and whether it is automatic or pinned. Deliberately not a list of choices — that would be the menu this feature exists to remove.
/lang en
Pins the conversation. Automatic detection may never overrule a person who said what they wanted, and that rule is enforced in the database, not in a branch someone could forget.
/lang auto
Hands them back to automatic detection. Without this, one mistyped code pinned a customer for the life of the contact, whatever they wrote afterwards.

On the record

Every contact says how its language got there.

Three values, and they answer the only question an operator ever asks about a conversation in the wrong language: who decided this?

unset Nobody has decided yet. The pack's own default is rendering, and the next readable sentence will settle it.
detected Read from the customer's own words. All three panes on this page.
explicit A person chose it — /lang, or a flow that had a real reason to. Detection will not touch it again until someone says /lang auto.

“The detector never ran” and “the detector declined” look identical from outside — and they have opposite fixes. One is /lang auto; the other is a message with more words in it. So the timeline records which of the two happened, rather than leaving a blank.

For pack authors

One line to declare it. Then the platform holds you to it.

If your pack speaks one language, there is nothing to do — say nothing and everything behaves as it always has. If it speaks more than one, this is the line worth adding today.

Declare what you speak

supported_languages: is optional, and it is a claim the platform checks in both directions against the locale files you actually ship. Declare a language you have no file for, or ship a file for a language you did not declare, and the deploy is refused.

# manifest.yaml — binaa-supply
supported_channels: [whatsapp, web]
supported_languages: [ar, en, ur]

default_settings:
  locale: ar        # the fallback, not the answer — detection overrides it

Why declare it at all when the files are right there on disk? Because intent is not on disk. A pack that shipped Arabic and English and then lost its last English file drops to one language and looks, byte for byte, exactly like a pack that was always Arabic. The declaration is what makes the difference visible.

Ship the same keys in every language

A pack shipping two or more languages must ship the same copy in each — the same keys, interpolating the same placeholders. A missing translation now fails the deploy.

This is a gate because the runtime is forgiving, and forgiveness here is indistinguishable from correctness. A key missing from locale.ur.yaml does not error — it silently falls through to another language your pack ships. So an Urdu customer reads one Arabic line in the middle of an Urdu card, and nothing anywhere reports a problem. A pack shipping a single language is never nagged about any of this.

Setting a language from a flow — rarely

set_language exists for the cases that genuinely need it: a compliance rule, or a CRM record that already knows. It is the exception, not the pattern — a language menu asks the customer what they already answered by typing.

What makes it safe is that it cannot store a language your pack does not ship. A refusal is a port you can handle, not a silent write — so you can tell the customer what you do speak:

- do: set_language
  args:
    language: '$tap.lang'   # a tag you ship, or "auto" to un-pin
  outputs:
    ok:
      - do: 
    unsupported:        # carries { requested, supported } — say what you DO speak
      - do: 

The previous way of doing this validated nothing: any string became explicit, detection was then blocked forever, every translated string missed, and the customer read your default while the record claimed something else. Silent, permanent, and untested — which is why the old path is gone.

If you do one thing

Add the line, then read your own logs.

Add supported_languages: to your manifest and deploy. The check runs in both directions, so it is the fastest way to find a language you meant to ship and didn't — the failure that otherwise reaches a customer as a blank line, or a sentence in a language they don't read.

Then look at the contacts on a live project. A wall of unset on a multilingual pack means your customers are sending greetings, not sentences — and greetings, as the panes above show, are the one input this feature is honestly bad at.