; 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. on LOAD { 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 (always translates, ignoring the auto-skips) alias tr { if ($len($1-) == 0) { echo $chan usage: /tr | 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) } }