# Changelog All notable changes to the **Group Auto-Translation** plugin are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this plugin adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The version here always matches `manifest.json`'s `version`. ## [Unreleased] ## [1.3.17] - 2026-11-10 ### Fixed - Exclude host capability-concurrency rejections from the LibreTranslate circuit breaker failure count. ### Changed - Align plugin metadata with OpenWA v0.30.0. ## [1.3.16] - 2026-11-08 ### Changed - Align plugin metadata with OpenWA v0.29.1. ### Fixed - Translate forwarded group messages into the group's languages without updating the forwarder's learned language or running `/tr` commands. ## [1.3.15] - 2026-11-06 ### Changed - Align plugin metadata with OpenWA v0.29.0. ### Fixed - Bound per-group storage keys to 184 UTF-8 bytes for long session and group identifiers. ## [1.3.14] - 2026-10-31 ### Fixed - Preserve group command ordering across live configuration changes. ## [1.3.13] - 2026-10-31 ### Changed - Align plugin metadata with OpenWA v0.27.0. ## [1.3.12] - 2026-10-28 ### Changed - Align plugin metadata with OpenWA v0.26.0. - Document that a per-session override that points `libretranslateUrl` at another origin must also set `libretranslateApiKey`, which OpenWA 0.26.0 no longer inherits. - Correct the multi-backend advice: OpenWA installs a plugin once per gateway, so there is no instance per session. ## [1.3.11] - 2026-10-15 ### Changed - Align plugin metadata with OpenWA v0.25.0 and complete dashboard translations for all supported locales. ## [1.3.10] - 2026-09-24 ### Added - Hindi (`hi`) dashboard text: the plugin name, description and config field titles. ## [1.3.9] - 2026-09-24 ### Fixed - A message that reaches the plugin more than 5 minutes after it was sent is no longer translated. From OpenWA 0.23.6 a Baileys session delivers what WhatsApp queued during a disconnect once it reconnects. - A `/tr` command that late is still claimed but no longer run, since a replay can arrive out of order. - A `/tr` command is claimed as soon as it is recognized, so another auto-reply plugin no longer answers it when the group admin lookup fails or outlasts the host's 5 s hook budget. - A failed group admin lookup counts as an empty admin list: delegated controllers still work and `denyReply` decides whether the sender is told. - A failed `@lid` identity lookup is no longer remembered, so one slow engine read cannot lock a group admin out until the plugin restarts. ## [1.3.8] - 2026-09-23 ### Fixed - Catalog orders and shared product cards are no longer translated. From OpenWA 0.23.5 they arrive as `order` and `product`, on Baileys with the order note or product title as the body, which was posted back into the group as a quote-reply and fed to language detection. ### Changed - **Verified against OpenWA v0.23.6** (testedOpenWAVersion 0.23.4 -> 0.23.6). ## [1.3.7] - 2026-09-05 ### Fixed - **`setlang` and `auto` are admin-gated, as the README, the command table and the plugin description have always said.** Targeting yourself skipped authorization entirely, and an absent target defaults to yourself, so the plain ` setlang ` form was open to any group member. The confirmation is unconditional, so a member could make the bot post into the group on every attempt, the amplification `help` is explicitly bounded against, and could pin their own language. The in-chat help now marks both commands as admin, like its neighbours. - **A backend URL carrying credentials no longer reaches the host log.** When the configured URL has embedded credentials the gateway refuses every fetch and names the full URL in the error, which was rethrown verbatim and logged once per message. Enable-time validation already refused to log that field's value for exactly this reason. Userinfo is now stripped before the error leaves the client. ### Changed - **Verified against OpenWA v0.23.4** (testedOpenWAVersion 0.23.3 -> 0.23.4). ## [1.3.6] - 2026-08-25 ### Changed - **`minOpenWAVersion` raised to 0.8.0.** The plugin resolves the operator's backend host through `net.allowConfigHosts`, which first shipped in OpenWA 0.8.0. On a 0.7.x host it installed and enabled cleanly and then translated nothing, because only the shipped loopback entries were reachable. - **`/tr status` is now admin-only.** It prints the participant roster (who is ignored, who holds delegated control, the language recorded for each member), which is the plugin's own access-control state, and it answered any group member on every attempt. - **`/tr help` is answered at most once a minute per group.** It is the only reply an unauthorized user can draw, so repeating it was a way to make the bot post into the group indefinitely; the denial reply is opt-in for exactly that reason. ### Fixed - A `libretranslateUrl` the host cannot use is now named at enable time. A value with no scheme, embedded credentials, or a query string is refused by the host at the capability boundary, which surfaced only as translation silently never happening. The URL itself is never logged. - A failed language detection records why. An unreachable backend, a refused fetch and an SSRF-blocked address all returned without writing anything, so a group that had stopped being translated left no trace of the cause. - A shared contact card is no longer translated. OpenWA 0.23.2 fills the message body with the card's vCard, which sent a third party's name and number to the translation backend, posted a machine-translated card back into the group, and let language detection pin the sender's language from vCard field names on their first shared card. Poll questions are still translated: a poll question is ordinary prose. ### Changed - **Verified against OpenWA v0.23.3** (testedOpenWAVersion 0.23.0 → 0.23.3). ## [1.3.5] - 2026-08-20 ### Changed - **Verified against OpenWA v0.23.0** (testedOpenWAVersion 0.22.0 → 0.23.0). ## [1.3.4] - 2026-08-19 ### Changed - **Verified against OpenWA v0.22.0** (testedOpenWAVersion 0.20.0 → 0.22.0). ## [1.3.3] - 2026-08-16 ### Changed - **Verified against OpenWA v0.20.0** (testedOpenWAVersion 0.19.0 → 0.20.0); the catalog download URL now pins #sha256= for production installs. ## [1.3.2] — 2026-08-15 ### Fixed - **A group message can no longer stall the plugin worker.** The filter that skips messages with nothing to translate was a single regular expression whose emoji branch overlapped its URL branch — `\p{Emoji}` matches ASCII digits, `#` and `*`, because those are keycap-sequence components. Each additional link-shaped token multiplied the backtracking space, so a 145-character message took several seconds of uninterruptible CPU and `maxLength` allows 2000. The filter now scans token by token, which is linear regardless of input. - **A message of only digits or punctuation is translated again.** The same emoji property classified `1` and `#5` as emoji, so those messages were skipped silently. The filter now uses `\p{Extended_Pictographic}`, which is the property that actually means "picture character". ### Changed - **Compatibility re-verified against OpenWA v0.19.0** (testedOpenWAVersion 0.14.0 → 0.19.0). The manifest passes the v0.19.0 host's load-time validation (manifest, ingress and main-entry checks) and the built bundle loads under the loader contract. No capability surface this plugin uses changed between 0.14 and 0.19; the v0.19 breaking changes are host-side (API key length, removed REST endpoints, the plain-http install pin) and do not touch this plugin. ## [1.3.1] — 2026-08-12 ### Changed - **Declared the `storage:use` permission.** Each group's settings — whether translation is on, the language learned for every participant, and any delegated controllers — are kept in plugin storage. OpenWA is moving `ctx.storage` behind a permission the manifest has to declare, and a plugin that does not name it will have those reads and writes refused once the host enforces that. The failure would surface mid-conversation rather than at install: the plugin would keep running and quietly forget every group's configuration. Declaring it now costs nothing on the hosts you are running today — an unrecognized permission is ignored — so this release behaves exactly like 1.3.0 everywhere 1.3.0 ran. ## [1.3.0] — 2026-08-01 ### Fixed - **Group admins were refused their own commands.** WhatsApp delivers the author of a group message under a privacy id (`…@lid`) while the group's participant list comes back under phone ids (`…@c.us`). Those two carry unrelated numbers, so an admin failed every comparison and `/tr on`, `/tr setlang`, `/tr grant` and the rest were silently denied. Only the group's creator was ever recognized, because WhatsApp reports the owner in the author's own dialect — and in a group created by the bot's own number, even that did not help, since the bot's own messages are never processed. The result was a group with no working administrator at all. When the direct comparison finds nothing, the plugin now asks the host to resolve the author to its phone identity and compares again. The same bridge fixes `/tr grant`: a controller delegated by phone number is now recognized when they speak. The lookup costs one call, is spent only on what would otherwise be a denial, and is remembered afterwards. ### Changed - **The LibreTranslate backend is configurable without repackaging the plugin.** `libretranslateUrl` was checked against a fixed `localhost:7001` entry, so pointing the plugin at any other instance meant editing the manifest and rebuilding the `.zip` — and every other value silently failed. Any loopback address on any port is now accepted as shipped, and any other host is accepted over https through the manifest's `allowConfigHosts`. A loopback or private address still needs `SSRF_ALLOWED_HOSTS` on the OpenWA host; a plain-http backend on a non-loopback host still needs the manifest edit, because the host admits a config-supplied host only over https. ## [1.2.0] — 2026-08-01 ### Changed - **This plugin no longer introduces itself in your groups unless you ask it to.** Until now, enabling it made it post a "👋 Translation bot…" message into a group the first time it saw any message there. That fired **per group**, so a single enable announced the bot into every group the WhatsApp account belonged to — and the account running a plugin is usually somebody's own WhatsApp, not a dedicated bot number. The introduction is now controlled by a new **Announce this bot in new groups** setting, which is **off by default**, including for installs upgrading from an earlier version. Nothing else about the plugin was reachable without addressing it first: translation still requires an admin to run `/tr on`, and the same introduction text is still available to anyone on demand with `/tr help`. If you want the old behaviour, turn the new setting on. ## [1.1.1] — 2026-08-01 No behaviour change. 1.1.0 has now been smoke-tested against a newer host, so the tested-version field is updated to match. ### Verified - **Confirmed against a live OpenWA 0.12.1 host**, with a connected WhatsApp session, driven by real WhatsApp messages from a second connected number in a real WhatsApp group, against a real self-hosted LibreTranslate instance (English + Indonesian models) reachable at `http://localhost:7001` from inside the host container. `/tr status` reported `Translator: ok` — a live health probe of LibreTranslate through the host-proxied `ctx.net.fetch`. `/tr on` returned `✅ Translation activated.`, and both `/tr setlang en` (self) and `/tr setlang id ` applied and read back correctly in `/tr status`. The message `good morning, how are you today` was answered with `ID: selamat pagi, bagaimana kabarmu hari ini`. - **The transformer-band fix from 1.1.0 holds**: the hook registered at priority 50, the Transformer band assigned in `PLUGIN-STANDARD.md`. - **Not covered:** load or concurrency. ## [1.1.0] — 2026-07-31 ### Fixed - **A message could go untranslated entirely if another auto-reply plugin was also installed.** This plugin ran at the default hook priority — the same one an auto-reply plugin claiming its own messages runs at by default — so whether this plugin ever got a chance to translate a given message depended on which plugin happened to register first, which could change across a restart or re-enable. It now registers in the transformer band, ahead of every responder, so it translates an eligible message before any responder can answer it, instead of racing them for registration order. Note this only affects when the translation runs: a translated message is still passed on afterward, so a co-installed auto-reply plugin can still see and answer the original text. This plugin claims only its own `/tr` admin commands, never a translated conversational message. ## [1.0.7] — 2026-07-30 ### Fixed - **An empty translation is dropped instead of thrown away.** Whatever the backend returned was forwarded straight into the send, and a backend that answers 200 with a blank translation is a real case. OpenWA 0.12 rejects an empty positional capability argument outright, so that stopped being a blank WhatsApp bubble and became a thrown capability error the coordinator swallowed — a translation that silently never arrives either way. It is now dropped deliberately, at the one place that can tell why. - **An empty translation is dropped where it is produced, not at the send.** The guard first shipped in the gateway could never fire: the formatter prefixes each entry with a flag and language code, so a blank translation still rendered a non-empty `🇪🇸 ES:` bubble. It is now dropped at the point the translation is collected, which also keeps the decision log's count honest. - **`timeoutMs` is clamped in code, not just hinted in the schema.** A `configSchema` `min` is a form hint the host never enforces, and the host clamps a fetch timeout of `<= 0` up to 1 ms — so a config of `0` made every translate abort instantly, with the coordinator swallowing the error and nothing logged. Values are now clamped to 500–30000 where it is actually enforceable. ## [1.0.6] — 2026-07-18 ### Fixed - **Per-session config overrides are now honored at message time without resetting the circuit breaker on every message.** The coordinator was built once at `onEnable` and the hook read that cached instance, so a per-session override (e.g. a different LibreTranslate instance or command prefix for one session) set via the dashboard after enable was ignored. The hook now recomputes a signature of the coordinator-affecting config fields per event and rebuilds the coordinator only when that signature changes — so an override takes effect, while the LibreTranslate client's circuit-breaker state is preserved across messages for an unchanged backend (a naive per-event rebuild would open/close the backend anew on each call and defeat the breaker's purpose). ## [1.0.5] — 2026-07-02 ### Fixed - **`denyReply` is now honored.** The denial reply for a restricted command was sent unconditionally, ignoring the `denyReply` config (which the manifest documents as default `false`). It now replies only when `denyReply` is enabled — so by default an unauthorized user cannot make the bot echo an "admins only" message back into the group on every attempt. ### Changed - **README Security section corrected.** It previously claimed `SSRF_ALLOWED_HOSTS` "no longer applies to plugins" — the opposite of the truth. The host SSRF guard blocks loopback/private addresses at connect for every `ctx.net.fetch` regardless of `net.allow`, so a self-hosted LibreTranslate on `localhost`/`127.0.0.1`/a private host (including the default `http://localhost:7001`) requires `SSRF_ALLOWED_HOSTS=` on the gateway. The Security section and config table now say so. ## [1.0.4] — 2026-06-25 ### Fixed - Translations now actually apply. The LibreTranslate client read the response with `res.json()`, but the sandboxed `ctx.net.fetch` returns the body as a string and provides no `.json()` method (functions can't cross the worker boundary) — so every call threw and failed open, a silent no-op. The client now parses `res.body` directly. ## [1.0.3] — 2026-06-23 ### Fixed - Participant lookups now reject prototype keys (`__proto__`, `constructor`, `prototype`) and test existence with `hasOwnProperty`, so a crafted participant/target id can no longer read or write `Object.prototype`. - Concurrent messages for the same group are serialized through a per-(session, chat) lock, closing a load→mutate→save race that could duplicate the help announcement or drop a participant-language update. The lock map self-evicts when a chat's queue drains. - A LibreTranslate `/translate` response without a string `translatedText` now fails the call (counted by the circuit breaker and excluded from the reply) instead of posting the literal text `undefined`. ## [1.0.2] — 2026-06-23 ### Fixed - Telugu (`te`) localization: the `libretranslateUrl` field title was left in English ("LibreTranslate URL"); it is now localized to "LibreTranslate చిరునామా", matching the other locales. ## [1.0.1] — 2026-06-23 ### Added - Localized dashboard text (`name`, `description`, config field titles) for es, fr, it, ar, he, te, zh-CN, zh-HK via `manifest.i18n`. English remains the default + fallback. Translations are machine-generated; human review recommended for ar/he/te. ## [1.0.0] — 2026-06-23 First release. Built against the OpenWA v0.7 plugin contract. ### Added - Auto-translation of group messages between participants' languages via a LibreTranslate backend, with in-chat `/tr` commands (help, status, on/off, setlang, auto, ignore/unignore, grant/revoke). Admin-gated via `ctx.engine.getGroupInfo`; disabled until enabled. - All outbound calls go through the host's SSRF-guarded `ctx.net.fetch`; the LibreTranslate host must be in the manifest `net.allow` allowlist. Per-call timeout defaults to 4000ms (≤ the host hook budget), with a circuit breaker that backs off a flaky backend.