CognxSafeTrack Claude Sonnet 4.6 commited on
Commit ·
f786c37
1
Parent(s): 71969b1
feat(i18n): full 5-language support across all bot handlers
Browse filesExtend 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 +2 -6
- apps/whatsapp-worker/src/handlers/EnrollHandler.ts +11 -1
- apps/whatsapp-worker/src/handlers/ExerciseHandler.ts +15 -7
- apps/whatsapp-worker/src/handlers/FeedbackHandler.ts +1 -4
- apps/whatsapp-worker/src/handlers/NavigationHandler.ts +3 -3
- apps/whatsapp-worker/src/handlers/NudgeHandler.ts +7 -8
- apps/whatsapp-worker/src/handlers/OnboardingHandler.ts +2 -1
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 |
-
|
| 37 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 68 |
-
|
| 69 |
-
:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
}
|
| 108 |
|
| 109 |
-
|
|
|
|
| 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 =
|
| 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 =
|
| 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 =
|
| 78 |
await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
|
| 79 |
return true;
|
| 80 |
} else {
|
| 81 |
-
const msg =
|
| 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
|
| 45 |
-
|
| 46 |
-
ENCOURAGEMENT:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
RESURRECTION:
|
| 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 |
-
|
|
|
|
| 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 |
|