Part 2 of 7 · Translation relay series ~4 min read

How a message gets its language detected

The relay can’t translate a message until it knows what language the message is in. That sounds trivial — and for a long, clearly-written paragraph it is. But real customer messages are short, full of order numbers, signed with a name in a third language, and sometimes written half in one language and half in another. This post walks through how a message gets in (two ways), how it gets cleaned down to the words that matter, and how the relay reads its language without guessing wrong on the hard ones.

Key takeaways

  • Two ways in feed one thread: email through SES inbound and a web widget through a Function URL.
  • Every message is cleaned first — signatures, quoted history, and footers are stripped before detection.
  • A cheap first pass reads the language; a careful fallback handles short, mixed, or code-heavy notes.
  • The detected language and a confidence read are saved on the thread so later turns can reuse them.
  • If the relay still isn’t sure, the message is marked “language unclear” for a person, never guessed silently.

Two ways in, one cleaned message

Two inputs and a cleanup step feed one thread A diagram with three vertical lane columns at the top and a single unified row at the bottom. Lane one, Email: a customer sends a message to your support address; SES inbound writes the raw email to S3; an intake Lambda walks the message to the plain-text body. Lane two, Web widget: a visitor types into the small chat box on your site; the widget posts the text to a Lambda Function URL with the thread id. Lane three, Cleanup and detect: whichever way the message arrived, a cleanup step strips the signature, the quoted reply history, and footers like sent-from-my-phone, leaving only the new words; then a cheap language read runs, and for short, mixed, or code-heavy notes a careful fallback runs a stronger read. All three lanes converge on the same conversation thread, which stores the cleaned text, the detected language, and a confidence score. A note at the bottom: the relay never guesses silently — if the language is still unclear after the fallback, the message is flagged for a person. Lane 1 · SES inbound Email • Customer mails your support address • SES writes raw email to S3 • Intake Lambda pulls the body • Onto the thread by sender + subject Lane 2 · Function URL Web widget • Visitor types in the chat box • Widget posts to a Function URL • Carries the thread id • Same thread as email, one place Lane 3 · clean + read Cleanup and detect • Strip signature, history, footers • Cheap first pass reads the language • Careful fallback for short or mixed • Save language + confidence on thread Conversation thread (one per customer) cleaned text · language · confidence · channel · original kept · thread id stored in DynamoDB — later turns reuse the customer’s known language to translate-in step The relay never guesses silently — if the language is unclear, the message is flagged for a person.
Fig 2. Two inputs and a cleanup-and-detect step converge on one thread. Email and the web widget both land in the same place; the cleanup step strips noise; a cheap read with a careful fallback figures out the language and saves it on the thread.

Lane 1: email

The most common way in. You point your support address — or a forwarding copy of it — at the relay through Amazon SES. When a customer emails, SES writes the raw message to an S3 bucket, and that write triggers a small intake Lambda. The Lambda walks the email to its plain-text body (falling back to the HTML body, stripped to text, if there’s no plain-text part), figures out which conversation it belongs to from the sender and subject, and drops the new message onto that thread. If it’s a brand-new sender, it starts a fresh thread.

Email is messy. A reply often carries the entire history of the conversation quoted below it, plus a signature block, plus a mobile footer. Translating all of that would be wasteful and confusing, so the very next step is cleanup — covered in Lane 3.

Lane 2: the web widget

Some customers would rather not email at all. A small chat widget — one line of script on your site — lets a visitor type a question in their own language and get help right there. The widget posts the text to a Lambda Function URL (a plain web address that runs a Lambda, with no API Gateway in front of it), along with a thread id so the back-and-forth stays together. From there it joins the exact same flow as an email: same cleanup, same detection, same thread store. The customer doesn’t care which pipe their message took, and neither does the rest of the system.

The widget text is usually cleaner than email — no signature, no quoted history — but it’s often shorter, which makes language detection harder, not easier. A two-word message gives the detector very little to go on.

Lane 3: cleanup, then read the language

Whichever way the message arrived, the relay now has to find the words that matter and read their language. Cleanup comes first. A small deterministic step — plain Python, no model — strips the email signature, the quoted reply history (the lines that start with “>” or sit under “On Tuesday, X wrote:”), and footers like “Sent from my iPhone.” What’s left is the new text the customer actually wrote this time.

Then detection runs in two passes. The first pass is cheap: for a message of reasonable length, a fast script-and-statistics check reads the language confidently and for free. Most messages stop here. The second pass is the careful fallback, and it exists for the hard cases — a three-word reply, a message that mixes two languages, a note that’s mostly product codes and prices with only a few real words. For those, the relay asks Bedrock with Claude Haiku 4.5 to read the language, since a model handles short and mixed text far better than a statistics check. Each pass returns both a language and a confidence score, and both go onto the thread.

There’s one more rule, and it’s the important one: the relay never guesses silently. If even the careful fallback comes back unsure — say, a message that’s genuinely half English and half Spanish, or one too short to call — the message is marked “language unclear” and shown to a person, with the original text, before anything gets translated. A wrong language guess at this step poisons every step after it, so it’s the one place the system would rather stop and ask.

Why detection is its own step

It would be tempting to fold detection into the translate step — just hand the message to the model and say “translate this to English, whatever it is.” That works most of the time, but it hides the one fact you most want recorded: which language the customer is writing in. The relay needs that fact to translate the reply back later, to pick the right tone, and to reuse it on the customer’s next message so a one-word follow-up like “sí” doesn’t get mis-read. Saving the detected language on the thread, with its confidence, makes every later step cheaper and steadier.

Next post: how the cleaned message gets translated into your team’s language, shown side by side with the original, with anything the relay wasn’t sure about highlighted for a human to check.

All posts