Skip to content

Send a bilingual campaign in the language of the matched keyword - #24

Open
emanueletech wants to merge 1 commit into
diwenne:mainfrom
emanueletech:feat/keyword-language
Open

Send a bilingual campaign in the language of the matched keyword#24
emanueletech wants to merge 1 commit into
diwenne:mainfrom
emanueletech:feat/keyword-language

Conversation

@emanueletech

Copy link
Copy Markdown
Contributor

The problem

A campaign aimed at a bilingual audience has to write every message twice — one half per language — and then send both halves to everyone. The reader scrolls past the half they cannot read, in the public reply, the opening DM, the follow gate and the final message alike.

The change

The keyword that matched the comment picks the language. matchKeywords() already returned matchedKeyword; it only had to reach the send.

Messages stay written once, with each half opened by its flag:

🇮🇹 Ciao, vuoi scaricare gli sfondi?

🇺🇸 Hi! Are you interested in the wallpapers?

pickLanguage() cuts on the flags and is applied inside the two rendering functions in lib/tracking/message.ts, so a single place covers public reply, opening DM, follow gate and final message.

Which keyword means which language is configuration, not code. KEYWORD_LANGUAGES maps them (documented in .env.example):

KEYWORD_LANGUAGES=sfondi:it,wallpaper:en,wallpapers:en

Unset by default, so this changes nothing for anyone who does not opt in: an unmapped keyword — or an "any word" campaign — leaves the language null and the whole message goes out exactly as it does today. Six languages are supported (it, en, es, fr, de, pt), one flag each; a malformed pair in the variable is skipped rather than thrown, so a typo degrades to the current behaviour instead of breaking the worker.

Two non-obvious details

  • {link} is written at the bottom, after the last half, so a naive cut left every reader but the last one without a link. Neutral tokens are re-attached to the half that lost them.
  • Pressing the follow gate button, the comment is out of the picture and the language would be lost halfway through, so it travels in the payload: reveal:<id>:it. The suffix is optional and the parser accepts both forms, so buttons already sent stay valid.

Button labels go through pickLabel(), which narrows to a single language even without a known keyword: Instagram truncates button titles at 20 characters, and a bilingual label would arrive cut mid-word.

Checks

npm run typecheck, npm run lint, npm test, npm run build all pass. 12 new tests in __tests__/language.test.ts (154 total, up from 142) cover the malformed map, an absent language, a non-bilingual message, the trailing {link}, a third language and the button labels.

Not exercised against the live Meta API — the message rendering and the payload round-trip are covered by unit tests only.

Credit

The idea and the first implementation are Fabiano's, in ispazio/openreply@484e675. This port replaces the hardcoded keyword map and campaign defaults with configuration so it fits any account, not just his.

@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

@emanueletech is attempting to deploy a commit to the diwenne's projects Team on Vercel.

A member of the Team first needs to authorize it.

Bilingual campaigns had to send both halves to everyone, making every
message twice as long as the reader needed. matchKeywords() already
returned matchedKeyword; it only had to reach the send.

pickLanguage() cuts on the flag that opens each half and is applied
inside the two rendering functions, so one place covers public reply,
opening DM, follow gate and final message.

Which keyword means which language is configuration, not code:
KEYWORD_LANGUAGES maps them (documented in .env.example) and an unmapped
keyword — or an unset variable — leaves the message whole, so nothing
changes for anyone who does not opt in.

Two non-obvious details:

- {link} is written at the bottom, after the last half, so a naive cut
  left every other reader without a link: neutral tokens are re-attached
  to the half that lost them.
- pressing the follow gate button, the comment is out of the picture and
  the language would be lost halfway, so it travels in the payload
  (reveal:<id>:it). The suffix is optional: buttons already sent in the
  old format stay valid.

Button labels go through pickLabel(), which narrows to a single language
even without a known keyword, because Instagram truncates titles at 20
characters and a bilingual label would arrive cut mid-word.

Ported from ispazio/openreply@484e675, with the iSpazio-specific keyword
map and defaults replaced by configuration.

Co-authored-by: Fabiano Confuorto <37019652+ispazio@users.noreply.github.com>
@emanueletech
emanueletech force-pushed the feat/keyword-language branch from 16fc413 to 3bceadd Compare August 18, 2026 11:45
Adam-Jemmali pushed a commit to Adam-Jemmali/openreply that referenced this pull request Aug 19, 2026
@emanueletech

Copy link
Copy Markdown
Contributor Author

Following the i18n discussion in #22, a note on this one — it is a different animal, but the size objection may well apply here too, so let me make it easy to say no or to shrink it.

This PR is not a translation layer: nothing in the UI changes, and it is inert unless KEYWORD_LANGUAGES is set. It solves a problem that shows up on any account with a mixed audience: a bilingual campaign has to send both halves to everyone, so half the message is noise to whoever reads it.

If the size is what makes it awkward, it splits cleanly in two, and the first half stands on its own:

  1. pickLanguage() plus the two rendering functions — the message is cut on the flag that opens each half, covering public reply, opening DM, follow gate and final message from one place. About 180 lines with tests.
  2. The language in the postback payload (reveal:<id>:it) — needed only because the original comment is out of the picture once the user taps the follow gate button, so without it the final message falls back to bilingual halfway through. This is the part that touches the worker in several places.

Happy to open just the first, or to close this if the feature is not a direction you want the project to take. No hard feelings either way — the two merged fixes were the ones that mattered to me, since they were breaking real sends.

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