Skip to content

Locale detection

Source: platform/apps/app/src/lib/rag/detect-locale.ts · rendered from main on every deploy — edit in the repo, not here

platform/apps/app/src/lib/rag/detect-locale.ts
/**
* Detect response language from the visitor's own words. ADR-0121 D1.
*
* Puccha serves exactly two response languages (Thai, English), so this is a
* deliberately tiny dominant-language check, not a general language classifier —
* no dependency, Workers-safe, and nothing added to the widget bundle (the
* widget never calls this; the chat route does).
*
* We weigh Thai *characters* against English *words*, not character-vs-character.
* Thai is written without spaces, so its natural unit is the character; English's
* is the word. A strict char-vs-char count let one long English noun outvote a
* whole Thai phrase — "สนใจทำ software" (6 Thai chars vs 8 Latin chars) was
* classified English and got an English refusal (the bug that motivated this
* refinement of ADR-0121 D1). Counting English by word, "สนใจทำ software" is
* 6 Thai vs 1 English → Thai, while "What is the ค่ะ policy on refunds?" is
* 2 Thai vs 8 English → English, both correct.
*
* Text with no letters at all (empty, digits / emoji / punctuation only) returns
* `null`, so the caller falls back to the client-supplied locale.
*
* This only ever drives *generation* language (system prompt, refusal, canned
* replies, handoff copy, query-rewrite, answer-cache key). It is never a
* retrieval filter — hybridSearch / Vectorize / FTS do not gate on locale and
* bge-m3 is multilingual, so an English query still retrieves Thai chunks.
*/
export function detectLocale(text: string): 'th' | 'en' | null {
if (!text) return null;
let thaiChars = 0;
let latinWords = 0;
let inLatinWord = false;
for (const ch of text) {
if (ch >= '' && ch <= '๿') {
thaiChars++;
inLatinWord = false;
} else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
if (!inLatinWord) latinWords++;
inLatinWord = true;
} else {
inLatinWord = false;
}
}
if (thaiChars === 0 && latinWords === 0) return null; // no decidable script
return thaiChars >= latinWords ? 'th' : 'en';
}