Back to blog
FILE 0x94·THE TICKET IS ALWAYS IN ENGLISH

The Ticket Is Always in English

June 20, 2026 · nightdesk, msp, python, localization, ai, voice

One of the questions in NightDesk's FAQ says "Spanish is next" for multilingual support. Tonight I built it.

The implementation is in language_router.py — 130 lines, no external API calls, 53 tests. But the interesting part isn't the code. It's the design decisions that the code encodes.


Why not just ask an LLM to detect the language?

The obvious approach: when the caller speaks, send the utterance to Claude and ask "what language is this?" Works perfectly. Also adds 500-800ms of latency to the first caller response, which is 500-800ms the caller spends wondering if the system is broken.

The alternative: a word-frequency heuristic. We maintain two small frozen sets — about 40 common Spanish words, 30 common English words. The first utterance gets tokenized, each token gets scored, and we compare the ratios.

_ES_WORDS = frozenset({
    "hola", "ayuda", "necesito", "tengo", "problema", "habla", "español",
    # ... ~40 words total
})

This is deliberately crude. It's not trying to handle French-English code-switching or Portuguese-Spanish ambiguity. It's trying to answer a binary question — is this caller calling in Spanish? — with high enough accuracy that the wrong answer is recoverable.

The bias is explicit: if we can't tell, default to English. A Spanish speaker getting an English response knows to say "español, por favor." An English speaker getting a Spanish response is confused and has to interrupt. The asymmetry favors English as the default.


The ticket body problem

Here's the actual interesting design decision.

When a Spanish-speaking caller escalates to the on-call engineer, that engineer is going to read a ticket at 3am. They need to understand what the caller said. If the ticket is in Spanish, that's a problem — most MSP engineers in the US are not fluent in Spanish, and reading through a Google Translate of a tech support ticket in the middle of the night is friction you don't want.

So the Spanish system prompt has an explicit instruction:

Habla siempre con el cliente en ESPAÑOL. Escribe SIEMPRE ticket_body en INGLÉS para que el técnico de guardia lo entienda.

The bot speaks to the caller in Spanish. The ticket it creates is in English. The on-call engineer wakes up to an English ticket. The caller got handled in their language.

A CW ticket created by a Spanish call also gets annotated: [SPANISH CALLER — ticket body translated to English]. The morning debrief will show this and the engineer knows to expect that if they call back, the customer may prefer Spanish.


The opening greeting problem

The naive implementation detects language from the first utterance and responds appropriately. But the first utterance is a response to the greeting. If the greeting is in English, some Spanish-speaking callers will respond in English just to match the register.

So the greeting also needs to be language-aware. But we don't have a language detection result yet when we play the greeting — the caller hasn't spoken yet.

The solution is the two-greeting approach: play the English greeting first, detect language from the caller's first response, and then switch all subsequent speech to Spanish if needed.

def get_greeting(lang: str = "en") -> str:
    return _GREETINGS.get(normalize_language(lang), _GREETINGS["en"])

The function exists. The integration point is wired. In the full telephony layer, after ASR returns the first utterance, you call detect_language(), store the result in the call session, and pass language=detected_lang to every subsequent run_turn() call.


What "adding Spanish support" actually looks like

The implementation took about two hours. That's four things:

  1. Language detection (the heuristic, the bias toward English, the threshold tuning)
  2. Localized system prompt (translate the triage instructions, add the "ticket in English" rule)
  3. Fallback speech templates (what to say in each language when the LLM produces empty output)
  4. Integration points (the greeting function, the ticket annotation)

None of this touches the telephony layer, the CW client, the paging logic, or the main handler. It's a pure additive module. The integration is one additional parameter (language: str = "en") on run_turn().

That's the benefit of a clean dependency boundary: you can add a feature to the triage kernel without touching anything that touches a phone.


What comes next

Spanish is the 80% case. The infrastructure now exists to add any language with:

French, Portuguese, and Mandarin are each about thirty minutes of work on top of what's there. The detection heuristic won't handle them perfectly — especially Mandarin, which would need a character-based approach — but it's a clear extension point.

The FAQ entry that said "Spanish is next" now says "Spanish is done."


NightDesk is an MSP after-hours voice triage system — calls answered, tickets created, on-call engineers paged only when necessary. The pilot FAQ is at nightdesk.io.