feat: WhatsApp group auto-translation (LibreTranslate)#278
Closed
dallascyclist wants to merge 14 commits into
Closed
feat: WhatsApp group auto-translation (LibreTranslate)#278dallascyclist wants to merge 14 commits into
dallascyclist wants to merge 14 commits into
Conversation
forFeature() alone does not register an entity's metadata with the named 'data' connection (autoLoadEntities is not enabled), so runtime repository queries threw 'No metadata for TranslationGroup'. Add the modules/translation entity glob alongside the others. Surfaced by live testing (migrations passed because the CLI data-source uses a broad src/** glob).
…self-language translation - Commands always reply now; denials and unresolved targets are no longer silent. - Admin/controller matching tolerates WID suffix differences (widEquals helper). - Effective source: trust detection only when it names a language the group uses, else fall back to the sender's known language; always exclude the sender's own language from targets. Fixes a message being translated into its own language when detection misfires (e.g. colloquial es misread as gl). - Gateway logs resolved group admins to diagnose the LID vs @c.us scheme mismatch. All surfaced by live testing.
getGroupInfo participant ids can be in the phone (@c.us) scheme while message authors arrive as LID (@lid), so a string match never recognized the group creator as an admin. The group 'owner' field is reported in the author's scheme, so include it in getGroupAdmins (deduped). Verified live: the creator (LID author) is now authorized for admin commands with no manual delegated-controller entry. Non-owner admins under a differing scheme still need '/tr grant @user'; full per-admin LID<->phone resolution (via enforceLidAndPnRetrieval) is a tracked follow-up.
Contributor
Author
|
Thanks @rmyndharis — the Tier-2 capability layer in #294 is exactly what this needed. The translation logic here was already isolated behind ports ( |
Contributor
Author
|
Superseded by #300 — ported to a first-party extension plugin on the Tier-2 capability layer (#294), with the translation core reused unchanged. Closing in favor of that one. Thanks again @rmyndharis! |
rmyndharis
pushed a commit
that referenced
this pull request
Jun 18, 2026
Ports the translation feature onto the Tier-2 capability layer (#294): the framework-agnostic core/ (coordinator, command parser, reply formatter, ports) is reused unchanged, with ChatGateway/ConfigStore implemented over ctx.messages / ctx.engine / ctx.storage and per-group state in plugin KV storage. Registered disabled by default; enable via POST /plugins/translation/enable. Supersedes the core-module approach in #278 now that the plugin send-capability exists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional, config-gated feature that auto-translates messages within a WhatsApp group between participants' languages, using a self-hosted LibreTranslate instance. When enabled and activated in a group, each text message is translated into the languages of the other participants and posted as a single combined quote-reply. Everything is driven by in-group
/trcontrol commands; the bot stays dormant until an admin runs/tr on, and the whole feature is off unlessTRANSLATION_ENABLED=true.Example: in a group with an English speaker and a Spanish speaker, each message gets one reply with the translation the other needs.
Design note — and a question for maintainers
I'd have preferred to ship this as a first-class
EXTENSIONplugin — it's exactly the "auto-reply / extension" use case the plugin system advertises. I didn't, for one concrete reason:PluginContext.getService()is currently stubbed to returnundefined(src/core/plugins/plugin-loader.service.ts), so an extension plugin has no sanctioned way to send a message or reach the engine. As it stands, theEXTENSIONcategory can receive and transform inbound messages via hooks but can't respond — which rules out auto-reply, schedulers, and this translator.So I built it as a first-class NestJS module instead, but deliberately in a ports-and-adapters layout: the translation logic in
core/depends only on three interfaces (Translator,ConfigStore,ChatGateway) and has zero NestJS/TypeORM/engine imports. The OpenWA-specific wiring lives in thin adapters. The intent is thatcore/lifts into a plugin almost unchanged once the plugin system can send.Question: would you be open to giving
PluginContexta sanctioned messaging capability (a scoped send API, or a vettedgetService)? That would unlock the wholeEXTENSIONecosystem, and I'd happily port this feature onto it. I'm glad to go whichever way you prefer — keep it as a module, or land the plugin-capability change first and I'll convert it. If you want me to do a PR and build this I can but I didn't want to stomp on something someone else was doing (or not knowing the intent)What's included
src/modules/translation/—core/(coordinator, command parser, reply formatter, ports) +adapters/(LibreTranslate HTTP client with timeout + circuit breaker, TypeORM config store, chat gateway overMessageService/SessionService), themessage:receivedhook, the NestJS module, and theTranslationGroupentity + migration (on thedataconnection).app.module.ts, gated byTRANSLATION_ENABLED(mirrors the existingQUEUE_ENABLEDpattern). No impact when off.mentionedIdsonIncomingMessage(a one-field passthrough — wwebjs already carries it — for@mentioncommand targeting);MessageService.reply()too (it was only insendText()), so the bot's quote-replies inherit it.Behavior
/tr setlang). The bot never translates a message into its own source language, and is resilient to detector misfires on short/colloquial text./tr on|off,/tr setlang <code> [me|@user|number],/tr auto,/tr ignore|unignore,/tr grant|revoke,/tr status,/tr help. Every command replies (success or reason — no silent failures).Configuration
TRANSLATION_ENABLED,LIBRETRANSLATE_URL,LIBRETRANSLATE_API_KEY,LIBRETRANSLATE_TIMEOUT_MS,TRANSLATION_COMMAND_PREFIX,TRANSLATION_MIN_LENGTH,TRANSLATION_MAX_LENGTH,TRANSLATION_THROTTLE_INTERVAL_MS,TRANSLATION_DENY_REPLY— documented in.env.example, with fail-fast validation.Testing
Known limitations / follow-ups
onGroupJoinengine event would enable true announce-on-add.ownerfield (reported in the LID scheme that matches message authors). A non-owner admin on a differing scheme currently needs/tr grant; full per-admin LID↔phone resolution (viaenforceLidAndPnRetrieval) is a follow-up.TRANSLATION_THROTTLE_INTERVAL_MSis plumbed through config but not yet enforced (the typing simulation already spaces sends)./trcommands cover configuration).