CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
71969b1
·
1 Parent(s): 3d9b9c9

feat(bot): add EN/ES/PT language support in onboarding flow

Browse files

Replace 2-button interactive picker with 5-language interactive list
(FR, Wolof, EN, ES, PT). WhatsApp buttons are capped at 3, so the
list format is now used throughout the language selection step.

Adds getDefaultTrack() and getSectorConfirmMsg() helpers to map all
5 languages to their respective track IDs and localised confirmations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/whatsapp-worker/src/handlers/OnboardingHandler.ts CHANGED
@@ -5,6 +5,14 @@ import { logger } from '../logger';
5
  import { clearTimeTravelContext } from '../timeTravelContext';
6
  import { FlowConfigSchema } from '@repo/shared-types';
7
 
 
 
 
 
 
 
 
 
8
  const LEGACY_SECTORS: Array<{ id: string; label: string }> = [
9
  { id: 'SEC_COMMERCE', label: 'Commerce / Vente' },
10
  { id: 'SEC_AGRI', label: 'Agriculture / Élevage' },
@@ -25,6 +33,22 @@ function getValidatedConfig(ctx: MessageContext) {
25
  return parsed.data;
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  function getSectors(ctx: MessageContext): Array<{ id: string; label: string }> {
29
  const config = getValidatedConfig(ctx);
30
  return config?.sectors || LEGACY_SECTORS;
@@ -38,7 +62,7 @@ export class OnboardingHandler implements MessageHandler {
38
  if (isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI')) return true;
39
 
40
  // 2. Language Selection
41
- if (normalizedText === 'LANG_FR' || normalizedText === 'LANG_WO') return true;
42
 
43
  // 3. Sector Selection (only if not active enrollment)
44
  const sectors = getSectors(ctx);
@@ -62,16 +86,14 @@ export class OnboardingHandler implements MessageHandler {
62
  if (user) {
63
  // Guard: if user hasn't completed onboarding yet (no activity), skip hard reset
64
  if (!user.activity) {
65
- logger.info({ traceId, userId: user.id }, "INSCRIPTION during onboarding — resending language buttons");
66
- bodyText = user.language === 'WOLOF' ? "Tànnal sa làkk :" : "Choisissez votre langue :";
67
- await whatsappQueue.add('send-interactive-buttons', {
68
  userId: user.id,
69
  organizationId: ctx.organizationId,
70
- bodyText,
71
- buttons: [
72
- { id: 'LANG_FR', title: 'Français 🇫🇷' },
73
- { id: 'LANG_WO', title: 'Wolof 🇸🇳' }
74
- ]
75
  });
76
  return true;
77
  }
@@ -96,14 +118,13 @@ export class OnboardingHandler implements MessageHandler {
96
  bodyText = "Bienvenue ! Choisissez votre langue :";
97
  }
98
 
99
- await whatsappQueue.add('send-interactive-buttons', {
100
  userId: user.id,
101
  organizationId: ctx.organizationId,
 
102
  bodyText,
103
- buttons: [
104
- { id: 'LANG_FR', title: 'Français 🇫🇷' },
105
- { id: 'LANG_WO', title: 'Wolof 🇸🇳' }
106
- ]
107
  });
108
  return true;
109
  }
@@ -111,32 +132,31 @@ export class OnboardingHandler implements MessageHandler {
111
  if (!user) return false;
112
 
113
  // --- 2. Language selection ---
114
- if (normalizedText === 'LANG_FR' || normalizedText === 'LANG_WO') {
115
- const newLang = normalizedText === 'LANG_FR' ? 'FR' : 'WOLOF';
116
- user = await prisma.user.update({ where: { id: user.id }, data: { language: newLang } });
117
- const promptText = newLang === 'FR' ? "Parfait ! Dans quel domaine te trouves-tu ?" : "Baax na ! Ci ban mbir ngay yëngu ?";
118
-
 
 
 
 
 
 
 
119
  const sectors = getSectors(ctx);
120
- const rows = sectors.map((s) => ({
121
- id: s.id,
122
- title: s.label
123
- }));
124
-
125
- // Add fallback "Other" if less than 10 sectors (Meta limit is 10 rows per section)
126
  if (rows.length < 10 && !rows.some(r => r.id === 'SEC_AUTRE')) {
127
- rows.push({ id: 'SEC_AUTRE', title: newLang === 'FR' ? 'Autre secteur' : 'Beneen mbir' });
128
  }
129
 
130
  await whatsappQueue.add('send-interactive-list', {
131
  userId: user.id,
132
  organizationId: ctx.organizationId,
133
- headerText: newLang === 'FR' ? "Ton secteur" : "Sa Mbir",
134
- bodyText: promptText,
135
- buttonLabel: newLang === 'FR' ? "Secteurs" : "Tànn",
136
- sections: [{
137
- title: newLang === 'FR' ? 'Liste' : 'Mbir',
138
- rows
139
- }]
140
  });
141
  return true;
142
  }
@@ -147,11 +167,11 @@ export class OnboardingHandler implements MessageHandler {
147
  if (sector || normalizedText.startsWith('SEC_')) {
148
  const activity = sector ? sector.label : text.trim();
149
  user = await prisma.user.update({ where: { id: user.id }, data: { activity } });
150
- const msg = user.language === 'FR' ? `Secteur noté : *${activity}*` : `Bind nanu la ci: *${activity}*`;
151
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
152
 
153
  const config = getValidatedConfig(ctx);
154
- const trackId = config?.defaultTrackId || (user.language === 'FR' ? 'T1-FR' : 'T1-WO');
155
 
156
  await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
157
  return true;
@@ -160,12 +180,12 @@ export class OnboardingHandler implements MessageHandler {
160
  // --- 4. Free-text Activity detection ---
161
  if (!user.activity && text.length > 2) {
162
  user = await prisma.user.update({ where: { id: user.id }, data: { activity: text.trim() } });
163
- const msg = user.language === 'FR' ? `Secteur noté : *${text.trim()}*` : `Bind nanu la ci: *${text.trim()}*`;
164
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
165
-
166
  const config = getValidatedConfig(ctx);
167
- const trackId = config?.defaultTrackId || (user.language === 'FR' ? 'T1-FR' : 'T1-WO');
168
-
169
  await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
170
  return true;
171
  }
 
5
  import { clearTimeTravelContext } from '../timeTravelContext';
6
  import { FlowConfigSchema } from '@repo/shared-types';
7
 
8
+ const LANGUAGE_ROWS = [
9
+ { id: 'LANG_FR', title: 'Français 🇫🇷' },
10
+ { id: 'LANG_WO', title: 'Wolof 🇸🇳' },
11
+ { id: 'LANG_EN', title: 'English 🇬🇧' },
12
+ { id: 'LANG_ES', title: 'Español 🇪🇸' },
13
+ { id: 'LANG_PT', title: 'Português 🇧🇷' },
14
+ ];
15
+
16
  const LEGACY_SECTORS: Array<{ id: string; label: string }> = [
17
  { id: 'SEC_COMMERCE', label: 'Commerce / Vente' },
18
  { id: 'SEC_AGRI', label: 'Agriculture / Élevage' },
 
33
  return parsed.data;
34
  }
35
 
36
+ function getDefaultTrack(language: string): string {
37
+ const map: Record<string, string> = { FR: 'T1-FR', WOLOF: 'T1-WO', EN: 'T1-EN', ES: 'T1-ES', PT: 'T1-PT' };
38
+ return map[language] ?? 'T1-FR';
39
+ }
40
+
41
+ function getSectorConfirmMsg(language: string, activity: string): string {
42
+ const map: Record<string, string> = {
43
+ FR: `Secteur noté : *${activity}*`,
44
+ WOLOF: `Bind nanu la ci: *${activity}*`,
45
+ EN: `Sector noted: *${activity}*`,
46
+ ES: `Sector registrado: *${activity}*`,
47
+ PT: `Setor registrado: *${activity}*`,
48
+ };
49
+ return map[language] ?? `Secteur noté : *${activity}*`;
50
+ }
51
+
52
  function getSectors(ctx: MessageContext): Array<{ id: string; label: string }> {
53
  const config = getValidatedConfig(ctx);
54
  return config?.sectors || LEGACY_SECTORS;
 
62
  if (isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI')) return true;
63
 
64
  // 2. Language Selection
65
+ if (['LANG_FR', 'LANG_WO', 'LANG_EN', 'LANG_ES', 'LANG_PT'].includes(normalizedText)) return true;
66
 
67
  // 3. Sector Selection (only if not active enrollment)
68
  const sectors = getSectors(ctx);
 
86
  if (user) {
87
  // Guard: if user hasn't completed onboarding yet (no activity), skip hard reset
88
  if (!user.activity) {
89
+ logger.info({ traceId, userId: user.id }, "INSCRIPTION during onboarding — resending language list");
90
+ await whatsappQueue.add('send-interactive-list', {
 
91
  userId: user.id,
92
  organizationId: ctx.organizationId,
93
+ headerText: "XAMLÉ",
94
+ bodyText: "Choose your language / Choisissez votre langue / Tànnal sa làkk",
95
+ buttonLabel: "Language",
96
+ sections: [{ title: "Languages", rows: LANGUAGE_ROWS }]
 
97
  });
98
  return true;
99
  }
 
118
  bodyText = "Bienvenue ! Choisissez votre langue :";
119
  }
120
 
121
+ await whatsappQueue.add('send-interactive-list', {
122
  userId: user.id,
123
  organizationId: ctx.organizationId,
124
+ headerText: "XAMLÉ",
125
  bodyText,
126
+ buttonLabel: "Language",
127
+ sections: [{ title: "Languages", rows: LANGUAGE_ROWS }]
 
 
128
  });
129
  return true;
130
  }
 
132
  if (!user) return false;
133
 
134
  // --- 2. Language selection ---
135
+ const LANG_MAP: Record<string, { lang: 'FR' | 'EN' | 'ES' | 'PT' | 'WOLOF'; sectorPrompt: string; sectorHeader: string; sectorButton: string; sectorOther: string; sectorSection: string }> = {
136
+ LANG_FR: { lang: 'FR', sectorPrompt: "Parfait ! Dans quel domaine travailles-tu ?", sectorHeader: "Ton secteur", sectorButton: "Secteurs", sectorOther: "Autre secteur", sectorSection: "Liste" },
137
+ LANG_WO: { lang: 'WOLOF', sectorPrompt: "Baax na ! Ci ban mbir ngay yëngu ?", sectorHeader: "Sa Mbir", sectorButton: "Tànn", sectorOther: "Beneen mbir", sectorSection: "Mbir" },
138
+ LANG_EN: { lang: 'EN', sectorPrompt: "Great! What sector do you work in?", sectorHeader: "Your sector", sectorButton: "Sectors", sectorOther: "Other sector", sectorSection: "List" },
139
+ LANG_ES: { lang: 'ES', sectorPrompt: "¡Perfecto! ¿En qué sector trabajas?", sectorHeader: "Tu sector", sectorButton: "Sectores", sectorOther: "Otro sector", sectorSection: "Lista" },
140
+ LANG_PT: { lang: 'PT', sectorPrompt: "Ótimo! Em que setor você trabalha?", sectorHeader: "Seu setor", sectorButton: "Setores", sectorOther: "Outro setor", sectorSection: "Lista" },
141
+ };
142
+
143
+ if (normalizedText in LANG_MAP) {
144
+ const { lang, sectorPrompt, sectorHeader, sectorButton, sectorOther, sectorSection } = LANG_MAP[normalizedText];
145
+ user = await prisma.user.update({ where: { id: user.id }, data: { language: lang } });
146
+
147
  const sectors = getSectors(ctx);
148
+ const rows = sectors.map((s) => ({ id: s.id, title: s.label }));
 
 
 
 
 
149
  if (rows.length < 10 && !rows.some(r => r.id === 'SEC_AUTRE')) {
150
+ rows.push({ id: 'SEC_AUTRE', title: sectorOther });
151
  }
152
 
153
  await whatsappQueue.add('send-interactive-list', {
154
  userId: user.id,
155
  organizationId: ctx.organizationId,
156
+ headerText: sectorHeader,
157
+ bodyText: sectorPrompt,
158
+ buttonLabel: sectorButton,
159
+ sections: [{ title: sectorSection, rows }]
 
 
 
160
  });
161
  return true;
162
  }
 
167
  if (sector || normalizedText.startsWith('SEC_')) {
168
  const activity = sector ? sector.label : text.trim();
169
  user = await prisma.user.update({ where: { id: user.id }, data: { activity } });
170
+ const msg = getSectorConfirmMsg(user.language, activity);
171
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
172
 
173
  const config = getValidatedConfig(ctx);
174
+ const trackId = config?.defaultTrackId || getDefaultTrack(user.language);
175
 
176
  await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
177
  return true;
 
180
  // --- 4. Free-text Activity detection ---
181
  if (!user.activity && text.length > 2) {
182
  user = await prisma.user.update({ where: { id: user.id }, data: { activity: text.trim() } });
183
+ const msg = getSectorConfirmMsg(user.language, text.trim());
184
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
185
+
186
  const config = getValidatedConfig(ctx);
187
+ const trackId = config?.defaultTrackId || getDefaultTrack(user.language);
188
+
189
  await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
190
  return true;
191
  }