Funciones del correo
Reglas, labels, búsqueda, contactos, citas, offline, atajos e i18n.
En esta página
Referencia de lo que el correo hace hoy. Donde una función no existe o solo existe a medias, se dice expresamente.
Reglas de correo#
- Una regla tiene
name(1–80),enabled,priority(entero −1000..1000; menor número = se evalúa antes),conditions(esquema v2, ver "Motor de condiciones v2"; el formato v1{match, items}o un arreglo simple se sigue leyendo y se guarda como v2), entre 1 y 30 condiciones en grupos de hasta 4 niveles, entre 1 y 12 acciones,stopProcessingy, opcionalmente,labelId(regla de "Asignar automáticamente" de una etiqueta). Máximo 100 reglas por usuario. - Condiciones: árbol de grupos
and/or/not(*ninguna* de las hijas) con hojas tipadas; ver "Motor de condiciones v2". Una regla sin condiciones nunca coincide. - Acciones:
addLabel,removeLabel,moveToLabelFolder {labelId},markRead,star,archive(saltar Entrada),moveToFolder {folder}(inbox|archive|trash|spam),delete(a la papelera: borrar definitivamente no existe como acción),markSpam,snooze {hours}(1–8760),forwardTo {address}(desactivado por defecto: requiereRULES_FORWARD_ENABLED=true, solo a direcciones verificadas del propio usuario, máx. 30 por hora y sin reenviar correos automáticos) ystopProcessing. Sobre una misma etiqueta gana la última acción en orden de prioridad. - Cada regla se evalúa una vez contra el estado inicial del correo: las acciones no disparan otras reglas (sin cadenas ni bucles). Si varias reglas fijan carpeta, gana la de mayor prioridad.
stopProcessing: truecorta el procesamiento. - Regex de usuario: máx. 200 caracteres; se rechazan referencias hacia atrás, lookahead/lookbehind, cuantificadores anidados y alternancias dentro de grupos cuantificados (protección contra ReDoS).
- Cuándo se aplican: al recibir un correo (con cabeceras, adjuntos y contactos reales); con "Aplicar a correos existentes" (páginas de 200 con progreso, vista previa del número de coincidencias y Deshacer por lote; solo
inboxyarchive, nunca reenvía); y en el cron (catch-up de los últimos 2 días, lotes de 200). El cuerpo completo se lee del almacenamiento solo si la regla usabody/bodyHtml. Una regla no saca despamun correo que ya llegó como spam por cabeceras. - Si las tablas
Rule/RuleRunno existen, la lectura devuelve[]y la escritura responde 503 (ejecutanpm run db:ensure). - Se gestionan en Ajustes → Reglas (arrastra o usa los botones para cambiar la prioridad) y, por etiqueta, en Ajustes → Etiquetas → "Asignar automáticamente". Es el mismo motor y la misma tabla.
POST /api/rules
{
"name": "Facturas a Finanzas",
"enabled": true,
"priority": 0,
"conditions": {
"match": "any",
"items": [
{ "field": "from", "op": "contains", "value": "facturacion@" },
{ "field": "subject", "op": "regex", "value": "factura|invoice" },
{ "field": "hasAttachment", "value": true }
]
},
"actions": [ { "type": "addLabel", "labelId": "<id-de-etiqueta>" }, { "type": "markRead" } ],
"stopProcessing": false
}Etiquetas (labels)#
- CRUD completo (
/api/labels,/api/labels/reorder,/api/labels/{id}/move-emails). Hasta 500 por usuario;namees el segmento (máx. 50, sin/, comas ni caracteres de control), único entre hermanas (409 si se repite). Color#RGB/#RRGGBB(por defecto#6366f1). Crear conname: "Trabajo/Proyecto A"crea la jerarquía. aliasSuffixyfilterRegex(campos antiguos) se migran solos a una regla equivalente vinculada (Rule.labelId: condicióntoAliasy regex de asunto/cuerpo) y se vacían; mientras no se migren siguen funcionando.- Importar y exportar: las etiquetas de Gmail con
/(X-Gmail-Labels) crean jerarquía; las carpetas de Outlook, Thunderbird, Maildir y ZIP se importan como etiquetas de comportamientofoldercon su jerarquía; el export escribe la ruta completa enX-Gmail-Labelsy los correos de una etiqueta-carpeta van a su subdirectorio en el ZIP. - Ver "Carpetas y etiquetas" para la jerarquía y el comportamiento
tag/folder. - El Organizer puede proponer o aplicar etiquetas con IA.
Carpetas y etiquetas#
Modelo híbrido Gmail + Outlook. Las carpetas del sistema (inbox, sent, drafts, scheduled, archive, trash, spam) son fijas; las etiquetas se anidan (hasta 5 niveles, Trabajo/Proyecto A) y cada una tiene un comportamiento:
tag(por defecto, estilo Gmail): el correo conserva su carpeta (sigue en Entrada) y gana la etiqueta.folder(estilo Outlook/Hotmail): al asignarla el correo sale de Entrada (folder=archive,previousFolder=inbox) y su ubicación visible es la etiqueta; no aparece en Archivados. Al quitarla, o al volver a Entrada a mano, regresa a Entrada. "Mover a…" y arrastrar a una etiqueta-carpeta hacen exactamente eso (y dejan las otras etiquetas-carpeta: un correo vive en una sola).- Filtrar por una etiqueta padre incluye a sus subetiquetas (
?label=trabajo;&subs=0para excluirlas). Los contadores de los padres acumulan hilos distintos de sus descendientes. - Borrar una etiqueta nunca borra correos: las subetiquetas suben un nivel (o se borran con
?children=delete) y los correos que solo estaban en una carpeta vuelven a Entrada. - Columnas aditivas de
Label:parentId,behavior,sortOrder,icon,showInSidebar,showUnread,fullPath(ruta calculada). Unicidad(userId, parentId, name)con el índiceLabel_user_parent_name_key. Correos:Email.hdrs(cabeceras de la lista blanca, ≤ 4 KB). Reglas:Rule.labelId,matchedCount,lastMatchedAt; tablasRuleBatch/RuleBatchItem(lotes con deshacer). - UI: árbol
role="tree"en la barra lateral y en Ajustes; arrastrar y soltar con alternativa de teclado (flechas, Alt+flechas para reordenar y anidar, F2 renombrar, Supr eliminar, menú contextual).
Carpetas vs etiquetas (barra lateral). Dentro de la sección "Etiquetas y carpetas" hay dos grupos colapsables, con su contador de elementos: *Carpetas* (comportamiento folder) y *Etiquetas* (comportamiento tag). Son el mismo objeto con otro comportamiento, por eso comparten jerarquía, reglas y arrastrar y soltar.
- Carpeta: el correo vive en un solo lugar y sale de Entrada. Etiqueta: marca adicional; un correo puede tener varias y sigue en Entrada. Ejemplo: "Facturas" como carpeta aparta los recibos de Entrada; "Urgente" como etiqueta solo los destaca.
- Regla de agrupación: cada elemento raíz se muestra en el grupo de su propio comportamiento y sus subetiquetas heredan el grupo de su raíz (una carpeta dentro de una etiqueta se ve dentro de esa etiqueta, con icono de carpeta), de modo que el árbol nunca se rompe.
- El botón "+" abre un menú con "Nueva etiqueta" y "Nueva carpeta"; el icono "?" explica la diferencia. Desde el menú "…" de cada elemento, "Convertir en carpeta" / "Convertir en etiqueta" cambia el comportamiento (
PATCH /api/labels/{id}conbehavior; los correos ya etiquetados no se mueven salvomoveExisting: true, disponible en Ajustes → Etiquetas). - Contadores: el número resaltado son los no leídos y, al pasar el cursor, aparece el total (
236/265); el lector de pantalla anuncia "236 sin leer de 265".
Motor de condiciones v2#
Las condiciones se guardan como { "v": 2, "root": Group }. Un grupo es { "type": "group", "op": "and"|"or"|"not", "children": [...] } (not = ninguna de las hijas coincide); una hoja es { "field", "op", "value"|"values", ... }. Máx. 4 niveles y 30 condiciones, valores de hasta 500 caracteres y listas de hasta 100. La validación es estricta (zod) y las regex pasan las mismas defensas anti-ReDoS (200 caracteres, sin referencias hacia atrás ni lookarounds, sin cuantificadores anidados).
Lógica de tres valores: si falta un dato (por ejemplo las cabeceras de un correo anterior a esta función) la condición es *desconocida* y no coincide, tampoco dentro de un not. Cada regla tiene un presupuesto de 75 ms por correo.
| Campo | Operadores / valor |
|---|---|
from fromName fromDomain replyTo | Texto: contains notContains equals notEquals startsWith endsWith regex notRegex in notIn wildcard notWildcard. fromDomain admite subdomains: true |
to toDomain cc bcc toAlias | Igual; se comprueba cada dirección de la lista. toAlias = sufijo de usuario+sufijo@ |
subject body bodyHtml language | Texto (+ containsAny). Opciones caseSensitive y wholeWord. El texto se acota a 20 KB. language: es/en/pt/fr/de/it por heurística de palabras frecuentes |
hasAttachment attachmentName attachmentType attachmentCount attachmentsSize | Booleano / texto (extensión o MIME, p. ej. pdf, image/*) / numérico (bytes) |
size spamScore | Numérico: lt lte eq gte gt (size = adjuntos + cuerpo, aproximado) |
date hour dayOfWeek | before after between (ISO); hora 0–23 y día 0 (domingo)–6 con tz opcional (IANA) |
isReply isForward isThread isCalendarInvite senderInContacts senderIsMe isRead isStarred | Booleano (value) |
hasLabel inFolder | Etiqueta por id, nombre o ruta (un padre incluye a sus descendientes) / carpeta del sistema |
header auth | header: name de la lista blanca (list-id, list-unsubscribe, precedence, auto-submitted, x-mailer, reply-to, authentication-results…) + exists notExists o un operador de texto. auth: mech spf/dkim/dmarc con pass fail none |
POST /api/rules
{
"name": "Facturas de proveedores",
"labelId": "<id-de-la-etiqueta-carpeta-Facturas>",
"conditions": {
"v": 2,
"root": { "type": "group", "op": "and", "children": [
{ "field": "fromDomain", "op": "in", "values": ["empresa.com", "proveedor.es"], "subdomains": true },
{ "field": "subject", "op": "containsAny", "values": ["factura", "invoice"], "wholeWord": true },
{ "field": "attachmentType", "op": "equals", "value": "pdf" },
{ "type": "group", "op": "not", "children": [
{ "field": "header", "name": "auto-submitted", "op": "exists" }
] }
] }
},
"actions": [ { "type": "markRead" } ],
"stopProcessing": false
}- Probar:
POST /api/rules/previewevalúa unas condiciones contra tus últimos 50 o 200 correos (o, conmode: "count", por páginas sobre todo el correo) sin efectos; limitado por ritmo. - Aplicar a existentes:
POST /api/rules/applyconruleIdolabelId(páginas concursor,dryRun,batchId); cada lote guarda el estado previo yPOST /api/rules/batches/{id}/undolo deshace una sola vez sin pisar cambios posteriores. - Plantillas: todo de un dominio, boletines (
List-Unsubscribe), facturas con PDF, de mis contactos y notificaciones automáticas. Cada regla se describe en lenguaje natural ("Si el dominio del remitente es "empresa.com" y el asunto contiene "factura" → etiquetar "Facturas"").
Búsqueda con operadores#
La barra de búsqueda admite operadores y texto libre, todo combinado con AND. Máximo 300 caracteres y 30 términos; los valores admiten comillas.
| Operador | Significado |
|---|---|
label:x | Etiqueta con ese nombre (sin distinguir mayúsculas) |
from:x | El remitente contiene x |
to:x | Destinatarios (to) contienen x |
subject:x | El asunto contiene x |
has:attachment | Tiene adjuntos (único valor válido de has:) |
is:unread, is:read, is:starred | Estado. Pedir unread y read a la vez las anula |
from:ana subject:"informe mensual" has:attachment is:unread label:"Mis facturas" contrato- El texto libre usa búsqueda de texto completo de PostgreSQL (
websearch_to_tsquery, configuraciónsimple) sobre asunto, remitente y extracto (no sobre el cuerpo completo); si falla o no encuentra nada, cae aILIKE. - Con búsqueda, el alcance es global (todas las carpetas salvo papelera y spam). Resultados de 20 en 20.
- No existen los operadores
before:,after:,in:,filename:,cc:nibcc:. Cualquier otroclave:valorse trata como texto libre. Los rangos de fecha, remitente y "con adjuntos" están en el filtro avanzado de la interfaz (since,until,from,hasAttachment).
Redactar y enviar#
- Borradores: autoguardado con retardo de 2 s, guardado al ocultar la pestaña o cerrar (
keepalive), y solo se guardan adjuntos ya subidos. Indicador "Guardando… / Borrador guardado". - Límites de envío: hasta 100 destinatarios (To+Cc+Bcc), asunto ≤ 998 caracteres,
MAX_SENDS_PER_HOUR(200 por defecto) enviados por hora y usuario (429). El hookEMAIL_PRE_SENDpuede bloquear (422) o modificar el contenido. - Adjuntos: hasta 25 por correo; inline (base64) hasta 35 MB en total (413); por subida previa (
/api/upload) hasta 200 MB por archivo. Extensiones peligrosas rechazadas y tipo real validado; antivirus opcional (Seguridad). - Reply/Reply-all/Forward: responder a todos incluye Cc originales (con parsing correcto de nombres con comas y comillas) y reenviar copia los adjuntos con verificación de propiedad en el servidor.
- Programar envío:
scheduledAtse pasa a Resend y el correo queda en la carpeta "Scheduled". No hay código que lo mueva a "Sent" al llegar la hora (no verificado). Cancelar un envío programado existe solo como API (POST /api/emails/[id]/cancel) sin interfaz. - Snooze: solo existe la API (
POST /api/emails/[id]/snooze), sin interfaz. La reactivación es perezosa al listar el correo. - Deshacer envío con retardo: no existe.
- Idempotencia:
POST /api/emailsaceptaIdempotency-Key(8–128 caracteres): un reintento devuelve{duplicate:true}y una petición concurrente responde 425. La interfaz de redacción no lo envía; sí la cola offline y Elixir. - Sellado: envío con cifrado de extremo a extremo, ver Sealer.
Hilos de conversación y compatibilidad con otros clientes#
Lo que enviamos se agrupa y se ve bien en Gmail, Outlook/Hotmail, Apple Mail, Thunderbird, Titan, Zoho y Yahoo; y lo que recibimos de ellos se agrupa y se pliega aquí. Está construido sobre los estándares (RFC 5322 §3.6.4 para Message-ID/In-Reply-To/References, RFC 2369 para List-*, RFC 3676 para la firma -- ) y sobre los patrones documentados de cada cliente; no se ha probado contra los servicios reales, solo contra fixtures sintéticos reproducibles (src/lib/__tests__/helpers/thread-fixtures.ts, una conversación mixta de 13 correos de 12 clientes).
- Modelo (columnas aditivas de
Email, creadas pornpm run db:ensure):rfcMessageId(Message-ID sin<>, dominio en minúsculas),inReplyTo,refs(References normalizadas: la raíz + las 50 más recientes; las pistas de Outlook se guardan al final con el prefijo~) ythreadKey(raíz canónica del hilo:m:<Message-ID>,e:<id>oh:<clave heredada>). Índices(userId, threadKey),(userId, rfcMessageId)y parcial(userId, inReplyTo). Si faltan las columnas todo sigue funcionando con la heurística heredada. - Algoritmo (
src/lib/threading.ts, una sola definición para la lista, el SQL de conteos/orden/filtros/multi-buzón, el lector, la ingesta y la importación): JWZ simplificado con union-find sobre Message-ID/In-Reply-To/References. Ignora ids basura o gigantes (<>,null, > 255 caracteres…), es inmune a ciclos, y ningún hilo pasa de 500 mensajes (una unión que lo superaría se ignora). La clave es función del conjunto de mensajes: llegue en el orden que llegue, el mismo hilo da la misma clave, y el mensaje que une dos hilos fusiona sus claves con unUPDATEen lote. El asunto puede cambiar a mitad de la conversación sin partir el hilo, y dos conversaciones con el mismo asunto no se juntan. - Respaldo sin cabeceras útiles: asunto normalizado + intersección de participantes (sin contarte a ti) en una ventana de 45 días. Solo se aplica a mensajes sin Message-ID ni cabeceras de respuesta (importados de orígenes pobres) y a respuestas cuyo padre no conocemos (p. ej. el proveedor de envío sustituyó nuestro Message-ID). Un mensaje nuevo con su propio Message-ID y sin In-Reply-To es una conversación nueva. Las listas (
List-Id), autorespuestas (Auto-Submitted) yPrecedence: bulknunca se unen por asunto.Thread-Indexde Outlook/Exchange se usa como pista secundaria. - Asuntos multilingües: se ignoran Re, Res, RV, AW, SV, VS, Antw, R, ENC, Fwd, FW, WG, TR, I, Rif, Odp, VB, PD, VL, FS, YNT, 回复, 答复, 回覆, 转发,
Re[2]:y los dos puntos de ancho completo, además de las marcas de invitación de calendario. El mismo texto de regex se usa en JavaScript y en PostgreSQL (un test compara ambos con 220 correos aleatorios, con y sinthreadKey). - Ingesta: el webhook de Resend guarda el
message_idy las cabeceras que vengan en el payload; si el asunto parece una respuesta y faltanIn-Reply-To/References, pide el correo completo a Resend.process-attachmentsvuelve a calcular el hilo con el MIME crudo (raw.eml) y la importación (mail-transfer) usa las cabeceras del archivo. Nunca hace fallar la entrega. - Correos antiguos: un relleno perezoso (
backfillThreadHeaders) leeraw.eml,raw.jsonorawMimeUrl(descarga con las salvaguardas deraw-mime.ts) en lotes desde el cron (/api/cron/run) o a mano conPOST /api/mail/threads/backfill(repetir hastaremaining: 0). Lo que no tiene cabeceras sigue agrupándose por asunto + destinatarios. - Envío (
POST /api/emails): el cliente solo mandainReplyToEmailId(yreplyMode:reply,replyAlloforward); el servidor comprueba que el correo original es tuyo (404 si no) y emiteIn-Reply-To: <id del original>,References: <cadena hasta el original, máx. raíz + 50>y unMessage-IDpropio. Un correo nuevo llevaMessage-IDyX-Entity-Ref-ID(para que Gmail no lo una por asunto a otros envíos parecidos). Los reenvíos no llevan In-Reply-To/References (como Gmail, Apple Mail y Thunderbird) pero se agrupan con el original en tu bandeja. Los envíos de Elixir/campañas no llevan cabeceras de hilo. - Asunto saliente: un solo prefijo, en el idioma del original si es conocido (
Re,Res,AW,SV,VS,Antw,Odp,YNT;Fwd,FW,WG,TR,RV,ENC…) oRe:/Fwd:; nuncaRe: Re:ni idiomas mezclados. Si quitas el prefijo a propósito, se respeta. - Qué se puede garantizar con Resend:
In-Reply-ToyReferencesviajan enheaders(el SDK envía el objeto tal cual a la API) y el payload se verifica con el Resend simulado. ElMessage-IDpropio también se envía enheaders; si el proveedor lo rechaza se reintenta una vez sin él, y si lo sustituye por otro, las respuestas que citen el id real se agrupan por el respaldo (asunto + participantes, 45 días). El SDK instalado no expone el Message-ID final de un envío saliente (no verificado contra el servicio real), así que no hay tarea que lo recupere. - Cita al responder (
src/lib/reply-builder.ts):div.gmail_quote > div.gmail_attr+blockquote.gmail_quotecon el estilo de Gmail (los pliegan Gmail, Apple Mail, Thunderbird, Titan, Zoho y Yahoo), atribución en tu idioma con fecha larga y zona, nombre escapado, original saneado (sin scripts, estilos externos, controles ni píxeles de seguimiento), tope de 100 KB con marcador[...], y versión texto con>por nivel. Al reenviar: bloque estilo Gmail o estilo Outlook (De/Enviado el/Para/Asunto) yReenviar como adjunto (.eml). Las imágenes incrustadas (data:) salen como partes inline conContent-ID, porque Gmail y Outlook no muestrandata:. - Historial recibido (
quoted-html.ts,quoted-text.ts): pliega Gmail, Outlook.com/Hotmail (#divRplyFwdMsg,#appendonsend), Outlook de escritorio (bloqueDe:/Enviado el:/Para:/Asunto:y traducciones), Apple Mail/iCloud, Thunderbird, Yahoo, Proton, Zoho, Titan/Flock (detección genérica atribución + cita final), Samsung, separadores-----Original Message-----, y texto plano con>anidado y atribuciones en unos 17 idiomas. Nunca deja un mensaje sin contenido visible. En la conversación cada mensaje muestra solo lo nuevo; "ocultar duplicados" (activado por defecto) pliega el historial que ya está en el hilo sin perder nada. - Límites reales: las clases de Spark, Superhuman, Front y HubSpot no se han podido verificar sin correos reales (se cubren con la detección genérica); un borrador de respuesta reabierto pierde el vínculo con el original (
inReplyToEmailIdno se guarda enDraft) y se apoya en el respaldo; el Message-ID de un envío lo fija Resend/SES y puede diferir del que enviamos.
Contactos#
- Página
/contactscon búsqueda en el servidor, "cargar más" y edición/borrado. Origenlocalogoogle(la sincronización con Google trae contactos consource=google; los locales conservan sus datos). - API
/api/contacts(límite 1000,q≤ 100; cabecerasX-Total-CountyX-Has-More). Crear un contacto existente da 409CONTACT_EXISTS. - Sugerencias (
/api/contacts/suggestions?q=, máx. 8): mezcla contactos y direcciones de los últimos 250 correos; las usan los campos Para/Cc/Cco. - Importar/exportar CSV o vCard no existe en la interfaz.
Calendario y citas#
- Calendario (
/calendar): vistas mes, semana y día; calendarios por defecto (el propio, "invitaciones compartidas" y festivos de solo lectura); eventos con asistentes que reciben invitación por correo (.ics) y aviso.icsde cancelación al borrar. No hay recurrencia (RRULE) al crear eventos. - RSVP a invitaciones recibidas (aceptar / provisional / rechazar) responde con
.ics. Disponibilidad de compañeros del mismo dominio de correo (/api/calendar/domain-availability). - Recordatorios push de eventos que empiezan en los próximos 15 min (cron, cada 5 min como máximo; se desactivan con
calendarNotificationsEnabled=false). - Google:
POST /api/google/synces unidireccional (Google → local): calendarios, eventos (−30 a +180 días; los cancelados se borran) y contactos, con límites de páginas y una sola sincronización a la vez (409SYNC_IN_PROGRESS). Meet en eventos vía la API de Meet con el token del usuario. - Citas (
/appointments, reserva pública/book/<id>): agendas con duración (15–120 min en la UI), zona horaria, disponibilidad semanal (por defecto lun–vie 09:00–17:00) y videollamada Meet o Zoom. Los huecos respetan la zona horaria y descartan lo ocupado (eventos y reservas). Reserva con límites (10/h por IP y 3/h por correo), bloqueo transaccional anti doble reserva (409) y correo de confirmación con.ics. Cancelación con enlace firmado que caduca al empezar la cita.
Offline y PWA#
- Service worker (
sw.js): cachea solo recursos estáticos (/_next/static/*, iconos y/offline.html; máx. 80 entradas). Nunca cachea/api/*, peticiones conAuthorizationni páginas HTML. Sin red, las navegaciones muestran/offline.html. - Cola offline: acciones por lote de la lista (
/api/emails/batchy/api/drafts/batch) se guardan (máx. 200, hasta 8 intentos con backoff 2 s–5 min) y se reenvían conIdempotency-Keyy el token de la cuenta que las originó. Enviar un correo sin red no se encola. - Caché de lectura: IndexedDB por usuario. Tiempo real:
/api/sse(máx. 3 conexiones por usuario, vida 50 s con reconexión porLast-Event-ID). - PWA:
manifestdinámico con nombre, logo y colores del dominio (display: standalone); avisos de instalación y de notificaciones una sola vez. - Push web (VAPID): con
NEXT_PUBLIC_VAPID_PUBLIC_KEYyVAPID_PRIVATE_KEY, o pares generados y guardados en la tablapush_vapid_config. Se notifica al recibir correo y en recordatorios.
Atajos de teclado#
Un único listener global, activo en la lista de correo. Se ignora si hay Ctrl/Meta/Alt, si el foco está en un campo de texto, si hay un diálogo abierto o durante una composición IME.
| Tecla | Acción |
|---|---|
c | Redactar |
/ | Enfocar el buscador (también en /docs) |
j / k | Siguiente / anterior |
Enter / o | Abrir el correo enfocado |
r | Responder |
x | Alternar selección |
e | Archivar |
#, Delete, Backspace | Mover a la papelera |
Escape | Cierra selector de etiquetas, luego limpia selección, luego cierra el correo, luego quita el foco |
Ctrl/Cmd+Enter | Enviar (en el editor de redacción) |
No existen secuencias tipo g i, ni u, s, l, ?, ni ayuda de atajos, ni atajos de calendario o contactos.
Idiomas (i18n)#
- Idiomas: español (por defecto) e inglés. Resolución: cookie
bloomx-lang→ cabeceraAccept-Language→ español.<html lang>se actualiza sin recargar; el usuario cambia el idioma en Ajustes → Apariencia (y en el encabezado de /docs). - Cliente:
const { t } = useI18n(). Servidor:getServerTranslator(). Clavest("seccion.clave", {param}); si falta, se busca en español, luego inglés y, por último, se muestra la clave. - Añadir una clave: en
src/lib/i18n/messages/es.ts(define el tipo) yen.ts(tipado: exige las mismas claves). Un test comprueba las claves usadas en las pantallas migradas. - Añadir un idioma: ampliar
LOCALESyLOCALE_LABELSencore.ts, crearmessages/<xx>.tsy registrarlo enindex.ts; para la landing, ampliar ademásLANDING_LOCALES(espejo idéntico en el backend) ylanding-messages.ts. - La landing de cada empresa tiene sus textos por idioma (Landing y login).