Skip to content

feat: WhatsApp group auto-translation (LibreTranslate)#278

Closed
dallascyclist wants to merge 14 commits into
rmyndharis:mainfrom
dallascyclist:feat/whatsapp-group-translation
Closed

feat: WhatsApp group auto-translation (LibreTranslate)#278
dallascyclist wants to merge 14 commits into
rmyndharis:mainfrom
dallascyclist:feat/whatsapp-group-translation

Conversation

@dallascyclist

Copy link
Copy Markdown
Contributor

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 /tr control commands; the bot stays dormant until an admin runs /tr on, and the whole feature is off unless TRANSLATION_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 EXTENSION plugin — 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 return undefined (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, the EXTENSION category 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 that core/ lifts into a plugin almost unchanged once the plugin system can send.

Question: would you be open to giving PluginContext a sanctioned messaging capability (a scoped send API, or a vetted getService)? That would unlock the whole EXTENSION ecosystem, 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 over MessageService/SessionService), the message:received hook, the NestJS module, and the TranslationGroup entity + migration (on the data connection).
  • Conditionally registered in app.module.ts, gated by TRANSLATION_ENABLED (mirrors the existing QUEUE_ENABLED pattern). No impact when off.
  • Two small, independently-useful engine touch-points:
    • surface mentionedIds on IncomingMessage (a one-field passthrough — wwebjs already carries it — for @mention command targeting);
    • run the existing anti-ban typing simulation in MessageService.reply() too (it was only in sendText()), so the bot's quote-replies inherit it.

Behavior

  • Per-message source detection, with learn-by-observation of each participant's language (pinnable via /tr setlang). The bot never translates a message into its own source language, and is resilient to detector misfires on short/colloquial text.
  • Commands: /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).
  • Permissions: WhatsApp group admins, plus admin-delegated members.
  • Anti-ban posture: single combined send per message, dormant-by-default, inherited typing simulation, trivial-message skipping.

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

  • Unit tests across the module: command parser, coordinator (learning debounce, target selection, permission tiers, misfire fallback), reply formatter, LibreTranslate client (timeout + circuit breaker), config store, chat gateway, and the hook. Full suite green.
  • Validated end-to-end against a live WhatsApp group and a self-hosted LibreTranslate.

Known limitations / follow-ups

  • Announce-on-add: the engine doesn't expose a group-join event, so the bot announces on first activity in a group rather than the instant it's added. A small onGroupJoin engine event would enable true announce-on-add.
  • LID identity: group admins are matched via the group owner field (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 (via enforceLidAndPnRetrieval) is a follow-up.
  • Throttle: TRANSLATION_THROTTLE_INTERVAL_MS is plumbed through config but not yet enforced (the typing simulation already spaces sends).
  • Text only for now (no media/caption translation); no dashboard config UI yet (the /tr commands cover configuration).
  • Remote Add a the ability to use remote LibreTranslate via API key

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.
@dallascyclist dallascyclist changed the title Feat/whatsapp group translation feat: WhatsApp group auto-translation (LibreTranslate) Jun 17, 2026
@dallascyclist

Copy link
Copy Markdown
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 (Translator / ConfigStore / ChatGateway) specifically so it could move to a plugin, so I'll port it to a first-party extension on top of ctx.messages/ctx.engine and open a new plugin-based PR to supersede this one (I'll close this once that's up). Appreciate the fast turnaround and the auto-reply reference.

@dallascyclist

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant