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

feat(i18n): full 5-language support across all bot handlers

Browse files

Extend EN/ES/PT support to NavigationHandler, ExerciseHandler,
NudgeHandler, AdminHandler, FeedbackHandler, and EnrollHandler.
All user-facing messages now use per-language string maps instead
of WOLOF/FR binary checks.

EnrollHandler now sends a localised "content coming soon" message
when the requested track doesn't exist, instead of silently failing.

OnboardingHandler falls back to T1-FR for EN/ES/PT until orgs
create language-specific tracks via the admin dashboard.

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

apps/whatsapp-worker/src/handlers/AdminHandler.ts CHANGED
@@ -33,12 +33,8 @@ export class AdminHandler implements JobHandler {
33
 
34
  const { sendAudioMessage } = await import('../whatsapp-cloud');
35
  await sendAudioMessage(user.phone, overrideAudioUrl, tenantConfig);
36
- await sendTextMessage(user.phone,
37
- user.language === 'WOLOF'
38
- ? "Baax na ! Yónnee *SUITE* ngir dem ci kanam."
39
- : "Bravo ! Envoyez *SUITE* pour passer à la leçon suivante.",
40
- tenantConfig
41
- );
42
 
43
  await prisma.response.create({
44
  data: {
 
33
 
34
  const { sendAudioMessage } = await import('../whatsapp-cloud');
35
  await sendAudioMessage(user.phone, overrideAudioUrl, tenantConfig);
36
+ const adminMsg = { FR: "Bravo ! Envoyez *SUITE* pour passer à la leçon suivante.", WOLOF: "Baax na ! Yónnee *SUITE* ngir dem ci kanam.", EN: "Well done! Send *CONTINUE* to move to the next lesson.", ES: "¡Bravo! Envía *CONTINUAR* para pasar a la siguiente lección.", PT: "Muito bem! Envie *CONTINUAR* para passar para a próxima lição." }[user.language] ?? "Bravo ! Envoyez *SUITE* pour passer à la leçon suivante.";
37
+ await sendTextMessage(user.phone, adminMsg, tenantConfig);
 
 
 
 
38
 
39
  await prisma.response.create({
40
  data: {
apps/whatsapp-worker/src/handlers/EnrollHandler.ts CHANGED
@@ -39,7 +39,17 @@ export class EnrollHandler implements JobHandler {
39
  async handle(job: Job<JobData>, connection: Redis): Promise<void> {
40
  const { userId, trackId, organizationId } = job.data;
41
  const track = await prisma.track.findUnique({ where: { id: trackId } });
42
- if (!track) return;
 
 
 
 
 
 
 
 
 
 
43
 
44
  if (track.isPremium) {
45
  try {
 
39
  async handle(job: Job<JobData>, connection: Redis): Promise<void> {
40
  const { userId, trackId, organizationId } = job.data;
41
  const track = await prisma.track.findUnique({ where: { id: trackId } });
42
+ if (!track) {
43
+ logger.warn({ userId, trackId }, '[EnrollHandler] Track not found — cannot enroll');
44
+ const user = await prisma.user.findUnique({ where: { id: userId } });
45
+ if (user?.phone) {
46
+ const tenantConfig = await this.getTenantConfig(organizationId as string, connection);
47
+ const msgMap: Record<string, string> = { FR: "La formation est en cours de préparation. Nous te préviendrons dès qu'elle est disponible ! 📚", WOLOF: "Dafa daan jàppale — dinañu la xam bu amee ci kanam ! 📚", EN: "Training content is being prepared. We'll notify you when it's ready! 📚", ES: "El contenido de formación está en preparación. ¡Te avisaremos cuando esté listo! 📚", PT: "O conteúdo da formação está a ser preparado. Iremos notificá-lo quando estiver pronto! 📚" };
48
+ const msg = msgMap[user.language] ?? msgMap['FR'];
49
+ await sendTextMessage(user.phone, msg, tenantConfig);
50
+ }
51
+ return;
52
+ }
53
 
54
  if (track.isPremium) {
55
  try {
apps/whatsapp-worker/src/handlers/ExerciseHandler.ts CHANGED
@@ -3,6 +3,8 @@ import { isFuzzyMatch } from '../services/utils';
3
  import { prisma } from '../services/prisma';
4
  import { logger } from '../logger';
5
  import { getTimeTravelContext } from '../timeTravelContext';
 
 
6
 
7
 
8
 
@@ -64,9 +66,14 @@ export class ExerciseHandler implements MessageHandler {
64
  if (!pendingProgress) {
65
  // If completed and no revalidation, send navigation guidance
66
  if (userProgress?.exerciseStatus === 'COMPLETED') {
67
- const reminder = user.language === 'WOLOF'
68
- ? "Mat nga bés bi ba pare ! ✨\nBindal *2* wala *SUITE* ngir dem ci bés bi ci kanam."
69
- : "Tu as déjà validé cette étape ! ✨\nEnvoie *2* ou *SUITE* pour passer à la suite.";
 
 
 
 
 
70
  await whatsappQueue.add('send-message', { userId: user.id, text: reminder, organizationId: ctx.organizationId });
71
  return true;
72
  }
@@ -101,12 +108,13 @@ export class ExerciseHandler implements MessageHandler {
101
  const minWordCount = shouldBypassGuardrail ? 1 : 3;
102
 
103
  if (wordCount < minWordCount) {
104
- const msg = user.language === 'WOLOF' ? "Tontu bi gatt na..." : "Ta réponse est un peu courte.";
105
- await whatsappQueue.add('send-message', { userId: user.id, text: msg, organizationId: ctx.organizationId });
106
- return true;
107
  }
108
 
109
- await whatsappQueue.add('send-message', { userId: user.id, text: user.language === 'WOLOF' ? "⏳ Defar ak sa tontu..." : "⏳ Analyse de votre réponse...", organizationId: ctx.organizationId });
 
110
 
111
  let currentIterationCount = pendingProgress.iterationCount || 0;
112
  if (isDeepDiveAction) {
 
3
  import { prisma } from '../services/prisma';
4
  import { logger } from '../logger';
5
  import { getTimeTravelContext } from '../timeTravelContext';
6
+ import { getT } from '../services/i18n';
7
+ import { Language } from '@repo/database';
8
 
9
 
10
 
 
66
  if (!pendingProgress) {
67
  // If completed and no revalidation, send navigation guidance
68
  if (userProgress?.exerciseStatus === 'COMPLETED') {
69
+ const t = getT(user.language as Language);
70
+ const reminder = {
71
+ FR: `Tu as déjà validé cette étape ! ✨\nEnvoie *2* ou *SUITE* pour passer à la suite.`,
72
+ WOLOF: `Mat nga bés bi ba pare ! ✨\nBindal *2* wala *SUITE* ngir dem ci bés bi ci kanam.`,
73
+ EN: `You've already completed this step! ✨\nSend *2* or *CONTINUE* to move on.`,
74
+ ES: `¡Ya completaste este paso! ✨\nEnvía *2* o *CONTINUAR* para seguir adelante.`,
75
+ PT: `Você já completou esta etapa! ✨\nEnvie *2* ou *CONTINUAR* para avançar.`,
76
+ }[user.language] ?? t('exercise_prompt');
77
  await whatsappQueue.add('send-message', { userId: user.id, text: reminder, organizationId: ctx.organizationId });
78
  return true;
79
  }
 
108
  const minWordCount = shouldBypassGuardrail ? 1 : 3;
109
 
110
  if (wordCount < minWordCount) {
111
+ const shortMsg = { FR: "Ta réponse est un peu courte.", WOLOF: "Tontu bi gatt na...", EN: "Your answer is a bit short.", ES: "Tu respuesta es un poco corta.", PT: "Sua resposta é um pouco curta." }[user.language] ?? "Ta réponse est un peu courte.";
112
+ await whatsappQueue.add('send-message', { userId: user.id, text: shortMsg, organizationId: ctx.organizationId });
113
+ return true;
114
  }
115
 
116
+ const spinnerMsg = { FR: "⏳ Analyse de votre réponse...", WOLOF: "⏳ Defar ak sa tontu...", EN: "⏳ Analysing your answer...", ES: "⏳ Analizando tu respuesta...", PT: "⏳ Analisando sua resposta..." }[user.language] ?? "⏳ Analyse de votre réponse...";
117
+ await whatsappQueue.add('send-message', { userId: user.id, text: spinnerMsg, organizationId: ctx.organizationId });
118
 
119
  let currentIterationCount = pendingProgress.iterationCount || 0;
120
  if (isDeepDiveAction) {
apps/whatsapp-worker/src/handlers/FeedbackHandler.ts CHANGED
@@ -167,10 +167,7 @@ export class FeedbackHandler implements JobHandler {
167
  } catch (err: any) {
168
  logger.error({ err, userId, jobId: job.id }, '[FEEDBACK] Feedback generation failed');
169
 
170
- const errorMsg = language === 'WOLOF'
171
- ? "⚠️ Am na problem technique. Saytu waat bi rekk."
172
- : language === 'EN' ? "⚠️ Technical issue. Please try again."
173
- : "⚠️ Un problème technique est survenu. Réessaie.";
174
 
175
  await whatsappQueue.add('send-message', { userId, text: errorMsg, organizationId });
176
  throw err;
 
167
  } catch (err: any) {
168
  logger.error({ err, userId, jobId: job.id }, '[FEEDBACK] Feedback generation failed');
169
 
170
+ const errorMsg = { FR: "⚠️ Un problème technique est survenu. Réessaie.", WOLOF: "⚠️ Am na problem technique. Saytu waat bi rekk.", EN: "⚠️ Technical issue. Please try again.", ES: "⚠️ Problema técnico. Por favor, inténtalo de nuevo.", PT: "⚠️ Problema técnico. Por favor, tente novamente." }[language as string] ?? "⚠️ Un problème technique est survenu. Réessaie.";
 
 
 
171
 
172
  await whatsappQueue.add('send-message', { userId, text: errorMsg, organizationId });
173
  throw err;
apps/whatsapp-worker/src/handlers/NavigationHandler.ts CHANGED
@@ -38,7 +38,7 @@ export class NavigationHandler implements MessageHandler {
38
  });
39
 
40
  if (userProgress?.exerciseStatus !== 'COMPLETED' && userProgress?.exerciseStatus !== 'PENDING_DEEPDIVE' && !lastResponse) {
41
- const msg = user.language === 'WOLOF' ? "Dafa laaj nga tontu !" : "Tu dois d'abord répondre !";
42
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
43
  return true;
44
  }
@@ -74,11 +74,11 @@ export class NavigationHandler implements MessageHandler {
74
  where: { id: userProgress!.id },
75
  data: { exerciseStatus: 'PENDING_DEEPDIVE' }
76
  });
77
- const msg = user.language === 'WOLOF' ? "Wax ma ndox mi..." : "Très bien ! Quelle info ?";
78
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
79
  return true;
80
  } else {
81
- const msg = user.language === 'WOLOF' ? "Tontul exercice bi ba pare !" : "Réponds d'abord à l'exercice principal !";
82
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
83
  return true;
84
  }
 
38
  });
39
 
40
  if (userProgress?.exerciseStatus !== 'COMPLETED' && userProgress?.exerciseStatus !== 'PENDING_DEEPDIVE' && !lastResponse) {
41
+ const msg = { FR: "Tu dois d'abord répondre !", WOLOF: "Dafa laaj nga tontu !", EN: "You must answer first!", ES: "¡Debes responder primero!", PT: "Você precisa responder primeiro!" }[user.language] ?? "Tu dois d'abord répondre !";
42
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
43
  return true;
44
  }
 
74
  where: { id: userProgress!.id },
75
  data: { exerciseStatus: 'PENDING_DEEPDIVE' }
76
  });
77
+ const msg = { FR: "Très bien ! Quelle info ?", WOLOF: "Wax ma ndox mi...", EN: "Great! What would you like to know?", ES: "¡Muy bien! ¿Qué información quieres?", PT: "Ótimo! Que informação você quer?" }[user.language] ?? "Très bien ! Quelle info ?";
78
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
79
  return true;
80
  } else {
81
+ const msg = { FR: "Réponds d'abord à l'exercice principal !", WOLOF: "Tontul exercice bi ba pare !", EN: "Answer the main exercise first!", ES: "¡Responde primero al ejercicio principal!", PT: "Responda primeiro ao exercício principal!" }[user.language] ?? "Réponds d'abord à l'exercice principal !";
82
  await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
83
  return true;
84
  }
apps/whatsapp-worker/src/handlers/NudgeHandler.ts CHANGED
@@ -41,15 +41,14 @@ export class NudgeHandler implements JobHandler {
41
  const tenantConfig = await this.getTenantConfig(organizationId || '', connection);
42
  if (!user?.phone) return;
43
 
44
- const isWolof = user.language === 'WOLOF';
45
- const messages = {
46
- ENCOURAGEMENT: isWolof
47
- ? "Assalamuyalaykum ! Fatte wuñu sa mbir. Tontu bu gatt ngir wéy ? 💪"
48
- : "Coucou ! On n'a pas oublié ton projet. Une petite réponse pour continuer ? 💪",
49
- RESURRECTION: isWolof
50
- ? "Sa liggeey mu ngi lay xaar ! Am succès dafa laaj lëkkalë. Ñu tàmbaleeti ? 🚀"
51
- : "Ton business t'attend ! Le succès vient de la régularité. On s'y remet ? 🚀"
52
  };
 
53
 
54
  const nudgeType = (type || 'ENCOURAGEMENT') as keyof typeof messages;
55
  const text = messages[nudgeType] || messages.ENCOURAGEMENT;
 
41
  const tenantConfig = await this.getTenantConfig(organizationId || '', connection);
42
  if (!user?.phone) return;
43
 
44
+ const nudgeStrings: Record<string, { ENCOURAGEMENT: string; RESURRECTION: string }> = {
45
+ FR: { ENCOURAGEMENT: "Coucou ! On n'a pas oublié ton projet. Une petite réponse pour continuer ? 💪", RESURRECTION: "Ton business t'attend ! Le succès vient de la régularité. On s'y remet ? 🚀" },
46
+ WOLOF: { ENCOURAGEMENT: "Assalamuyalaykum ! Fatte wuñu sa mbir. Tontu bu gatt ngir wéy ? 💪", RESURRECTION: "Sa liggeey mu ngi lay xaar ! Am succès dafa laaj lëkkalë. Ñu tàmbaleeti ? 🚀" },
47
+ EN: { ENCOURAGEMENT: "Hey! We haven't forgotten about your project. A quick reply to keep going? 💪", RESURRECTION: "Your business is waiting! Success comes from consistency. Let's get back to it? 🚀" },
48
+ ES: { ENCOURAGEMENT: "¡Hola! No hemos olvidado tu proyecto. ¿Una respuesta rápida para continuar? 💪", RESURRECTION: "¡Tu negocio te espera! El éxito viene de la constancia. ¿Lo retomamos? 🚀" },
49
+ PT: { ENCOURAGEMENT: "Oi! Não esquecemos do seu projeto. Uma resposta rápida para continuar? 💪", RESURRECTION: "Seu negócio está esperando! O sucesso vem da regularidade. Vamos retomar? 🚀" },
 
 
50
  };
51
+ const messages = nudgeStrings[user.language] ?? nudgeStrings['FR'];
52
 
53
  const nudgeType = (type || 'ENCOURAGEMENT') as keyof typeof messages;
54
  const text = messages[nudgeType] || messages.ENCOURAGEMENT;
apps/whatsapp-worker/src/handlers/OnboardingHandler.ts CHANGED
@@ -34,7 +34,8 @@ function getValidatedConfig(ctx: MessageContext) {
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
 
 
34
  }
35
 
36
  function getDefaultTrack(language: string): string {
37
+ // EN/ES/PT fall back to T1-FR until org creates language-specific tracks
38
+ const map: Record<string, string> = { FR: 'T1-FR', WOLOF: 'T1-WO', EN: 'T1-FR', ES: 'T1-FR', PT: 'T1-FR' };
39
  return map[language] ?? 'T1-FR';
40
  }
41