From 3023a1579cbf47c77952271534bab37b615c1a2f Mon Sep 17 00:00:00 2001
From: aguestuser <aguestuser@riseup.net>
Date: Tue, 19 Nov 2019 23:45:31 -0500
Subject: [PATCH] [75] update EN -> ES translations

side-effects:
* restructure `strings/messages` JSON structure
* add regression test to make sure translations are provided in ES for
  all EN strings
---
 app/services/dispatcher/commands/execute.js   |   8 +-
 app/services/dispatcher/commands/index.js     |   4 +-
 app/services/dispatcher/commands/parse.js     |   7 +-
 .../dispatcher/strings/messages/EN.js         | 143 +++++++-----
 .../dispatcher/strings/messages/ES.js         | 216 ++++++++++++------
 .../dispatcher/commands/execute.spec.js       |  32 +--
 .../dispatcher/commands/parse.spec.js         |  56 ++---
 .../unit/services/dispatcher/messages.spec.js |   3 +-
 .../services/dispatcher/messenger.spec.js     |   2 +-
 test/unit/services/dispatcher/strings.spec.js |  19 ++
 10 files changed, 303 insertions(+), 187 deletions(-)
 create mode 100644 test/unit/services/dispatcher/strings.spec.js

diff --git a/app/services/dispatcher/commands/execute.js b/app/services/dispatcher/commands/execute.js
index bafaa8f..c6b9a88 100644
--- a/app/services/dispatcher/commands/execute.js
+++ b/app/services/dispatcher/commands/execute.js
@@ -52,7 +52,7 @@ const execute = async (executable, dispatchable) => {
 // ADD
 
 const maybeAddPublisher = async (db, channel, sender, phoneNumberInput) => {
-  const cr = messagesIn(sender.language).commandResponses.publisher.add
+  const cr = messagesIn(sender.language).commandResponses.add
   if (!(sender.type === PUBLISHER)) {
     return Promise.resolve({ status: statuses.UNAUTHORIZED, message: cr.unauthorized })
   }
@@ -102,7 +102,7 @@ const showInfo = async (db, channel, sender, cr) => ({
 // JOIN
 
 const maybeAddSubscriber = async (db, channel, sender, language) => {
-  const cr = messagesIn(language).commandResponses.subscriber.add
+  const cr = messagesIn(language).commandResponses.join
   return sender.type === SUBSCRIBER
     ? Promise.resolve({ status: statuses.NOOP, message: cr.noop })
     : addSubscriber(db, channel, sender, language, cr)
@@ -117,7 +117,7 @@ const addSubscriber = (db, channel, sender, language, cr) =>
 // LEAVE
 
 const maybeRemoveSender = async (db, channel, sender) => {
-  const cr = messagesIn(sender.language).commandResponses.subscriber.remove
+  const cr = messagesIn(sender.language).commandResponses.leave
   return sender.type === NONE
     ? Promise.resolve({ status: statuses.UNAUTHORIZED, message: cr.unauthorized })
     : removeSender(db, channel, sender, cr)
@@ -136,7 +136,7 @@ const removeSender = (db, channel, sender, cr) => {
 // REMOVE
 
 const maybeRemovePublisher = async (db, channel, sender, publisherNumber) => {
-  const cr = messagesIn(sender.language).commandResponses.publisher.remove
+  const cr = messagesIn(sender.language).commandResponses.remove
   const { isValid, phoneNumber: validNumber } = validator.parseValidPhoneNumber(publisherNumber)
 
   if (!(sender.type === PUBLISHER)) {
diff --git a/app/services/dispatcher/commands/index.js b/app/services/dispatcher/commands/index.js
index fdbe9ad..3ce17ff 100644
--- a/app/services/dispatcher/commands/index.js
+++ b/app/services/dispatcher/commands/index.js
@@ -1,8 +1,8 @@
 const { execute } = require('./execute')
-const { parseCommand } = require('./parse')
+const { parseExecutable } = require('./parse')
 
 // Dispatchable -> Promise<{dispatchable: Dispatchable, commandResult: CommandResult}>
 const processCommand = dispatchable =>
-  execute(parseCommand(dispatchable.sdMessage.messageBody), dispatchable)
+  execute(parseExecutable(dispatchable.sdMessage.messageBody), dispatchable)
 
 module.exports = { processCommand }
diff --git a/app/services/dispatcher/commands/parse.js b/app/services/dispatcher/commands/parse.js
index b38a44d..f42d9cc 100644
--- a/app/services/dispatcher/commands/parse.js
+++ b/app/services/dispatcher/commands/parse.js
@@ -3,9 +3,8 @@ const { commandsByLanguage } = require('../strings/commands')
 const { commands } = require('./constants')
 const { defaultLanguage } = require('../../../config')
 
-// TODO(aguestuser|2019-11-17): rename this parseExecutable
 // string -> Executable
-const parseCommand = msg => {
+const parseExecutable = msg => {
   const { command, language, matches } = _findCommandMatch(msg) || {}
   return {
     command: command || commands.NOOP,
@@ -14,7 +13,7 @@ const parseCommand = msg => {
   }
 }
 
-// string -> {command: string, language: string, matches: Array<string>}
+// string -> Array<{command: string, language: string, matches: Array<string>}>
 const _findCommandMatch = msg => {
   // attempt to match on every variant of every command in every language
   // return first variant that matches (along with language and payload capturing group)
@@ -33,4 +32,4 @@ const _findCommandMatch = msg => {
   return find(matchResults, ({ matches }) => !isEmpty(matches))
 }
 
-module.exports = { parseCommand }
+module.exports = { parseExecutable }
diff --git a/app/services/dispatcher/strings/messages/EN.js b/app/services/dispatcher/strings/messages/EN.js
index 8a6dc99..119412b 100644
--- a/app/services/dispatcher/strings/messages/EN.js
+++ b/app/services/dispatcher/strings/messages/EN.js
@@ -10,18 +10,32 @@ const support = `
 HOW IT WORKS
 ----------------------------
 
--> Signalboost has admins and subscribers.
--> Admins send announcements that are broadcast to subscribers.
--> People can subscribe by sending HELLO or HOLA to this number.
--> Unsubscribe by sending GOODBYE or ADÍOS to this number.
--> Send HELP or AYUDA to list commands that make Signalboost do things.
--> Learn more: https://signalboost.info
+Signalboost numbers have admins and subscribers.
+
+-> When admins send messages, they are broadcast to all subscribers.
+-> If enabled, subscribers can send responses that only admins can read.
+-> Subscribers cannot send messages to each other. (No noisy crosstalk!)
+
+Signalboost numbers understand commands.
+
+-> Sending HELP lists the commands.
+-> People can subscribe by sending HELLO (or HOLA) and unsusbcribe with GOODBYE (or ADIÓS).
+-> Sending a language name (for example: ESPAÑOL or ENGLISH) switches languages.
+
+Signalboost tries to preserve your privacy.
+
+-> Signalboost users cannot see each other's phone numbers.
+-> Signalboost does not read or store anyone's messages.
+
+Learn more: https://signalboost.info
 `
 
 const notifications = {
   publisherAdded: (commandIssuer, addedPublisher) =>
     `New admin ${addedPublisher} added by ${commandIssuer}`,
-  broadcastResponseSent: channel => `Your message was forwarded to the admins of [${channel.name}]`,
+  broadcastResponseSent: channel =>
+    `Your message was forwarded to the admins of [${channel.name}].
+    Send HELP to see commands I understand! :)`,
   deauthorization: publisherPhoneNumber => `
 ${publisherPhoneNumber} has been removed from this channel because their safety number changed.
 
@@ -36,12 +50,14 @@ ADD ${publisherPhoneNumber}
 Until then, they will be unable to send messages to or read messages from this channel.`,
   noop: "Whoops! That's not a command!",
   unauthorized: "Whoops! I don't understand that.\n Send HELP to see commands I understand!",
+
   welcome: (addingPublisher, channelPhoneNumber) => `
 You were just made an admin of this Signalboost channel by ${addingPublisher}. Welcome!
 
 People can subscribe to this channel by sending HELLO to ${channelPhoneNumber} and unsubscribe by sending GOODBYE.
 
 Reply with HELP for more info.`,
+
   signupRequestReceived: (senderNumber, requestMsg) =>
     `Signup request received from ${senderNumber}:\n ${requestMsg}`,
   signupRequestResponse:
@@ -49,31 +65,35 @@ Reply with HELP for more info.`,
 }
 
 const commandResponses = {
-  // ADD/REMOVE
-  publisher: {
-    add: {
-      success: num => `${num} added as an admin.`,
-      unauthorized,
-      dbError: num => `Whoops! There was an error adding ${num} as an admin. Please try again!`,
-      invalidNumber,
-    },
-    remove: {
-      success: num => `${num} removed as an admin.`,
-      unauthorized,
-      dbError: num => `Whoops! There was an error trying to remove ${num}. Please try again!`,
-      invalidNumber,
-      targetNotPublisher: num => `Whoops! ${num} is not an admin. Can't remove them.`,
-    },
+  // ADD
+
+  add: {
+    success: num => `${num} added as an admin.`,
+    unauthorized,
+    dbError: num => `Whoops! There was an error adding ${num} as an admin. Please try again!`,
+    invalidNumber,
   },
+
+  // REMOVE
+
+  remove: {
+    success: num => `${num} removed as an admin.`,
+    unauthorized,
+    dbError: num => `Whoops! There was an error trying to remove ${num}. Please try again!`,
+    invalidNumber,
+    targetNotPublisher: num => `Whoops! ${num} is not an admin. Can't remove them.`,
+  },
+
   // HELP
+
   help: {
     publisher: `[COMMANDS I UNDERSTAND:]
 
-HELP / AYUDA
+HELP
 -> lists commands
 
 INFO
--> shows stats, explains how signalboost works
+-> shows stats, explains how Signalboost works
 
 RENAME new name
 -> renames channel to "new name"
@@ -90,24 +110,29 @@ RESPONSES ON
 RESPONSES OFF
 -> disables subscribers from sending messages to admins
 
-GOODBYE / ADIOS
--> leaves this channel`,
+GOODBYE
+-> leaves this channel
+
+ESPAÑOL
+-> switches language to Spanish`,
+
     subscriber: `[COMMANDS I UNDERSTAND:]
 
-HELP / AYUDA
+HELP
 -> lists commands
 
 INFO
 -> shows stats, explains how signalboost works
 
-HELLO / HOLA
+HELLO
 -> subscribes you to announcements
 
-GOODBYE / ADIOS
+GOODBYE
 -> unsubscribes you from announcements`,
   },
 
   // INFO
+
   info: {
     publisher: channel => `
 ---------------------------
@@ -121,6 +146,7 @@ admins: ${channel.publications.map(a => a.publisherPhoneNumber).join(', ')}
 responses: ${channel.responsesEnabled ? 'ON' : 'OFF'}
 messages sent: ${channel.messageCount.broadcastIn}
 ${support}`,
+
     subscriber: channel => `
 ---------------------------
 CHANNEL INFO:
@@ -130,11 +156,12 @@ name: ${channel.name}
 phone number: ${channel.phoneNumber}
 responses: ${channel.responsesEnabled ? 'ON' : 'OFF'}
 subscribers: ${channel.subscriptions.length}
-admins: ${channel.publications.length}
 ${support}`,
     unauthorized,
   },
+
   // RENAME
+
   rename: {
     success: (oldName, newName) =>
       `[${newName}]\nChannel renamed from "${oldName}" to "${newName}".`,
@@ -142,52 +169,56 @@ ${support}`,
       `[${oldName}]\nWhoops! There was an error renaming the channel [${oldName}] to [${newName}]. Try again!`,
     unauthorized,
   },
-  // JOIN/LEAVE
-  subscriber: {
-    add: {
-      success: channel => {
-        const { name } = channel
-        return `
+
+  // JOIN
+
+  join: {
+    success: channel => {
+      const { name } = channel
+      return `
 Welcome to Signalboost! You are now subscribed to the [${name}] channel.
 
 Reply with HELP to learn more or GOODBYE to unsubscribe.`
-      },
-      dbError: `Whoops! There was an error adding you to the channel. Please try again!`,
-      noop: `Whoops! You are already a member of the channel.`,
-    },
-    remove: {
-      success: `You've been removed from the channel! Bye!`,
-      error: `Whoops! There was an error removing you from the channel. Please try again!`,
-      unauthorized,
     },
+    dbError: `Whoops! There was an error adding you to the channel. Please try again!`,
+    noop: `Whoops! You are already a member of the channel.`,
+  },
+
+  // LEAVE
+
+  leave: {
+    success: `You've been removed from the channel! Bye!`,
+    error: `Whoops! There was an error removing you from the channel. Please try again!`,
+    unauthorized,
   },
-  // TOGGLE RESPONSES
+
+  // RESPONSES_ON / RESPONSES_OFF
+
   toggleResponses: {
     success: setting => `Subscriber responses turned ${upperCase(setting)}.`,
     unauthorized,
     dbError: setting =>
       `Whoops! There was an error trying to set responses to ${setting}. Please try again!`,
-    invalidSetting: setting =>
-      `Whoops! ${setting} is not a valid setting. You can set responses to be either ON or OFF.`,
   },
+
+  // SET_LANGUAGE
+
+  setLanguage: {
+    success: 'I will talk to you in English now! \n Send HELP to list commands I understand.',
+    dbError: 'Whoops! Failed to store your language preference. Please try again!',
+  },
+
+  // TRUST
+
   trust: {
     success: phoneNumber => `Updated safety number for ${phoneNumber}`,
     error: phoneNumber =>
       `Failed to update safety number for ${phoneNumber}. Try again or contact a maintainer!`,
-    partialError: (phoneNumber, success, error) =>
-      `Updated safety number for ${success} out of ${success +
-        error} channels that ${phoneNumber} belongs to.`,
     invalidNumber,
     unauthorized,
-    targetNotMember: phoneNumber =>
-      `Whoops! ${phoneNumber} is not an admin or subscriber on this channel. Cannot reactivate them.`,
     dbError: phoneNumber =>
       `Whoops! There was an error updating the safety number for ${phoneNumber}. Please try again!`,
   },
-  setLanguage: {
-    success: 'I will talk to you in English now!',
-    dbError: 'Failed to store your language preference. Please try again!',
-  },
 }
 
 const prefixes = {
diff --git a/app/services/dispatcher/strings/messages/ES.js b/app/services/dispatcher/strings/messages/ES.js
index 6792445..5e34ec4 100644
--- a/app/services/dispatcher/strings/messages/ES.js
+++ b/app/services/dispatcher/strings/messages/ES.js
@@ -1,71 +1,110 @@
 const { upperCase } = require('lodash')
-const unauthorized = 'Whoops! No tiene autorización para hacerlo en este canal.'
+
+const systemName = 'El administrador del sistema de Signalboost'
+const unauthorized = 'Lo siento! No tiene autorización para hacerlo en este canal.'
+const invalidNumber = phoneNumber =>
+  `¡Lo siento! "${phoneNumber}" no es un número de teléfono válido. Los números de teléfono deben incluir códigos de país con el prefijo '+'.`
 
 const support = `
 ----------------------------
 CÓMO FUNCIONA
 ----------------------------
 
--> Los números de Signalboost tienen administradores y suscriptores.
--> Los administradores envían anuncios que se transmiten a los suscriptores.
--> Suscríbete a los anuncios enviando "HOLA" a un número.
--> Darse de baja enviando "ADIOS" al número.
--> Enviar "AYUDA" a un número para enumerar los comandos que hacen que haga cosas.
--> Más información: https://0xacab.org/team-friendo/signalboost`
+Los números de Signalboost tienen administradores y suscriptores.
+
+-> Cuando los administradores envían mensajes, se transmiten a todos los suscriptores.
+-> Si está habilitado, los suscriptores pueden enviar respuestas que solo los administradores pueden leer.
+-> Los suscriptores no pueden enviarse mensajes entre sí. (¡No hay diafonía ruidosa!)
+
+Los números de Signalboost entienden los comandos.
+
+-> Las personas pueden suscribirse enviando HOLA y darse de baja con ADIÓS.
+-> Enviar un nombre de idioma (por ejemplo: ESPAÑOL o ENGLISH) cambia de idioma.
+-> Enviar AYUDA enumera los comandos.
+
+Signalboost intenta preservar su privacidad.
+
+-> Los usuarios de Signalboost no pueden ver los números de teléfono de los demás.
+-> Signalboost no lee ni almacena los mensajes de nadie.
+
+Para más información: https://signalboost.info`
 
 const notifications = {
-  broadcastResponseSent: channel => `
-Su mensaje fue enviado a los administradores de [${channel.name}].
-¡Envíe AYUDA para ver los comandos que entiendo! :)
-`,
-  welcome: (channel, addingPublisher) => {
-    const { name } = channel
-    return `
-¡Bienvenido a Signalboost! ${addingPublisher} te acaba de hacer un administrador del canal [${name}].
-
-Responda con AYUDA para obtener más información o ADIÓS para irse.`
-  },
-  noop: 'Whoops! Eso no es un comando!',
+  publisherAdded: (commandIssuer, addedPublisher) =>
+    `Nuevo administrador ${addedPublisher} agregado por ${commandIssuer}`,
+
+  broadcastResponseSent: channel =>
+    `Su mensaje fue enviado a los administradores de [${channel.name}].
+    ¡Envíe AYUDA para ver los comandos que entiendo! :)`,
+
+  deauthorization: publisherPhoneNumber => `
+${publisherPhoneNumber} se ha eliminado de este canal porque su número de seguridad cambió.
+    
+Esto es casi seguro porque reinstalaron Signal en un nuevo teléfono.
+
+Sin embargo, existe una pequeña posibilidad de que un atacante haya comprometido su teléfono y esté tratando de hacerse pasar por él.
+
+Verifique con ${publisherPhoneNumber} para asegurarse de que todavía controlan su teléfono, luego vuelva a autorizarlos con:
+  
+  AGREGAR ${publisherPhoneNumber}
+  
+  Hasta entonces, no podrán enviar mensajes ni leer mensajes de este canal.`,
+
+  welcome: (addingPublisher, channelPhoneNumber) => `
+Acabas de convertirte en administrador de este canal Signalboost por ${addingPublisher}. ¡Bienvenidos!
+
+Las personas pueden suscribirse a este canal enviando HOLA a ${channelPhoneNumber} y cancelar la suscripción enviando ADIÓS.
+
+Responda con AYUDA para más información.`,
+
+  noop: 'Lo siento! Eso no es un comando!',
   unauthorized: `
-Whoops! No entiendo eso.
-¡Envíe AYUDA para ver los comandos que entiendo! :)`,
+Lo siento! No entiendo eso.
+Envíe AYUDA para ver los comandos que entiendo! :)`,
+
+  signupRequestReceived: (senderNumber, requestMsg) =>
+    `Solicitud de registro recibida de ${senderNumber}: \n {requestMsg}`,
+  signupRequestResponse:
+    '¡Gracias por registrarte en Signalboost! \nEn breve recibirás un mensaje de bienvenida en tu nuevo canal...',
 }
 
 const commandResponses = {
-  // ADD/REMOVE PUBLISHER
-  publisher: {
-    add: {
-      success: num => `${num} agregó como administrador.`,
-      unauthorized,
-      dbError: num =>
-        `Whoops! Se produjo un error al agregar ${num} como administrador. ¡Inténtalo de nuevo!`,
-      invalidNumber: num =>
-        `Whoops! Error al agregar "${num}". Los números de teléfono deben incluir códigos de país con el prefijo '+'`,
-    },
-    remove: {
-      success: num => `${num} eliminado como administrador.`,
-      unauthorized,
-      dbError: num =>
-        `Whoops! Se produjo un error al intentar eliminar ${num}. ¡Inténtalo de nuevo!`,
-      invalidNumber: num =>
-        `¡Vaya! Error al eliminar "${num}". Los números de teléfono deben incluir códigos de país con el prefijo '+'`,
-      targetNotPublisher: num => `¡Vaya! ${num} no es un administrador. No puedo eliminarla.`,
-    },
+  // ADD
+
+  add: {
+    success: num => `${num} agregó como administrador.`,
+    unauthorized,
+    dbError: num =>
+      `¡Lo siento! Se produjo un error al agregar ${num} como administrador. ¡Inténtalo de nuevo!`,
+    invalidNumber: num =>
+      `¡Lo siento! Error al agregar "${num}". Los números de teléfono deben incluir códigos de país con el prefijo '+'`,
+  },
+
+  // REMOVE
+
+  remove: {
+    success: num => `${num} eliminado como administrador.`,
+    unauthorized,
+    dbError: num =>
+      `¡Lo siento! Se produjo un error al intentar eliminar ${num}. ¡Inténtalo de nuevo!`,
+    invalidNumber: num =>
+      `¡Lo siento. Error al eliminar "${num}". Los números de teléfono deben incluir códigos de país con el prefijo '+'`,
+    targetNotPublisher: num => `Lo siento. ${num} no es un administrador. No puedo eliminarla.`,
   },
+
   // HELP
+
   help: {
-    publisher: `
+    publisher: `[COMMANDS I UNDERSTAND:]
 AYUDA
 -> listas de comandos
 
-INFORMACIÓN
--> muestra estadísticas, explica el refuerzo de señal
+INFO
+-> muestra estadísticas, explica cómo funciona Signalboost
 
 RENOMBRAR nuevo nombre
 -> cambia el nombre del canal a "nuevo nombre"
 
-RESPUESTAS ACTIVADAS / RESPUESTAS DESACTIVADAS
--> activa / desactiva las respuestas del suscriptor
 
 AGREGAR + 1-555-555-5555
 -> convierte a + 1-555-555-5555 en administrador
@@ -73,14 +112,22 @@ AGREGAR + 1-555-555-5555
 QUITAR + 1-555-555-5555
 -> elimina + 1-555-555-5555 como administrador
 
+RESPUESTAS ACTIVADAS
+-> permite a los suscriptores enviar mensajes a los administradores
+
+RESPUESTAS DESACTIVADAS
+-> desactiva a los suscriptores de enviar mensajes a los administradores
+
 ADIÓS
 -> te quita del canal`,
-    subscriber: `
+
+    subscriber: `[COMMANDS I UNDERSTAND:]
+    
 AYUDA
 -> listas de comandos
 
-INFORMACIÓN
--> explica el refuerzo de señal
+INFO
+-> explica explica cómo funciona Signalboost
 
 HOLA
 -> te suscribe a mensajes
@@ -90,6 +137,7 @@ ADIÓS
   },
 
   // INFO
+
   info: {
     publisher: channel => `
 ---------------------------
@@ -103,66 +151,86 @@ respuestas: ${channel.responsesEnabled ? 'ACTIVADAS' : 'DESACTIVADAS'}
 mensajes enviados: ${channel.messageCount.broadcastIn}
 administradorxs: ${channel.publications.map(a => a.publisherPhoneNumber).join(',')}
 ${support}`,
+
     subscriber: channel => `
 ---------------------------
-CHANNEL INFO:
+INFO DEL CANAL:
 ---------------------------
 
 nombre: ${channel.name}
 número de teléfono: ${channel.phoneNumber}
 respuestas: ${channel.responsesEnabled ? 'ACTIVADAS' : 'DESACTIVADAS'}
 suscriptorxs: ${channel.subscriptions.length}
-administradorxs: ${channel.publications.length}
 ${support}`,
     unauthorized,
   },
+
   // RENAME
+
   rename: {
     success: (oldName, newName) => `[${newName}]\nCanal renombrado de "${oldName}" a "${newName}".`,
     dbError: (oldName, newName) =>
-      `[${oldName}]\nWhoops! Se produjo un error al cambiar el nombre del canal [${oldName}] a [${newName}]. ¡Inténtalo de nuevo!`,
+      `[${oldName}]\nLo siento! Se produjo un error al cambiar el nombre del canal [${oldName}] a [${newName}]. ¡Inténtalo de nuevo!`,
     unauthorized,
   },
-  // ADD/REMOVE SUBSCRIBER
-  subscriber: {
-    add: {
-      success: channel => {
-        const { name } = channel
-        return `
-¡Bienvenido a Signalboost! Ahora está suscrito al canal [${name}].
+
+  // JOIN
+
+  join: {
+    success: channel => {
+      const { name } = channel
+      return `
+¡Bienvenido a Signalboost! Ahora estás suscrito al canal [${name}].
 
 Responda con AYUDA para obtener más información o ADIÓS para darse de baja.`
-      },
-      dbError: `Whoops! Se produjo un error al agregarlo al canal. ¡Inténtalo de nuevo!`,
-      noop: `Whoops! Ya eres miembro del canal.`,
-    },
-    remove: {
-      success: `¡Has sido eliminado del canal! ¡Adiós!`,
-      error: `Whoops! Se produjo un error al eliminarlo del canal. ¡Inténtalo de nuevo!`,
-      unauthorized,
     },
+    dbError: `Lo siento! Se produjo un error al agregarlo al canal. Inténtalo de nuevo!`,
+    noop: `¡Lo siento! Ya eres miembro del canal.`,
   },
-  // TOGGLE RESPONSES
+
+  // LEAVE
+
+  leave: {
+    success: `¡Has sido eliminado del canal! ¡Adiós!`,
+    error: `¡Lo siento! Se produjo un error al eliminarlo del canal. ¡Inténtalo de nuevo!`,
+    unauthorized,
+  },
+
+  // RESPONSES_ON / RESPONSES_OFF
+
   toggleResponses: {
     success: setting => `Respuestas del suscriptor configurado en ${upperCase(setting)}.`,
     unauthorized,
     dbError: setting =>
-      `Whoops! Se produjo un error al intentar establecer respuestas a $ {setting}. ¡Inténtalo de nuevo!`,
-    invalidSetting: setting =>
-      `Whoops! $ {setting} no es una configuración válida. Puede configurar las respuestas para que estén ACTIVADAS o DESACTIVADAS.`,
+      `¡Lo siento! Se produjo un error al intentar establecer respuestas a ${setting}. ¡Inténtalo de nuevo!`,
   },
+
+  // SET_LANGUAGE
+
   setLanguage: {
-    success: '¡Hablaré contigo en español ahora!',
-    dbError: 'Lo ciento. Era un error. ¡Inténtalo de nuevo!',
+    success: '¡Hablaré contigo en español ahora! \nEnvia AYUDA para comandos que comprendo.',
+    dbError: '¡Lo siento! No se pudo almacenar su preferencia de idioma. ¡Inténtalo de nuevo!',
+  },
+
+  // TRUST
+
+  trust: {
+    success: phoneNumber => `Número de seguridad actualizado para ${phoneNumber}`,
+    error: phoneNumber =>
+      `Error al actualizar el número de seguridad para ${phoneNumber}. ¡Inténtalo de nuevo o contacta a un mantenedor!`,
+    invalidNumber,
+    unauthorized,
+    dbError: phoneNumber =>
+      `¡Lo siento! Se produjo un error al actualizar el número de seguridad de ${phoneNumber}. ¡Inténtalo de nuevo!`,
   },
 }
 
 const prefixes = {
-  helpResponse: `COMANDOS QUE ENTIENDO ...`,
-  broadcastResponse: `RESPUESTA DEL SUSCRIPTOR ...`,
+  broadcastResponse: `RESPUESTA DEL SUSCRIPTOR:`,
 }
 
 const EN = {
+  systemName,
   commandResponses,
   notifications,
   prefixes,
diff --git a/test/unit/services/dispatcher/commands/execute.spec.js b/test/unit/services/dispatcher/commands/execute.spec.js
index 7767942..1c08f6e 100644
--- a/test/unit/services/dispatcher/commands/execute.spec.js
+++ b/test/unit/services/dispatcher/commands/execute.spec.js
@@ -83,7 +83,7 @@ describe('executing commands', () => {
             expect(await processCommand(dispatchable)).to.eql({
               command: commands.ADD,
               status: statuses.SUCCESS,
-              message: CR.publisher.add.success(publisherPhoneNumber),
+              message: CR.add.success(publisherPhoneNumber),
               payload: publisherPhoneNumber,
             })
           })
@@ -96,7 +96,7 @@ describe('executing commands', () => {
             expect(await processCommand(dispatchable)).to.eql({
               command: commands.ADD,
               status: statuses.ERROR,
-              message: CR.publisher.add.dbError(publisherPhoneNumber),
+              message: CR.add.dbError(publisherPhoneNumber),
             })
           })
         })
@@ -115,7 +115,7 @@ describe('executing commands', () => {
           expect(result).to.eql({
             command: commands.ADD,
             status: statuses.ERROR,
-            message: CR.publisher.add.invalidNumber('foo'),
+            message: CR.add.invalidNumber('foo'),
           })
         })
       })
@@ -139,7 +139,7 @@ describe('executing commands', () => {
         expect(result).to.eql({
           command: commands.ADD,
           status: statuses.UNAUTHORIZED,
-          message: CR.publisher.add.unauthorized,
+          message: CR.add.unauthorized,
         })
       })
     })
@@ -256,7 +256,7 @@ describe('executing commands', () => {
           expect(await processCommand(dispatchable)).to.eql({
             command: commands.JOIN,
             status: statuses.SUCCESS,
-            message: CR.subscriber.add.success(channel),
+            message: CR.join.success(channel),
           })
         })
       })
@@ -268,7 +268,7 @@ describe('executing commands', () => {
           expect(await processCommand(dispatchable)).to.eql({
             command: commands.JOIN,
             status: statuses.ERROR,
-            message: CR.subscriber.add.error,
+            message: CR.join.error,
           })
         })
       })
@@ -288,7 +288,7 @@ describe('executing commands', () => {
         expect(result).to.eql({
           command: commands.JOIN,
           status: statuses.NOOP,
-          message: CR.subscriber.add.noop,
+          message: CR.join.noop,
         })
       })
     })
@@ -320,7 +320,7 @@ describe('executing commands', () => {
           expect(await processCommand(dispatchable)).to.eql({
             command: commands.LEAVE,
             status: statuses.SUCCESS,
-            message: CR.subscriber.remove.success,
+            message: CR.leave.success,
           })
         })
       })
@@ -331,7 +331,7 @@ describe('executing commands', () => {
           expect(await processCommand(dispatchable)).to.eql({
             command: commands.LEAVE,
             status: statuses.ERROR,
-            message: CR.subscriber.remove.error,
+            message: CR.leave.error,
           })
         })
       })
@@ -350,7 +350,7 @@ describe('executing commands', () => {
         expect(result).to.eql({
           command: commands.LEAVE,
           status: statuses.UNAUTHORIZED,
-          message: CR.subscriber.remove.unauthorized,
+          message: CR.leave.unauthorized,
         })
       })
     })
@@ -379,7 +379,7 @@ describe('executing commands', () => {
         expect(result).to.eql({
           command: commands.LEAVE,
           status: statuses.SUCCESS,
-          message: CR.subscriber.remove.success,
+          message: CR.leave.success,
         })
       })
     })
@@ -430,7 +430,7 @@ describe('executing commands', () => {
               expect(await processCommand(dispatchable)).to.eql({
                 command: commands.REMOVE,
                 status: statuses.SUCCESS,
-                message: CR.publisher.remove.success(publisherPhoneNumber),
+                message: CR.remove.success(publisherPhoneNumber),
               })
             })
           })
@@ -442,7 +442,7 @@ describe('executing commands', () => {
               expect(await processCommand(dispatchable)).to.eql({
                 command: commands.REMOVE,
                 status: statuses.ERROR,
-                message: CR.publisher.remove.dbError(publisherPhoneNumber),
+                message: CR.remove.dbError(publisherPhoneNumber),
               })
             })
           })
@@ -459,7 +459,7 @@ describe('executing commands', () => {
             expect(await processCommand(dispatchable)).to.eql({
               command: commands.REMOVE,
               status: statuses.ERROR,
-              message: CR.publisher.remove.targetNotPublisher(publisherPhoneNumber),
+              message: CR.remove.targetNotPublisher(publisherPhoneNumber),
             })
           })
         })
@@ -479,7 +479,7 @@ describe('executing commands', () => {
           expect(result).to.eql({
             command: commands.REMOVE,
             status: statuses.ERROR,
-            message: CR.publisher.remove.invalidNumber('foo'),
+            message: CR.remove.invalidNumber('foo'),
           })
         })
       })
@@ -500,7 +500,7 @@ describe('executing commands', () => {
         expect(result).to.eql({
           command: commands.REMOVE,
           status: statuses.UNAUTHORIZED,
-          message: CR.publisher.remove.unauthorized,
+          message: CR.remove.unauthorized,
         })
       })
     })
diff --git a/test/unit/services/dispatcher/commands/parse.spec.js b/test/unit/services/dispatcher/commands/parse.spec.js
index 8a88a3f..9e4e8c9 100644
--- a/test/unit/services/dispatcher/commands/parse.spec.js
+++ b/test/unit/services/dispatcher/commands/parse.spec.js
@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import { describe, it } from 'mocha'
 import { commands } from '../../../../../app/services/dispatcher/commands/constants'
-import { parseCommand } from '../../../../../app/services/dispatcher/commands/parse'
+import { parseExecutable } from '../../../../../app/services/dispatcher/commands/parse'
 import { languages } from '../../../../../app/constants'
 import { defaultLanguage } from '../../../../../app/config'
 
@@ -31,7 +31,7 @@ describe('parsing commands', () => {
         'hace ESPAÑOL',
       ]
       msgs.forEach(msg =>
-        expect(parseCommand(msg)).to.eql({
+        expect(parseExecutable(msg)).to.eql({
           command: commands.NOOP,
           language: defaultLanguage,
           payload: '',
@@ -45,7 +45,7 @@ describe('parsing commands', () => {
       it('parses an ADD command (regardless of case or whitespace)', () => {
         const msgs = ['ADD', 'add', ' add ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.ADD,
             language: languages.EN,
             payload: '',
@@ -54,7 +54,7 @@ describe('parsing commands', () => {
       })
 
       it('parses the payload from an ADD command', () => {
-        expect(parseCommand('ADD foo')).to.eql({
+        expect(parseExecutable('ADD foo')).to.eql({
           command: commands.ADD,
           language: languages.EN,
           payload: 'foo',
@@ -66,7 +66,7 @@ describe('parsing commands', () => {
       it('parses a HELP command (regardless of case or whitespace)', () => {
         const msgs = ['HELP', 'help', ' help ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.HELP,
             language: languages.EN,
             payload: '',
@@ -79,7 +79,7 @@ describe('parsing commands', () => {
       it('parses an INFO command (regardless of case or whitespace)', () => {
         const msgs = ['INFO', 'info', ' info ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.INFO,
             language: languages.EN,
             payload: '',
@@ -92,7 +92,7 @@ describe('parsing commands', () => {
       it('parses an JOIN command from "hello" or "join" (regardless of case or whitespace)', () => {
         const msgs = ['HELLO', 'hello', ' hello ', 'JOIN', 'join', '  join ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.JOIN,
             language: languages.EN,
             payload: '',
@@ -105,7 +105,7 @@ describe('parsing commands', () => {
       it('parses an LEAVE command from "goodbye" or "leave" (regardless of case or whitespace)', () => {
         const msgs = ['GOODBYE', 'goodbye', ' goodbye ', 'LEAVE', 'leave', '  leave ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.LEAVE,
             language: languages.EN,
             payload: '',
@@ -118,7 +118,7 @@ describe('parsing commands', () => {
       it('parses an REMOVE command (regardless of case or whitespace)', () => {
         const msgs = ['REMOVE', 'remove', ' remove ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.REMOVE,
             language: languages.EN,
             payload: '',
@@ -127,7 +127,7 @@ describe('parsing commands', () => {
       })
 
       it('parses the payload from an REMOVE command', () => {
-        expect(parseCommand('REMOVE foo')).to.eql({
+        expect(parseExecutable('REMOVE foo')).to.eql({
           command: commands.REMOVE,
           language: languages.EN,
           payload: 'foo',
@@ -140,7 +140,7 @@ describe('parsing commands', () => {
         it('parses an RENAME command (regardless of case or whitespace)', () => {
           const msgs = ['RENAME', 'rename', ' rename ']
           msgs.forEach(msg =>
-            expect(parseCommand(msg)).to.eql({
+            expect(parseExecutable(msg)).to.eql({
               command: commands.RENAME,
               language: languages.EN,
               payload: '',
@@ -150,7 +150,7 @@ describe('parsing commands', () => {
       })
 
       it('parses the payload from an RENAME command', () => {
-        expect(parseCommand('RENAME foo')).to.eql({
+        expect(parseExecutable('RENAME foo')).to.eql({
           command: commands.RENAME,
           language: languages.EN,
           payload: 'foo',
@@ -163,7 +163,7 @@ describe('parsing commands', () => {
         it('parses an RENAME command (regardless of case or whitespace)', () => {
           const msgs = ['RESPONSES ON', 'responses on', ' responses  on ']
           msgs.forEach(msg =>
-            expect(parseCommand(msg)).to.eql({
+            expect(parseExecutable(msg)).to.eql({
               command: commands.RESPONSES_ON,
               language: languages.EN,
               payload: '',
@@ -178,7 +178,7 @@ describe('parsing commands', () => {
         it('parses an RENAME command (regardless of case or whitespace)', () => {
           const msgs = ['RESPONSES OFF', 'responses off', ' responses  off ']
           msgs.forEach(msg =>
-            expect(parseCommand(msg)).to.eql({
+            expect(parseExecutable(msg)).to.eql({
               command: commands.RESPONSES_OFF,
               language: languages.EN,
               payload: '',
@@ -192,7 +192,7 @@ describe('parsing commands', () => {
       it('sets the language to English regardless of language in which English is specified', () => {
         const msgs = ['ENGLISH', 'INGLÉS', 'INGLES', 'english', 'inglés', 'ingles']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.SET_LANGUAGE,
             language: languages.EN,
             payload: '',
@@ -207,7 +207,7 @@ describe('parsing commands', () => {
       it('parses an ADD command (regardless of case or whitespace)', () => {
         const msgs = ['AGREGAR', 'agregar', ' agregar ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.ADD,
             language: languages.ES,
             payload: '',
@@ -216,7 +216,7 @@ describe('parsing commands', () => {
       })
 
       it('parses the payload from an ADD command', () => {
-        expect(parseCommand('AGREGAR foo')).to.eql({
+        expect(parseExecutable('AGREGAR foo')).to.eql({
           command: commands.ADD,
           language: languages.ES,
           payload: 'foo',
@@ -228,7 +228,7 @@ describe('parsing commands', () => {
       it('parses an HELP command (regardless of case or whitespace)', () => {
         const msgs = ['AYUDA', 'ayuda', ' ayuda ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.HELP,
             language: languages.ES,
             payload: '',
@@ -241,7 +241,7 @@ describe('parsing commands', () => {
       it('parses an INFO command but does NOT detect language', () => {
         const msgs = ['INFO', 'info', ' info ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.INFO,
             language: languages.EN,
             payload: '',
@@ -254,7 +254,7 @@ describe('parsing commands', () => {
       it('parses an JOIN command from "hola"(regardless of case or whitespace)', () => {
         const msgs = ['HOLA', 'hola', ' hola ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.JOIN,
             language: languages.ES,
             payload: '',
@@ -266,7 +266,7 @@ describe('parsing commands', () => {
     it('parses an LEAVE command from "ADIOS" (regardless of accents, case or whitespace)', () => {
       const msgs = ['ADIÓS', 'adiós', ' adiós ', 'ADIOS', 'adios', '  adios ']
       msgs.forEach(msg =>
-        expect(parseCommand(msg)).to.eql({
+        expect(parseExecutable(msg)).to.eql({
           command: commands.LEAVE,
           language: languages.ES,
           payload: '',
@@ -279,7 +279,7 @@ describe('parsing commands', () => {
     it('parses a REMOVE command (regardless of case or whitespace)', () => {
       const msgs = ['ELIMINAR', 'eliminar', ' eliminar ']
       msgs.forEach(msg =>
-        expect(parseCommand(msg)).to.eql({
+        expect(parseExecutable(msg)).to.eql({
           command: commands.REMOVE,
           language: languages.ES,
           payload: '',
@@ -288,7 +288,7 @@ describe('parsing commands', () => {
     })
 
     it('parses the payload from an REMOVE command', () => {
-      expect(parseCommand('ELIMINAR foo')).to.eql({
+      expect(parseExecutable('ELIMINAR foo')).to.eql({
         command: commands.REMOVE,
         language: languages.ES,
         payload: 'foo',
@@ -301,7 +301,7 @@ describe('parsing commands', () => {
       it('parses an RENAME command (regardless of case or whitespace)', () => {
         const msgs = ['RENOMBRAR', 'renombrar', ' renombrar ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.RENAME,
             language: languages.ES,
             payload: '',
@@ -311,7 +311,7 @@ describe('parsing commands', () => {
     })
 
     it('parses the payload from an RENAME command', () => {
-      expect(parseCommand('RENOMBRAR foo')).to.eql({
+      expect(parseExecutable('RENOMBRAR foo')).to.eql({
         command: commands.RENAME,
         language: languages.ES,
         payload: 'foo',
@@ -324,7 +324,7 @@ describe('parsing commands', () => {
       it('parses an RENAME command (regardless of case or whitespace)', () => {
         const msgs = ['RESPUESTAS ACTIVADAS', 'respuestas activadas', ' respuestas  activadas ']
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.RESPONSES_ON,
             language: languages.ES,
             payload: '',
@@ -343,7 +343,7 @@ describe('parsing commands', () => {
           ' respuestas  desactivadas ',
         ]
         msgs.forEach(msg =>
-          expect(parseCommand(msg)).to.eql({
+          expect(parseExecutable(msg)).to.eql({
             command: commands.RESPONSES_OFF,
             language: languages.ES,
             payload: '',
@@ -357,7 +357,7 @@ describe('parsing commands', () => {
     it('sets the language to Spanish regardless of language in which English is specified', () => {
       const msgs = ['ESPAÑOL', 'ESPANOL', 'SPANISH', 'español', 'espanol', 'spanish']
       msgs.forEach(msg =>
-        expect(parseCommand(msg)).to.eql({
+        expect(parseExecutable(msg)).to.eql({
           command: commands.SET_LANGUAGE,
           language: languages.ES,
           payload: '',
diff --git a/test/unit/services/dispatcher/messages.spec.js b/test/unit/services/dispatcher/messages.spec.js
index f2bc6e5..d6ecbbb 100644
--- a/test/unit/services/dispatcher/messages.spec.js
+++ b/test/unit/services/dispatcher/messages.spec.js
@@ -31,9 +31,8 @@ describe('messages module', () => {
       })
 
       describe('for subscriber', () => {
-        it('shows publisher count and subscriber count', () => {
+        it('shows subscriber count', () => {
           const msg = cr.info.subscriber(channel)
-          expect(msg).to.include('admins: 2')
           expect(msg).to.include('subscribers: 2')
         })
       })
diff --git a/test/unit/services/dispatcher/messenger.spec.js b/test/unit/services/dispatcher/messenger.spec.js
index afb665d..df1f7c2 100644
--- a/test/unit/services/dispatcher/messenger.spec.js
+++ b/test/unit/services/dispatcher/messenger.spec.js
@@ -311,7 +311,7 @@ describe('messenger service', () => {
       describe('for a newly added publisher', () => {
         const newPublisher = genPhoneNumber()
         const sdMessage = `${commands.ADD} ${newPublisher}`
-        const response = messages.commandResponses.publisher.add.success(newPublisher)
+        const response = messages.commandResponses.add.success(newPublisher)
         const welcome = messages.notifications.welcome(
           publisherSender.phoneNumber,
           channel.phoneNumber,
diff --git a/test/unit/services/dispatcher/strings.spec.js b/test/unit/services/dispatcher/strings.spec.js
new file mode 100644
index 0000000..8f58539
--- /dev/null
+++ b/test/unit/services/dispatcher/strings.spec.js
@@ -0,0 +1,19 @@
+import { describe, it } from 'mocha'
+import { expect } from 'chai'
+import messagesEN from '../../../../app/services/dispatcher/strings/messages/EN'
+import messagesES from '../../../../app/services/dispatcher/strings/messages/ES'
+
+describe('string translations', () => {
+  describe('for messages', () => {
+    it('has an ES string for every EN string', () => {
+      expect(messagesES.systemName).to.exist
+      Object.keys(messagesEN.commandResponses).forEach(
+        key => expect(messagesES.commandResponses[key]).to.exist,
+      )
+      Object.keys(messagesEN.notifications).forEach(
+        key => expect(messagesES.notifications[key]).to.exist,
+      )
+      Object.keys(messagesEN.prefixes).forEach(key => expect(messagesES.prefixes[key]).to.exist)
+    })
+  })
+})
-- 
GitLab