fix(whatsapp): admin-pinned template language + Arabic locale support (#647)

Reporter @Rekoo-PS hit three independent gaps trying to deliver an
Arabic Meta template. Bundled here because they fan out from the same
root cause (no first-class language config on the WhatsApp tab) and the
review surfaces are tightly coupled.

**1. Test send hardcoded `en_US` (`adminWhatsapp.js:141`).** Smoking gun
for "I can't make it work" — Meta returned template_not_found_in_language
(132001) on every test send for non-English templates, no matter what
else the admin configured. Replaced with `config.template_language ||
'en_US'`.

**2. No `template_language` field on `whatsapp_configs`.** The only
priors were per-message `data.language` (always null from our callers in
`adminEvents.js:854,1188`) and `app_settings.general_default_language`
(the *system UI* language, not the *template's* language registered with
Meta). Migration 137 adds the column; GET + PUT surface it; the
processor uses it as the highest-priority default when message_data
doesn't override.

Resolution order in `whatsappProcessor.processWhatsAppQueue` is now:
  1. message_data.language (per-event override — caller path TBD)
  2. config.template_language (admin-pinned template language)
  3. app_settings.general_default_language (system fallback)
  4. en_US (hardcoded last resort)

**3. `LANGUAGE_MAP` + `PASSWORD_LABELS` didn't cover Arabic.** Added
`ar` (Meta's single-code form per RFC; no region variant). For any
language we don't enumerate (e.g. Turkish `tr_TR`, Chinese `zh_CN`,
Hebrew `he_IL`), `resolveLanguageCode` now pass-throughs valid-shape
codes (lowercase-language + optional underscore + uppercase-region) and
forwards them to Meta as-is. If they don't match a registered template
Meta returns 132001, which the test route already surfaces back to the
admin via `error.message` — fail-loud, no silent fallback.

Validation:
- Unit smoke on `resolveLanguageCode` across 18 representative inputs
  (in-map, pass-through, canonicalization, rejection) — all behaviours
  correct.
- Lint clean on all 7 changed files.
- Frontend `tsc --noEmit` clean.
- Migration `node -c` syntax-checked; additive + `hasColumn`-guarded so
  re-running is safe.

Frontend: free-text input on the WhatsApp tab with EN + DE i18n.
Pointing at Meta's supported-languages docs via the hint text — Meta's
list grows; a hardcoded dropdown would rot.

Closes #647.
This commit is contained in:
Paul Nothaft
2026-06-20 23:29:08 +02:00
parent 7cf26795ec
commit 4fd7709596
7 changed files with 118 additions and 5 deletions
@@ -31,6 +31,7 @@ export const WhatsAppTab: React.FC = () => {
const [wabaId, setWabaId] = useState('');
const [accessToken, setAccessToken] = useState('');
const [templateName, setTemplateName] = useState('gallery_ready');
const [templateLanguage, setTemplateLanguage] = useState('');
const [enabled, setEnabled] = useState(false);
const [showToken, setShowToken] = useState(false);
const [testPhone, setTestPhone] = useState('');
@@ -43,6 +44,7 @@ export const WhatsAppTab: React.FC = () => {
// Leave it visible-as-masked so the admin sees that a token exists.
setAccessToken(data.access_token || '');
setTemplateName(data.template_name || 'gallery_ready');
setTemplateLanguage(data.template_language || '');
setEnabled(Boolean(data.enabled));
}
}, [data]);
@@ -53,6 +55,7 @@ export const WhatsAppTab: React.FC = () => {
waba_id: wabaId,
access_token: accessToken,
template_name: templateName,
template_language: templateLanguage,
enabled,
}),
onSuccess: () => {
@@ -175,6 +178,23 @@ export const WhatsAppTab: React.FC = () => {
</p>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('settings.whatsapp.templateLanguage', 'Template language')}
</label>
<Input
value={templateLanguage}
onChange={(e) => setTemplateLanguage(e.target.value)}
placeholder={t('settings.whatsapp.templateLanguagePlaceholder', 'e.g. en_US, de_DE, ar, pt_BR') as string}
/>
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t(
'settings.whatsapp.templateLanguageHint',
'Meta template language code, exactly as you registered it in Meta Business Manager (`ar`, `en_US`, `de_DE`, `pt_BR`, etc.). Leave empty to fall back to the system default language. Meta returns "template not found in language" if this doesn\'t match a registered template.',
)}
</p>
</div>
<label className="flex items-center gap-2 text-sm text-neutral-800 dark:text-neutral-200">
<input
type="checkbox"