; Auto translations for HexDroid
;
; auto-translate incoming foreign-language lines.
; Usage: /tr something
; ENDPOINT: point %ep at any LibreTranslate server. A bare base URL is fine, the script
; appends the "/translate" API path for you.
;
; The public https://libretranslate.com endpoint REQUIRES an API key; a self-hosted
; instance usually does not. Edit via Scripts > translate.hex > Edit.
set %lang $setting(applang)
if ($len(%lang) == 0) { set %lang en }
; ===== EDIT THESE =================================================
set %ep https://translate.boxlabs.uk; your LibreTranslate server (base URL is fine)
set %apikey ; API key (blank = none / self-hosted)
; =================================================================
set %trwarned 0 ; so a failing endpoint warns once, not per line
set %maxlen 500 ; don't ship very long lines to the endpoint
set %minconf 50 ; ignore translations the detector isn't sure about (0-100)
set %recent $list() ; recently-seen lines, to skip bouncer/CHATHISTORY repeats
; normalise %ep to the POST /translate path: drop a trailing slash, then add /translate
; unless it's already there (matching the path, not the "translate" that may be in a host).
if ($right(%ep, 1) == /) { set %ep $left(%ep, $calc($len(%ep) - 1)) }
if ($right(%ep, 10) != /translate) { set %ep %ep/translate }
}
; Build the LibreTranslate request body for text $1-, honouring an optional API key. The
; values are url-encoded, so the result is a single space-free token, which is what
; http.post needs since it splits its arguments on spaces.
alias tr_body {
if ($len(%apikey) > 0) { return q=$urlencode($1-)&source=auto&target=%lang&api_key=%apikey }
return q=$urlencode($1-)&source=auto&target=%lang
}
; Turn an HTTP status into a human hint ($1 = status).
alias tr_diag {
if ($1 == 405) { return status 405 (Method Not Allowed) from %ep. }
if ($1 == 403) { return status 403: the endpoint needs an API key, set %apikey (or the key is wrong). }
if ($1 == 400
) { return status 400: the
server rejected the request
(often a missing API key or unsupported language
).
}
if ($1 == 0
) { return could not reach
%ep, check the
URL/TLS and that the
server is up.
}
return status $1
}
; Auto-translate every incoming line that isn't ours and looks like it might be foreign.
; The buffer, sender nick, and original text are threaded through as context
; ($1 = buffer, $2 = nick, $3- = original) so the callback can place the result, attribute
; it, show the language pair, skip our own language, and reject same-language garble.
on TEXT {
; --- never translate our own lines. $isme is the primary signal, but a bouncer replaying
; our messages (CHATHISTORY / echo-message) can arrive without it, so also match our nick.
if ($isme == true) { return }
if ($nick == $me) { return }
; --- cheap skips: blanks, URLs, over-long lines
if ($len($text) < 2) { return }
if ($left($text, 4) == http) { return }
if ($len($text) > %maxlen) { return }
; --- skip ALL-CAPS Latin acronyms / shouting (LOL, BRB, AFK): uppercase-Latin/digits/spaces
; only. This never matches lowercase Latin or non-Latin scripts, so real foreign text is kept.
if ($re_match($text, ^[A-Z0-9 ]+$) == true) { return }
; --- skip lines that are mostly not letters (timestamps, scores, "+50 -3 (id 42)"): strip
; everything that isn't a letter (\p{L} keeps Cyrillic/CJK/accented too) and require the
; remaining letters to be at least half the length, so number/symbol noise never gets sent.
set %letters $re_replace($text, [^\p{L}], )
if ($calc($len(%letters) * 2) < $len($text)) { return }
; --- de-duplicate: a bouncer often delivers the same line live AND from history. Skip a line
; we translated recently (keyed by network+buffer so identical text on two nets still works).
; Note: '|' is the command separator, so the key uses '~' as its delimiter.
set %key $network~$buffer~$text
if ($has(%recent, %key) == true) { return }
push %recent %key
if ($len(%recent) > 24) { set %recent $slice(%recent, 1, $len(%recent)) }
; source=auto makes LibreTranslate report the detected language so we can skip our own
http.post %ep $tr_body($text) tr_auto $buffer $nick $text
}
on SIGNAL:tr_auto {
if ($httpok == true) {
set %src $json($httpbody, detectedLanguage.language)
set %conf $json($httpbody, detectedLanguage.confidence)
set %out $json($httpbody, translatedText)
set %orig $3-
; if the server omitted a confidence, don't let that filter everything out
if ($len(%conf) == 0) { set %conf 100 }
if ($len(%src) == 0) { set %src ? }
if ($len(%out) == 0) { return }
; detected source must differ from our language (skip English/acronyms read as English)
if (%src == %lang) { return }
; detector must be reasonably sure
if (%conf < %minconf) { return }
; output must actually differ from the original (a same-language round-trip is identical)
if (%out == %orig) { return }
; --- word-overlap guard: a real translation replaces most words, so if half or more of the
; translation's words were already in the original it is almost certainly a same-language
; garble (LibreTranslate mis-detecting informal English, eg "peanutbutter banana sandwich"
; -> "banana banana butter sandwich"). Drop those; genuine foreign text shares ~no words.
set %tw $split($lower(%out))
set %ow $split($lower(%orig))
set %tn $len(%tw)
set %hit 0
set %i 0
while (%i < %tn) {
set %w $get(%tw, %i)
if ($has(%ow, %w) == true) { inc %hit }
inc %i
}
if ($calc(%hit * 2) >= %tn) { return }
; passed every guard - show it, attributed to the sender with the src->dst language pair
echo $1 ↳
$2 (%src→%lang):
%out
}
else {
; surface a failure once, with a status-specific hint so the real cause is obvious
if (%trwarned == 0) {
set %trwarned 1
echo $1 *** translate: request failed.
$tr_diag
($httpstatus)
}
}
}
; manual: /tr <text> (always translates, ignoring the auto-skips)
alias tr {
if ($len($1-
) == 0
) { echo $chan usage:
/tr <text> |
return }
http.post %ep $tr_body($1-) tr_show $chan
}
on SIGNAL:tr_show {
if ($httpok == true) {
set %src $json($httpbody, detectedLanguage.language)
if ($len(%src) == 0) { set %src ? }
echo $1 ↳
(%src→%lang):
$json($httpbody, translatedText
)
}
else { echo $1 *** translate failed.
$tr_diag
($httpstatus) }
}