diff --git a/apps/ui/public/locales/en.json b/apps/ui/public/locales/en.json index f2f0179a..3156d95d 100644 --- a/apps/ui/public/locales/en.json +++ b/apps/ui/public/locales/en.json @@ -1,16 +1,27 @@ { "aborted": "Aborted", + "add_list": "Add List", "add_locale": "Add Locale", "add_template": "Add Template", "add_translation": "Add Translation", "archive": "Archive", + "back": "Back", "bcc": "BCC", + "blast": "Blast", "body": "Body", "bounced": "Bounced", "campaign_alert_pending": "This campaign has not been sent yet! Once the campaign is live or scheduled this tab will show the progress and results.", "campaign_alert_scheduled": "This campaign is pending delivery. It will begin to roll out at", "campaign_delivery_trigger_description": "Delivery for trigger campaigns is activated via API or journey action. An example request of how to trigger a send via API is available below.", + "campaign_form_channel_description": "This campaign is being sent over the {{channel}} channel. Set the subscription group this message will be associated to.", + "campaign_form_channel_instruction": "Setup the channel this campaign will go out on. The medium is the type of message, provider the sender that will process the message and subscription group the unsubscribe group associated to the campaign.", + "campaign_form_lists": "Select what lists to send this campaign to and what user lists you want to exclude from getting the campaign.", + "campaign_form_select_list": "Select one or more lists using the button above.", + "campaign_form_type": "Should a campaign be sent as a blast to a list of users or triggered individually via API.", + "campaign_list_generating": "This list is still generating. Sending before it has completed could result in this campaign not sending to all users who will enter the list. Are you sure you want to continue?", + "campaign_name": "Campaign Name", "campaigns": "Campaigns", + "cancel": "Cancel", "cc": "CC", "channel": "Channel", "click_rate": "Click Rate", @@ -51,7 +62,9 @@ "finished": "Finished", "from_email": "From Email", "from_name": "From Name", + "get_started": "Get Started", "headers": "Headers", + "images": "Images", "in_timezone": "In Timezone", "journey": "Journey", "journeys": "Journeys", @@ -61,24 +74,38 @@ "lists": "Lists", "load_user": "Load User", "locale": "Locale", + "login_basic_instructions": "Enter your email and password to continue.", + "login_email_available_methods": "What is your email address?", + "login_email_confirmation": "An email has been sent to the address you indicated with a link to continue.", + "login_email_instructions": "Next, please enter your email to receive an authentication link.", + "login_method_not_available": "This login method is not available for this email address or an account with this email address does not exist.", + "login_select_method": "Select an authentication method below to continue.", + "medium": "Medium", "message": "Message", "method": "Method", "name": "Name", + "no_providers": "No Providers", "no_template_alert_body": "There are no templates yet for this campaign. Add a locale above or use the button below to get started.", + "onboarding_installation_success": "Looks like everything is working with the installation! Now let's get you setup and ready to run some campaigns!", + "onboarding_project_setup_description": "At Parcelvoy, projects represent a single workspace for sending messages. You can use them for creating staging environments, isolating different clients, etc. Let's create your first one to get you started!", + "onboarding_project_setup_title": "Project Setup", "open_rate": "Open Rate", "opened_at": "Opened At", "options": "Options", "pending": "Pending", "phone": "Phone", + "plain_text": "Plain Text", "preheader": "Preheader", "preview": "Preview", "project": "Project", "provider": "Provider", "push": "Push", "raw_json": "Raw JSON", + "remove": "Remove", "remove_locale_warning": "Are you sure you want to delete this locale? The template cannot be recovered.", "reply_to": "Reply To", "running": "Running", + "save": "Save", "scheduled": "Scheduled", "search": "Search", "search_users": "Search by ID, email or phone", @@ -90,18 +117,23 @@ "setup": "Setup", "setup_integration": "Setup Integration", "setup_integration_description": "Campaigns depend on message integrations to be able to send. Configure an integration to start sending messages!", + "setup_integration_no_providers": "There are no providers configured for this channel. Please add a provider to continue.", "sign_out": "Sign Out", "state": "State", "status": "Status", "subject": "Subject", "subscription_group": "Subscription Group", "subscriptions": "Subscriptions", + "tags": "Tags", + "template_save": "Save Template", + "template_saved": "Template saved!", "test_webhook": "Test Webhook", "text": "Text", "throttled": "Throttled", "title": "Title", "translations": "Translations", "translations_description": "Manage the translations your campaign supports and will send to.", + "trigger": "Trigger", "type": "Type", "updated_at": "Updated At", "usage": "Usage", @@ -110,5 +142,6 @@ "users_count": "Users Count", "view_all": "View All", "visual": "Visual", - "webhook": "Webhook" + "webhook": "Webhook", + "welcome": "Welcome!" } \ No newline at end of file diff --git a/apps/ui/public/locales/es.json b/apps/ui/public/locales/es.json index cec92fcd..cca85416 100644 --- a/apps/ui/public/locales/es.json +++ b/apps/ui/public/locales/es.json @@ -1,16 +1,27 @@ { "aborted": "Cancellado", + "add_list": "Añadir lista", "add_locale": "Añadir localidad", "add_template": "Añadir Plantilla", "add_translation": "Añadir traducción", "archive": "Archivar", + "back": "Atrás", "bcc": "BCC", + "blast": "Volumen (Lista)", "body": "Cuerpo", "bounced": "Rebotado", "campaign_alert_pending": "¡Esta campaña aún no ha sido enviada! Una vez que la campaña esté activa o programada, esta pestaña mostrará el progreso y los resultados.", "campaign_alert_scheduled": "Esta campaña está pendiente de entrega. Comenzará a desplegarse a", "campaign_delivery_trigger_description": "", + "campaign_form_channel_description": "Esta campaña se envía a través del canal {{channel}}. Establezca el grupo de suscripción al que se asociará este mensaje.", + "campaign_form_channel_instruction": "Configure el canal por el que saldrá esta campaña. El medio es el tipo de mensaje, el proveedor el remitente que procesará el mensaje y el grupo de suscripción el grupo de baja asociado a la campaña.", + "campaign_form_lists": "Seleccione a qué listas desea enviar esta campaña y qué listas de usuarios desea excluir para que no reciban la campaña.", + "campaign_form_select_list": "Seleccione una o más listas usando el botón de arriba.", + "campaign_form_type": "¿Debería enviarse una campaña a una lista de usuarios o activarse individualmente a través de API?", + "campaign_list_generating": "Esta lista aún se está generando. Enviar antes de que se haya completado podría provocar que esta campaña no se envíe a todos los usuarios que entrarán en la lista. ¿Está seguro de que desea continuar?", + "campaign_name": "Nombre de la campaña", "campaigns": "Campañas", + "cancel": "Cancelar", "cc": "CC", "channel": "Canal", "click_rate": "Ratio de clics", @@ -51,7 +62,9 @@ "finished": "Completo", "from_email": "Del Correo Electrónico", "from_name": "Del Nombre", + "get_started": "Comenzar", "headers": "Encabezados", + "images": "Imágenes", "in_timezone": "En zona horaria", "journey": "Camino", "journeys": "Caminos", @@ -61,24 +74,38 @@ "lists": "Listas", "load_user": "Prueba Usario", "locale": "Localidad", + "login_basic_instructions": "Introduzca su correo electrónico y contraseña para continuar.", + "login_email_available_methods": "¿Cuál es tu dirección de correo electrónico?", + "login_email_confirmation": "Un email a sido enviado a la dirección indicada con un enlace para continuar.", + "login_email_instructions": "A continuación, introduzca su correo electrónico para recibir un enlace de autenticación.", + "login_method_not_available": "Este método de inicio de sesión no está disponible para esta dirección de correo electrónico o no existe una cuenta con esta dirección de correo electrónico.", + "login_select_method": "Seleccione un método de autenticación a continuación para continuar.", + "medium": "Medio", "message": "Mensaje", "method": "Método", "name": "Nombre", + "no_providers": "Sin proveedores", "no_template_alert_body": "", + "onboarding_installation_success": "¡Parece que todo está funcionando con la instalación! ¡Ahora vamos a configurarlo y prepararlo para ejecutar algunas campañas!", + "onboarding_project_setup_description": "En Parcelvoy, los proyectos representan un espacio de trabajo único para el envío de mensajes. Puede utilizarlos para crear entornos de ensayo, aislar diferentes clientes, etc. ¡Vamos a crear tu primero para empezar!", + "onboarding_project_setup_title": "Configuración del proyecto", "open_rate": "Ratio de apertura", "opened_at": "Abierto En", "options": "Opciones", "pending": "Pendiente", "phone": "Teléfono", + "plain_text": "Texto sin formato", "preheader": "Preencabezado", "preview": "Previsualizar", "project": "Proyecto", "provider": "Proveedor", "push": "Notificación", "raw_json": "JSON Sin Procesar", + "remove": "Eliminar", "remove_locale_warning": "¿Está seguro de que desea eliminar esta configuración regional? La plantilla no se puede recuperar.", "reply_to": "Responder a", "running": "Corriente", + "save": "Guardar", "scheduled": "Programado", "search": "Buscar", "search_users": "Búsqueda por ID, correo electrónico o teléfono", @@ -90,18 +117,23 @@ "setup": "Configurar", "setup_integration": "Configura Integración", "setup_integration_description": "Las campañas dependen de las integraciones de mensajes para poder enviar. ¡Configura una integración para empezar a enviar mensajes!", + "setup_integration_no_providers": "No hay proveedores configurados para este canal. Por favor, añada un proveedor para continuar.", "sign_out": "Cerrar sesión", "state": "Estado", "status": "Estado", "subject": "Sujeto", "subscription_group": "Grupo de Suscripción", "subscriptions": "Suscripciones", + "tags": "Etiquetas", + "template_save": "Guardar plantilla", + "template_saved": "¡Plantilla guardada!", "test_webhook": "Enviar Webhook de Prueba", "text": "Texto", "throttled": "Envío Estrangulado", "title": "Título", "translations": "Traducciones", "translations_description": "Administre las traducciones que su campaña admite y a las que enviará.", + "trigger": "Individual (API)", "type": "Tipo", "updated_at": "Actualizado En", "usage": "Uso", @@ -110,5 +142,6 @@ "users_count": "Número de usuarios", "view_all": "Ver Todo", "visual": "Visual", - "webhook": "Webhook" + "webhook": "Webhook", + "welcome": "¡Bienvenido!" } \ No newline at end of file diff --git a/apps/ui/src/views/auth/Login.tsx b/apps/ui/src/views/auth/Login.tsx index 28fddd15..a952909f 100644 --- a/apps/ui/src/views/auth/Login.tsx +++ b/apps/ui/src/views/auth/Login.tsx @@ -9,6 +9,7 @@ import { AuthMethod } from '../../types' import FormWrapper from '../../ui/form/FormWrapper' import TextInput from '../../ui/form/TextInput' import { Alert } from '../../ui' +import { useTranslation } from 'react-i18next' interface LoginParams { email: string @@ -16,6 +17,7 @@ interface LoginParams { } export default function Login() { + const { t } = useTranslation() const [searchParams] = useSearchParams() const [methods, setMethods] = useState() const [method, setMethod] = useState() @@ -39,7 +41,7 @@ export default function Login() { await api.auth.basicAuth(email, password, searchParams.get('r') ?? '/') } else if (method === 'email') { await api.auth.emailAuth(email, searchParams.get('r') ?? '/') - setMessage('An email has been sent to the address you indicated with a link to continue.') + setMessage(t('login_email_confirmation')) } else { await checkEmail(method, email) handleRedirect(method, email) @@ -49,7 +51,7 @@ export default function Login() { const checkEmail = async (method: string, email: string) => { const isAllowed = await api.auth.check(method, email) - if (!isAllowed) throw new Error('This login method is not available for this email address or an account with this email address does not exist.') + if (!isAllowed) throw new Error(t('login_method_not_available')) return isAllowed } @@ -66,8 +68,8 @@ export default function Login() { {!method && (
-

Welcome!

-

Select an authentication method below to continue.

+

{t('welcome')}

+

{t('login_select_method')}

{methods?.map((method) => ( @@ -77,8 +79,8 @@ export default function Login() { )} {method && method.driver === 'basic' && (
-

Welcome!

-

Enter your email and password to continue.

+

{t('welcome')}

+

{t('login_basic_instructions')}

onSubmit={handleLogin(method.driver)}> {form => <> @@ -86,34 +88,34 @@ export default function Login() { } - +
)} {method && method.driver === 'email' && (
-

Welcome!

+

{t('welcome')}

{message ? <> {message} - + : <> -

Next, please enter your email to receive an authentication link.

+

{t('login_email_instructions')}

onSubmit={handleLogin(method.driver)}> {form => <> } - + }
)} {method && !['basic', 'email'].includes(method.driver) && (
-

Auth

-

What is your email address?

+

{t('welcome')}

+

{t('login_email_available_methods')}

onSubmit={handleLogin(method.driver)} submitLabel={method.name}> @@ -121,7 +123,7 @@ export default function Login() { } - +
)}
diff --git a/apps/ui/src/views/auth/OnboardingProject.tsx b/apps/ui/src/views/auth/OnboardingProject.tsx index e97db77f..a426551f 100644 --- a/apps/ui/src/views/auth/OnboardingProject.tsx +++ b/apps/ui/src/views/auth/OnboardingProject.tsx @@ -1,21 +1,15 @@ import { useNavigate } from 'react-router-dom' import ProjectForm from '../project/ProjectForm' +import { useTranslation } from 'react-i18next' export default function OnboardingProject() { + const { t } = useTranslation() const navigate = useNavigate() return (
-

Project Setup

-

- { - `At Parcelvoy, projects represent a single workspace for sending messages. - You can use them for creating staging environments, isolating different clients, etc. - Let's create your first one to get you started!` - } -

- navigate('/projects/' + id)} - /> +

{t('onboarding_project_setup_title')}

+

{t('onboarding_project_setup_description')}

+ navigate('/projects/' + id)} />
) } diff --git a/apps/ui/src/views/auth/OnboardingStart.tsx b/apps/ui/src/views/auth/OnboardingStart.tsx index 9f460a40..126ffba0 100644 --- a/apps/ui/src/views/auth/OnboardingStart.tsx +++ b/apps/ui/src/views/auth/OnboardingStart.tsx @@ -1,11 +1,13 @@ import { LinkButton } from '../../ui/Button' +import { useTranslation } from 'react-i18next' export default function Onboarding() { + const { t } = useTranslation() return (
-

Welcome!

-

Looks like everything is working with the installation! Now let's get you setup and ready to run some campaigns!

- Get Started +

{t('welcome')}

+

{t('onboarding_installation_success')}

+ {t('get_started')}
) } diff --git a/apps/ui/src/views/campaign/CampaignForm.tsx b/apps/ui/src/views/campaign/CampaignForm.tsx index b2db4a53..01ee4f11 100644 --- a/apps/ui/src/views/campaign/CampaignForm.tsx +++ b/apps/ui/src/views/campaign/CampaignForm.tsx @@ -17,6 +17,7 @@ import Modal from '../../ui/Modal' import Button, { LinkButton } from '../../ui/Button' import { DataTable } from '../../ui/DataTable' import { Alert } from '../../ui' +import { useTranslation } from 'react-i18next' interface CampaignEditParams { campaign?: Campaign @@ -37,6 +38,7 @@ const ListSelection = ({ value, required, }: ListSelectionProps) => { + const { t } = useTranslation() const [project] = useContext(ProjectContext) const [isOpen, setIsOpen] = useState(false) const [lists, setLists] = useState(value ?? []) @@ -44,7 +46,7 @@ const ListSelection = ({ const handlePickList = (list: List) => { if (list.state !== 'ready') { - if (!confirm('This list is still generating. Sending before it has completed could result in this campaign not sending to all users who will enter the list. Are you sure you want to continue?')) return + if (!confirm(t('campaign_list_generating'))) return } const newLists = [...lists.filter(item => item.id !== list.id), list] @@ -77,35 +79,38 @@ const ListSelection = ({ } actions={ + onClick={() => setIsOpen(true)}>{t('add_list')} } /> item.id} columns={[ - { key: 'name' }, + { key: 'name', title: t('title') }, { key: 'type', + title: t('type'), cell: ({ item: { type } }) => snakeToTitle(type), }, { key: 'users_count', + title: t('users_count'), cell: ({ item }) => item.users_count?.toLocaleString(), }, - { key: 'updated_at' }, + { key: 'updated_at', title: t('updated_at') }, { key: 'options', + title: t('options'), cell: ({ item }) => ( ), }, ]} - emptyMessage="Select one or more lists using the button above." + emptyMessage={t('campaign_form_select_list')} /> }) => { + const { t } = useTranslation() const channels = [...new Set(subscriptions.map(item => item.channel))].map(item => ({ key: item, label: snakeToTitle(item), @@ -133,7 +139,7 @@ const ChannelSelection = ({ subscriptions, form }: { @@ -141,6 +147,7 @@ const ChannelSelection = ({ subscriptions, form }: { } const SubscriptionSelection = ({ subscriptions, form }: { subscriptions: Subscription[], form: UseFormReturn }) => { + const { t } = useTranslation() const channel = useWatch({ control: form.control, name: 'channel', @@ -158,7 +165,7 @@ const SubscriptionSelection = ({ subscriptions, form }: { subscriptions: Subscri x.id} @@ -167,6 +174,7 @@ const SubscriptionSelection = ({ subscriptions, form }: { subscriptions: Subscri } const ProviderSelection = ({ providers, form }: { providers: Provider[], form: UseFormReturn }) => { + const { t } = useTranslation() const channel = useWatch({ control: form.control, name: 'channel', @@ -184,7 +192,7 @@ const ProviderSelection = ({ providers, form }: { providers: Provider[], form: U x.id} @@ -193,35 +201,41 @@ const ProviderSelection = ({ providers, form }: { providers: Provider[], form: U } const TypeSelection = ({ campaign, form }: { campaign?: Campaign, form: UseFormReturn }) => { + const { t } = useTranslation() const type = useWatch({ control: form.control, name: 'type', }) + const options = [{ + key: 'blast', + label: t('blast'), + }, { + key: 'trigger', + label: t('trigger'), + }] return <> ({ key: item, label: snakeToTitle(item) }))} + subtitle={t('campaign_form_type')} + label={t('type')} + options={options} required /> { type !== 'trigger' && ( <> - - Select what lists to send this campaign to and what user lists you want to exclude from getting the campaign. - + {t('campaign_form_lists')} ([]) @@ -242,14 +257,12 @@ export function CampaignForm({ campaign, onSave, type }: CampaignEditParams) { const params: SearchParams = { limit: 9999, q: '' } api.subscriptions.search(project.id, params) .then(({ results }) => { - console.log('set subscriptions!') setSubscriptions(results) }) .catch(() => {}) api.providers.all(project.id) .then((results) => { - console.log('set providers!') setProviders(results) }) .catch(() => {}) @@ -276,18 +289,19 @@ export function CampaignForm({ campaign, onSave, type }: CampaignEditParams) { onSubmit={async (item) => await handleSave(item)} defaultValues={campaign ?? { type: type ?? 'blast' }} - submitLabel="Save" + submitLabel={t('save')} > {form => ( <> { !type && @@ -296,9 +310,7 @@ export function CampaignForm({ campaign, onSave, type }: CampaignEditParams) { campaign ? ( <> - - This campaign is being sent over the {campaign.channel} channel. Set the subscription group this message will be associated to. - + {t('campaign_form_channel_description', { channel: campaign.channel })} - - Setup the channel this campaign will go out on. The medium is the type of message, provider the sender that will process the message and subscription group the unsubscribe group associated to the campaign. - + {t('campaign_form_channel_instruction')} : Setup Integration - }>There are no providers configured for this channel. Please add a provider to continue. + {t('setup_integration')} + }>{t('setup_integration_no_providers')} } ) diff --git a/apps/ui/src/views/campaign/editor/EmailEditor.tsx b/apps/ui/src/views/campaign/editor/EmailEditor.tsx index 65fe6c34..9a2b0aad 100644 --- a/apps/ui/src/views/campaign/editor/EmailEditor.tsx +++ b/apps/ui/src/views/campaign/editor/EmailEditor.tsx @@ -11,11 +11,13 @@ import HtmlEditor from './HtmlEditor' import LocaleSelector from '../LocaleSelector' import { toast } from 'react-hot-toast/headless' import { QuestionIcon } from '../../../ui/icons' +import { useTranslation } from 'react-i18next' const VisualEditor = lazy(async () => await import('./VisualEditor')) export default function EmailEditor() { const navigate = useNavigate() + const { t } = useTranslation() const [project] = useContext(ProjectContext) const [campaign, setCampaign] = useContext(CampaignContext) const { templates } = campaign @@ -33,7 +35,7 @@ export default function EmailEditor() { const newCampaign = { ...campaign } newCampaign.templates = templates.map(obj => obj.id === id ? value : obj) setCampaign(newCampaign) - toast.success('Template saved!') + toast.success(t('template_saved')) } finally { setIsSaving(false) } @@ -65,7 +67,7 @@ export default function EmailEditor() { size="small" isLoading={isSaving} onClick={async () => await handleTemplateSave(template)} - >Save Template + >{t('template_save')} )} }