diff --git a/apps/app/gt.config.json b/apps/app/gt.config.json new file mode 100644 index 000000000..c18825af4 --- /dev/null +++ b/apps/app/gt.config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://assets.gtx.dev/config-schema.json", + "defaultLocale": "en", + "framework": "next-app", + "files": { + "gt": { + "output": "public/_gt/[locale].json" + } + }, + "locales": ["es"], + "_versionId": "c755ac63434ef4ae61355e3c4f6891f5452696a69eeacc71ef29580bd242c5c4" +} diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index 70247ad33..d555ca057 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -1,3 +1,4 @@ +import { withGTConfig } from 'gt-next/config'; import type { NextConfig } from 'next'; import './src/env.mjs'; @@ -51,4 +52,4 @@ const config: NextConfig = { }, }; -export default config; +export default withGTConfig(config, {}); diff --git a/apps/app/package.json b/apps/app/package.json index c87ecf70d..9ddef4d23 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -60,6 +60,7 @@ "dub": "^0.63.6", "framer-motion": "^12.18.1", "geist": "^1.3.1", + "gt-next": "^6.0.11", "lucide-react": "^0.534.0", "motion": "^12.9.2", "next": "15.4.2-canary.16", @@ -109,6 +110,7 @@ "eslint-config-next": "15.4.2-canary.16", "fleetctl": "^4.68.1", "glob": "^11.0.3", + "gtx-cli": "^2.0.22", "jsdom": "^26.1.0", "postcss": "^8.5.4", "prisma": "^6.13.0", @@ -132,7 +134,7 @@ "private": true, "scripts": { "analyze-locale-usage": "bunx tsx src/locales/analyze-locale-usage.ts", - "build": "next build", + "build": "bun run translate && next build", "db:generate": "bun run db:getschema && prisma generate", "db:getschema": "cp ../../node_modules/@trycompai/db/dist/schema.prisma prisma/schema.prisma", "deploy:trigger-prod": "npx trigger.dev@latest deploy", @@ -153,6 +155,7 @@ "test:ui": "vitest --ui", "test:watch": "vitest --watch", "trigger:dev": "npx trigger.dev@latest dev", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "translate": "locadex translate" } } diff --git a/apps/app/public/_gt/es.json b/apps/app/public/_gt/es.json new file mode 100644 index 000000000..a1f75267c --- /dev/null +++ b/apps/app/public/_gt/es.json @@ -0,0 +1,3049 @@ +{ + "740c728118e75062": "No autorizado", + "975483a69922af85": "Organización no encontrada", + "dec9bde15f2f8850": "Error al cambiar la organización", + "3fc6a8884ced0400": "No autorizado", + "4d5ecbb9f076fe6e": "Se produjo un error inesperado.", + "e728130d0e1f972d": "Demasiadas solicitudes", + "61d19c2f2b329848": "Miembro no encontrado", + "d3ef162509d76043": "Se requiere el ID de la organización", + "901feeda33e0d06a": "No tienes acceso a esta organización", + "fe8ed6127f1d8a28": "Intentar de nuevo", + "661f590ac8bf4c12": "Contáctanos", + "a3549d339504d412": [ + { + "i": 1, + "c": "Algo salió mal" + }, + { + "i": 2, + "c": [ + "Ha ocurrido un error inesperado. Por favor, inténtelo de nuevo", + { + "i": 3 + }, + " o contacte al soporte técnico si el problema persiste." + ] + } + ], + "35a1feb2130a712d": "Comp AI | Automatiza el cumplimiento de SOC 2, ISO 27001 y GDPR con IA.", + "bb3d124093904892": "Automatiza el cumplimiento de SOC 2, ISO 27001 y GDPR con IA.", + "9504f1b4596c3aa4": [ + { + "i": 1, + "c": "404 - Página no encontrada" + }, + { + "i": 2, + "c": "La página que está buscando no existe." + }, + { + "i": 3, + "c": "Regresar al panel de control" + } + ], + "3d881add2571492c": { + "i": 1, + "c": "Acceso No Autorizado" + }, + "fae26e6024bab53c": { + "i": 1, + "c": "No tienes permisos para acceder a este recurso. Por favor, contacta a tu administrador si crees que esto es un error." + }, + "43db39dd2edbe8d3": "Volver al Inicio", + "1ea978ddd129c39d": { + "i": 1, + "c": "Garantía de devolución del 100% del dinero • Cancela en cualquier momento" + }, + "b41159357b2fc542": "Usuario Desconocido", + "f86d1b3defd75cea": "Ninguno", + "b98a45755fc28047": "(Tú)", + "136d5c823f7a2b04": { + "i": 1, + "c": "Asignado" + }, + "921823d2710f0cea": "Analizando su stack tecnológico", + "e5ec62b46bf0162c": "AWS, GitHub, Stripe detectados", + "0ef15ba8f1c13bfd": "Investigando el cumplimiento del proveedor", + "a759ca99afbd59cb": "Verificando certificaciones de SOC 2 y seguridad", + "1730109fd84ea55b": "Redactando políticas de seguridad", + "75517b6b1237c41b": "Basado en su infraestructura", + "af7de767352d671d": "Identificando riesgos de cumplimiento", + "f57a749d9525e3d2": "Escaneando brechas y vulnerabilidades", + "7797799c48d1db67": "Configurando monitoreo", + "9c499c51fcbcdc68": "Seguimiento continuo de cumplimiento", + "b88e702dbaef0af4": "{completedCount} de {totalTasks} tareas completadas • Tiempo estimado restante: {timeRemaining}", + "be72aef821e2e6ed": "6-7 min", + "a6cdceda407c3317": "5-6 min", + "3a284a2c87fc3d10": "4-5 min", + "e0feb46352dfbeed": "3-4 min", + "cd1a33e3f01bcb43": "2-3 min", + "4b6c8714969fd747": "1-2 min", + "8e51c7064dacfb04": "Casi terminado...", + "b476049077f53672": "{progress}% completado{finalizingText}", + "3cce07a47aaecd1e": " - Finalizando...", + "b28df67f03ec4491": { + "i": 1, + "c": "La IA está construyendo tu programa de cumplimiento" + }, + "df758aed564578d6": { + "i": 1, + "c": "Este proceso generalmente toma de 2 a 7 minutos en completarse" + }, + "d9922fa69558da06": { + "i": 1, + "c": "Estamos analizando exhaustivamente su infraestructura para crear políticas precisas y personalizadas" + }, + "586834ef482b8cb6": { + "i": 1, + "c": "Progreso del trabajo en segundo plano" + }, + "0ecee0db45b29cb8": "Redactando Política de Seguridad de la Información", + "41eb5d2e0ac21481": "Personalizando según su infraestructura AWS y controles de seguridad", + "705ad1e9215891e8": "Investigando el Cumplimiento de Stripe", + "192f59e06cb05a89": "Analizando el cumplimiento de PCI DSS y las certificaciones de seguridad de pagos", + "43c0fc0b0765c229": "Evaluando Riesgos de Privacidad de Datos", + "1f4cda4f455396dc": "Mapeo de flujos de datos personales en sus sistemas", + "73c91b7f8448fee2": "Redactando Política de Control de Acceso", + "4c17cf0d2d77e685": "Incorporando su SSO de Okta y permisos basados en roles", + "76eadd3311d0f323": "Implementación de Controles de Cifrado", + "f387bc66ce7f1d67": "Configuración de estándares de TLS, datos en reposo y gestión de claves", + "c79f9d843b64606a": "Auditoría de Seguridad de GitHub", + "24b539d6c46ee952": "Revisando protección de ramas, controles de acceso y registros de auditoría", + "0e767dec0ac47b2e": "Creando Plan de Respuesta a Incidentes", + "b9b4315727ce98ac": "Creación de manuales de procedimientos para eventos de seguridad y violaciones de datos", + "6734e2ac39097884": "Recopilando Evidencia de AWS", + "8078cc4471f1190f": "Recopilando registros de CloudTrail, políticas de IAM y configuraciones de seguridad", + "be490db0e89674ea": "Evaluación de Riesgos de Terceros", + "3e2777d759c3b3ea": "Evaluando la postura de seguridad del proveedor y las brechas de cumplimiento", + "c48f9926af7bae34": "Redactando Política de Retención de Datos", + "7165542ad767c873": "Alineándose con los requisitos de GDPR y las necesidades empresariales", + "b2a0632a3aec87fb": "Monitoreo de la Postura de Seguridad", + "6c6cc1876cef4838": "Verificaciones continuas de cumplimiento en toda la infraestructura en la nube", + "ee2c96450c3ab873": "Construyendo Programa de Gestión de Proveedores", + "14f1e7c9da8d1177": "Estableciendo ciclos de revisión y flujos de trabajo de evaluación de riesgos", + "5809ba843c16e377": { + "i": 1, + "c": "Nuestra IA te está preparando para la auditoría" + }, + "8427bf4f149f5a5b": { + "i": 1, + "c": "Hemos comenzado a redactar políticas personalizadas, investigar el cumplimiento de proveedores y evaluar riesgos potenciales para prepararte para la auditoría." + }, + "ed08a01e762c7765": { + "i": 1, + "c": "Selecciona un plan para obtener acceso a tu programa de cumplimiento personalizado." + }, + "2769ade1e5eccf30": "Nuevo", + "25f457f910ba96b7": "Recomendado", + "7cf477b54cbf9769": { + "i": 1, + "c": "Más información" + }, + "f9e880ecfe39b966": "Beta", + "9d0c85d70bfa0721": { + "i": 1, + "c": "Algo salió mal" + }, + "9d9ec53c2f6ea64e": { + "i": 1, + "c": "Intentar de nuevo" + }, + "bcb78c262877e19c": "¡Gracias por sus comentarios!", + "f782f382eda43e85": "Error al enviar comentarios - ¿intentar de nuevo?", + "3526439d22b4696f": "Ideas para mejorar esta página o problemas que esté experimentando.", + "64f73192f64818f0": "Enviar Comentarios", + "3fdad70790a340de": { + "i": 1, + "c": "Comentarios" + }, + "ccde12c7d155ce35": { + "i": 1, + "c": [ + { + "i": 2, + "c": "¡Gracias por sus comentarios!" + }, + { + "i": 3, + "c": "Nos pondremos en contacto con usted lo antes posible", + "t": "p" + } + ] + }, + "f9d36d83a18e0bd5": "No se puede cargar más de 1 archivo a la vez", + "0d2bebe6b9b7ce80": "No se pueden cargar más archivos que el máximo permitido", + "ae8eb47da1dad13d": "No hay archivos seleccionados", + "9a4dad23ee4c833b": "El archivo {fileName} fue rechazado", + "179ef651bab441c9": "{count} archivos", + "c6e2b2d0996be976": "un archivo", + "98daadfd5e198286": "Subiendo {target}...", + "430ef2be0013bc62": "Archivos cargados", + "60a4205d70aa79b2": "Error al cargar archivos", + "fe6b742b4f127fbe": { + "i": 1, + "c": [ + { + "i": 2, + "c": { + "t": "Upload", + "i": 3 + } + }, + { + "i": 4, + "c": "Suelta los archivos aquí" + } + ] + }, + "c03965bbe59a4080": { + "i": 1, + "c": [ + { + "i": 2, + "c": { + "t": "Upload", + "i": 3 + } + }, + { + "i": 4, + "c": [ + { + "i": 5, + "c": "Suelta archivos aquí o haz clic para elegir archivos de tu dispositivo." + }, + { + "i": 6, + "c": [ + "Los archivos pueden tener hasta ", + { + "i": 7, + "k": "_gt_value_7", + "v": "v" + }, + "." + ], + "t": "p" + } + ], + "t": "div" + } + ] + }, + "5be3259dc52851fc": [ + "v", + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + } + ], + "1edad1fefd7df554": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Continuar con GitHub" + ] + }, + "1e880b9a65472bca": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Continuar con Google" + ] + }, + "f60cac73aa39ed39": "Enlace mágico enviado", + "f4f3595427a6949a": [ + "Revisa tu bandeja de entrada en ", + { + "i": 1, + "c": { + "i": 2, + "k": "_gt_value_2", + "v": "v" + } + }, + " para encontrar un enlace mágico para iniciar sesión." + ], + "71f67c3c3c85d78c": "Usar otro método", + "b242b18b1e0292fd": "Más opciones", + "881626e52856cf98": "Error al enviar el correo electrónico - ¿intentar de nuevo?", + "a78ed173cd68c144": "nombre@ejemplo.com", + "df7ef2aa0b99739a": "Continuar con correo electrónico", + "475b6babba755551": "Marcos de trabajo", + "2f66fab970291832": "Controles", + "52a43163cd17a362": "Políticas", + "fae3639874428042": "Tareas", + "ae844e9e87d50d26": "Personas", + "a57fa374fadbfa45": "Riesgos", + "0a4b18bbce20d8b8": "Proveedores", + "0d3f3a65c1686c1f": "Pruebas en la Nube", + "c37237fc8d425018": "Integraciones", + "45fa271ea0e1ac8e": "Beta", + "83ba12b529bef16e": "Referencias", + "0a75e464d833bca8": "Configuración", + "1a32674abce95535": "Próximamente", + "2b9fbbac40c37101": "Es necesario asegurar que {itemTitle} se alinee con los controles de acceso lógico SOC 2 CC6.1...", + "129d653a009388ac": "La organización utiliza AWS y Okta, debe incorporar requisitos específicos de la nube.", + "f23c067fee7bb793": "NIST 800-53 sugiere implementar AC-2 para los procedimientos de gestión de cuentas.", + "5e4e337077a12a87": "Hallazgo de auditoría anterior: falta de flujos de trabajo de aprobación documentados. Agregando sección 4.2.", + "35ee598510e2cb11": "Referencia cruzada con ISO 27001 A.9.2.1 - Registro y cancelación de registro de usuarios.", + "64148bf0eac8d3a5": "La política debe ser ejecutable para el equipo de DevOps, evitando un lenguaje excesivamente restrictivo.", + "9b389859fd57cf7a": "Incluyendo ejemplos específicos de roles de AWS IAM para hacer que la política sea concreta e implementable.", + "22592ea85ba402ae": "Niveles de clasificación de datos: Público, Interno, Confidencial, Restringido. Mapeando acceso.", + "87f03d1e1621e93e": "El equipo legal requiere cumplimiento del Artículo 32 de GDPR - agregando requisitos de cifrado.", + "846fa512ba420d44": "Considerando los principios de confianza cero mientras se mantiene la eficiencia operacional.", + "ae5bbf219cdeeb32": "La Sección 3.1 necesita una ruta de escalamiento más clara para las aprobaciones de solicitudes de acceso.", + "75dccab119eb6e72": "Agregando requisitos de revisión de acceso trimestral basados en las mejores prácticas de la industria.", + "f27207ad1e218172": "Verificando si {itemTitle} tiene un informe SOC 2 Tipo II válido con fecha dentro de los últimos 12 meses...", + "d6514386f8c3d26f": "Se encontró un incidente de seguridad del tercer trimestre de 2023. Evaluando las medidas de remediación implementadas.", + "e5ee41f02528b043": "El proveedor procesa datos de pago - se requiere verificación de cumplimiento de PCI DSS.", + "6fbd84f837cd18af": "Analizando términos del BAA: eliminación de datos dentro de 30 días, cifrado en reposo confirmado.", + "b569abc041153c55": "La lista de subprocesadores incluye AWS us-east-1. Verificando los requisitos de residencia de datos.", + "9eb26380d6d12818": "La autenticación de API utiliza OAuth 2.0 con tokens JWT. Revisando la expiración de tokens.", + "af670a0f23e7ba9d": "El proveedor obtuvo una puntuación de 89/100 en el último cuestionario de seguridad. Identificando áreas de mejora.", + "f91bae6a6e7c6105": "DPA de GDPR firmado el 2024-01-15. Las obligaciones del Artículo 28 parecen estar cumplidas.", + "e042a95c39abf2ea": "El informe de prueba de penetración muestra dos hallazgos de nivel medio - ambos remediados.", + "b62bb1387099e5fe": "El SLA garantiza un 99.9% de tiempo de actividad. Tiempo de respuesta a incidentes: 4 horas para críticos.", + "c845470f06a4fc92": "Cobertura de seguro: $10M responsabilidad cibernética. Adecuada para nuestro perfil de riesgo.", + "9e9830ffba816c49": "La integración requiere acceso de solo lectura a la API. Menor riesgo que los permisos de escritura.", + "0ea2933bc2458829": "{itemTitle} procesa aproximadamente 50K registros de clientes mensualmente...", + "3c1523c4cd8fb599": "Perfil del actor de amenaza: atacantes externos que se dirigen a credenciales de SaaS mediante phishing.", + "0e9e3ab055b8904f": "Adopción actual de MFA al 87%. Riesgo reducido pero no eliminado.", + "7e21aaa580f82098": "Probabilidad: Media (organizaciones similares sufren brechas 2 veces por año por encima del promedio de la industria).", + "aee31cdf32502440": "Impacto: Alto (potencial de exposición de PII, multas regulatorias de hasta $2M).", + "bc56a7b4768c2001": "Controles existentes: WAF, protección DDoS, detección de anomalías. Efectividad: 75%.", + "87bcd142c63687ea": "Riesgo residual después de controles: Medio-Bajo. Dentro del umbral de apetito de riesgo.", + "4a7731a6d4d5f897": "Riesgo de cadena de suministro: 3 proveedores críticos con acceso a sistemas de producción.", + "e5ed0062d5116cba": "Riesgo de cumplimiento: La aplicación del GDPR se está intensificando, multa reciente de €20M por una violación similar.", + "e1f3c9f17f422164": "Objetivo de tiempo de recuperación: 4 horas. Capacidad actual: 6 horas. Brecha identificada.", + "96225dcb30c456c4": "Tratamiento del riesgo: Aceptar con monitoreo, implementar controles adicionales de registro.", + "5984b63aa74924ac": "Ciclo de revisión trimestral establecido. KRI: Intentos de inicio de sesión fallidos > 1000/día.", + "5f34b7e28b939756": "Implementando {itemTitle} usando reglas de AWS Config y funciones Lambda...", + "16c74a7252f3d995": "Objetivo de control: Asegurar que todos los buckets de S3 tengan el cifrado habilitado por defecto.", + "36ec2ef6418208f8": "Estado actual: 67% conforme. El script de corrección automática solucionará las no conformidades.", + "1c31019eda7a46fb": "Metodología de pruebas: Escaneos automatizados diarios con alertas al equipo de seguridad.", + "ab04256d1fcbdaba": "Tasa de falsos positivos actualmente 12%. Ajustando lógica de detección para reducir ruido.", + "bf4e0a40e2532004": "Recopilación de evidencia: registros de CloudTrail agregados al SIEM central durante 90 días.", + "60c40abf51964aef": "El control se mapea a: SOC 2 CC6.7, ISO 27001 A.10.1.1, NIST 800-53 SC-28.", + "f9c6244d55a050ec": "Control compensatorio: Si falla el cifrado, el acceso se restringe únicamente a usuarios de VPN.", + "5dad1a33a09609c3": "Impacto en el rendimiento medido: <100ms de latencia añadida, aceptable para el caso de uso.", + "f908042093f47ebf": "Integración con sistema de tickets para seguimiento de excepciones y flujo de trabajo de aprobación.", + "f9953757d5392d19": "Revisión mensual de efectividad de controles programada. Métrica de éxito: 95% de cumplimiento.", + "a2c754145d58807e": "Registro de auditoría mantenido en almacenamiento inmutable durante 7 años según la política de retención.", + "8dddf07a80802343": "Recopilando líneas base de configuración de {itemTitle} del entorno de producción...", + "1415c9225dc34435": "Captura de pantalla tomada: Política de aplicación de MFA mostrando \"Requerido para todos los usuarios\".", + "01252cdbda19a7de": "Extrayendo registros de acceso de 30 días. Se encontraron 1,247 eventos de autenticación únicos.", + "97ca82efa410a7a8": "Reglas de firewall exportadas: 47 reglas en total, 12 permiten tráfico entrante, el resto deniega por defecto.", + "76f2f9f8ec2c6414": "Hoja de cálculo de revisión de acceso de usuarios generada. 234 usuarios activos, 18 requieren verificación.", + "f8126c811c3def47": "Tickets de gestión de cambios: 89 cambios de infraestructura, todos tienen registros de aprobación.", + "e3fe841e347e340e": "Informe de escaneo de vulnerabilidades: 2 hallazgos críticos, 5 altos, 23 medianos documentados.", + "9390e26e03e0ea15": "Resultados de prueba de respaldo: Última restauración exitosa 2024-11-28, RTO cumplido exitosamente.", + "a5e4bf9b25f6afdb": "Finalización de capacitación en seguridad: 96% de empleados, 4% tiene 7 días para completar.", + "7bcc0bd0dc78fa9b": "Prueba de respuesta a incidentes: Ejercicio de simulacro completado el 15-10-2024, informe adjunto.", + "81e51fda0ba12f00": "Evidencia de prueba de penetración: Resumen ejecutivo y cronograma de remediación incluidos.", + "eea6779588b6a0be": "Diagrama de arquitectura del sistema actualizado el 2024-11-01, refleja el estado actual con precisión.", + "081d0966b5c6fc72": "Seleccionar Organización", + "f1ad9915e45f9b87": "Buscar organización...", + "bf0d4657a8482f44": "No se encontraron resultados", + "2dcd8eb595a98922": "Crear Organización", + "b174a4971bd5a200": { + "i": 1, + "c": [ + "Basado en ", + { + "i": 2, + "k": "_gt_n_2", + "v": "n" + }, + "+ reseñas" + ] + }, + "3ab928741d208f41": "Cargando...", + "a656dc69a38a3639": "Cerrar sesión", + "12c395f03d83798a": "-", + "e0b196715ec7a4a6": "Borrador", + "1f34e63fe8f8a0f1": "Pendiente", + "2538c2d97c9b5e23": "En Progreso", + "166661f94b300f5b": "Completado", + "ac9e0f68a7aae8e3": "Publicado", + "90585f4150afc7b3": "Archivado", + "81bcca9042fe2974": "Requiere Revisión", + "b2be70822d55f16a": "Abrir", + "353b08f4363dbb2c": "Pendiente", + "7f7f6116f1c8f3a9": "Cerrado", + "fe5ee6266a8106e0": "Tema", + "d604bc5cf3386f00": "Oscuro", + "8bf65a425f50d140": "Claro", + "a03e4c1348ad9ba8": "Sistema", + "ec8a07c2ad1276af": { + "i": 1, + "c": "Conectando..." + }, + "a2d1b1e205f1f61e": { + "i": 1, + "c": "Error al establecer la conexión" + }, + "2c1c93a646bbea6e": "Idioma", + "4b00a015a0eb6fe8": "Error al buscar contacto: {statusText}", + "6b490d1fe5ca4f8e": "Contacto no encontrado", + "ac946a2e22f9f500": "Error al crear el contacto: {statusText}", + "c5a929fac7a9e327": "Se requiere un correo electrónico válido", + "c6fcbccbdcba05d8": "Contacto procesado", + "34dd4d56798aa662": "El contacto ya existe", + "dc18ab7fc9a77c05": "Error al crear el contacto", + "2cd5c7e662870add": "Contacto creado exitosamente", + "08350d0b3f3b9cba": "El acuerdo ya existe", + "e384ed5082c5fa0d": "Acuerdo creado exitosamente", + "c8b06a21a22e464b": "El correo electrónico, nombre y empresa son obligatorios", + "f5fdbe73ae34d0a3": "Formato de correo electrónico inválido", + "e86e7954529d6eb3": "Contacto actualizado, falló la creación de la empresa", + "51f378aa252b777c": "Contacto actualizado y empresa creada", + "85cf98d240d27ef7": "La pregunta es obligatoria", + "2b7207717ccb0bf1": "La respuesta es obligatoria", + "61446b88120b01c9": "No hay organización activa", + "bcd0bb9513c37265": "Se requiere ID", + "4992c8d68cc1c713": "El servicio de carga de archivos no está disponible actualmente. Por favor, contacte al soporte técnico.", + "571229b690a8d4c0": "El servicio de carga de archivos no está configurado correctamente.", + "61530496d883f969": "No autorizado - no se encontró organización", + "19edc8b7b3d6337c": "El archivo excede el límite de {maxSize}MB.", + "b25b0d1a56fc7800": "Se produjo un error desconocido.", + "7608344f5e4d254e": "El nombre de la integración es obligatorio", + "f11a7b66655ca857": "Integración no encontrada", + "13d336d41ee4f94c": "El ID de integración es obligatorio", + "69d3b06ddf8561b6": "Organización del usuario no encontrada", + "e9ec339c7c9a3b15": "Configuración de usuario no encontrada", + "a035e1c9da459c41": "Error al actualizar la configuración de integración", + "5df1f59871a959d5": "Invitación ya utilizada o expirada", + "b4cc3df4bf1e23b7": "El rol de invitación es obligatorio", + "ff0219794718b7d4": "Por favor seleccione al menos un marco de trabajo para agregar", + "8d0ba06dcf0ad320": "No se encontraron marcos de trabajo válidos o visibles para los ID proporcionados.", + "7e8c8c600c1502b3": "Se requiere al menos un correo electrónico.", + "98d113f40aa32db9": "Organización no autorizada o inválida.", + "a6c923502d5c309e": "Error en la invitación", + "93800980cf3f9f21": "El nombre es obligatorio", + "1a6916f38d78c414": "El nombre debe tener menos de 64 caracteres", + "499a010da4572c1a": "Ya existe una clave de API con este nombre", + "7ae282f878f98072": "La organización no existe o no tienes acceso", + "ec27ab25b88e116c": "Ocurrió un error inesperado al crear la clave API", + "6681202b93fd74f4": "Debe iniciar sesión para realizar esta acción", + "081c69c8073ccfa5": "Ocurrió un error al obtener las claves de API", + "58bc1fbef04322d1": "El usuario no tiene una organización", + "3185753fd1448445": "Error al obtener los usuarios de la organización", + "9494a06ca07676bf": "Error al invitar al empleado", + "8644739438f720c5": "Error al invitar miembro", + "6b1a11d296cfcf61": "Permiso denegado: Solo los administradores o propietarios pueden eliminar empleados.", + "ee8d78fee0ce6998": "Empleado objetivo no encontrado en esta organización.", + "526a5c7b7db48955": "El miembro objetivo no tiene el rol de empleado.", + "b7a345e2e32364ee": "No se puede eliminar al propietario de la organización.", + "615343fbec2fa6d1": "No puedes eliminarte a ti mismo.", + "0829dcd63781362b": "Error al eliminar el rol de empleado o miembro.", + "c669cbd9279d4fa2": "Clave API no encontrada o no autorizada para revocar", + "8c24e7e5f9fb61dc": "Clave API revocada exitosamente", + "55e68651c83799e4": "Ocurrió un error al revocar la clave API", + "360ae5f19e5fa38e": "El nombre de la organización es obligatorio", + "af681662d1cd37e8": "El nombre de la organización no puede exceder 255 caracteres", + "6b83f8a22c316bc5": "Entrada de usuario inválida", + "dfa1845a2bddfa6b": "Error al actualizar el nombre de la organización", + "934abe779b34dd97": "Por favor, ingrese un sitio web válido que comience con https://", + "71a97db924310d8e": "El sitio web no puede exceder 255 caracteres", + "ff69eed15988f709": "Error al actualizar el sitio web de la organización", + "ec2acd6e3ff9caa4": "Dirección de correo electrónico inválida", + "9b9d11d0e2023ea6": "El departamento es obligatorio", + "f457eac38691be78": "Ya existe un empleado con este correo electrónico en su organización", + "093af350818af61f": "Error al crear empleado", + "7de570cd40b05815": "Se requiere aprobador", + "e4970c4911820cfc": "Política no encontrada", + "3729aeae40b658ca": "El aprobador no es el mismo", + "0a607533715b6c29": "Cambios de política aceptados: {comment}", + "44dd7cbb5abca72a": "Error al actualizar el estado del archivo de políticas", + "b6c98291dc1dabf7": "El título es obligatorio", + "8b306dec89ad5329": "La descripción es obligatoria", + "4c8a96386ac5cb37": "Error al crear la política", + "891046481444e709": "Error al eliminar la política", + "c7b6c0692baa72d5": "Cambios de política denegados: {comment}", + "02cbf0d6e14f4565": "Error al actualizar la política", + "8394a0e608c7352e": "Error al actualizar la descripción general de la política", + "44a2775a2cd098b3": "El nombre del riesgo es obligatorio", + "1bd3412348952e61": "El nombre del riesgo debe tener al menos 1 carácter", + "92e6ce61fd5a7646": "El nombre del riesgo debe tener como máximo 100 caracteres", + "700fc80de6771e54": "La descripción del riesgo es obligatoria", + "662df059a32ed512": "La descripción del riesgo debe tener al menos 1 carácter", + "48abbd85fbb8d42e": "La descripción del riesgo debe tener como máximo 255 caracteres", + "6a508138767ae01b": "La categoría de riesgo es obligatoria", + "6cb1344d7bb60805": "Se requiere el departamento de riesgos", + "2eb27f85f5fa3757": "El ID de riesgo es obligatorio", + "5f94faac8ca6b62a": "El título del riesgo es obligatorio", + "a93d8137d0c4250c": "El estado del riesgo es obligatorio", + "7744873797fa4f62": { + "i": 1, + "c": [ + { + "i": 2, + "c": "Hacer una pregunta..." + }, + { + "i": 3, + "c": [ + { + "i": 4, + "c": "⌘" + }, + "K" + ] + } + ] + }, + "b550c0dd9f4f4013": { + "i": 1, + "c": [ + "Hola ", + { + "i": 2, + "k": "_gt_value_2", + "v": "v" + }, + ", ¿cómo puedo ayudarte ", + { + "i": 3 + }, + "hoy?" + ] + }, + "e25f5cd6c48a18a3": "Pregúntale algo a Comp AI...", + "eabbb0cad6684a29": { + "i": 1, + "c": "Seguir a Comp AI" + }, + "7381b05760ff4528": { + "i": 1, + "c": "Únete a nuestro Discord" + }, + "69dc7bfcbe2128f6": { + "i": 1, + "c": "GitHub" + }, + "b29583f46872d727": { + "i": 1, + "c": "Enviar" + }, + "576f822d612df1ed": { + "i": 1, + "c": "Destacar en GitHub" + }, + "649d1e2534341bbc": "Asistente", + "fe3092eea51ad000": { + "i": 1, + "c": "Razonamiento" + }, + "678e022381819009": { + "i": 1, + "c": "Razonó durante unos segundos" + }, + "5c7c6682db53c73d": "Un modelo de razonamiento", + "6f1348739b70bf49": "Seleccionar un modelo", + "49c99c1362eb67d8": { + "i": 1, + "c": "No se encontraron riesgos para esta organización." + }, + "7631338167dd32ed": "El archivo \"{fileName}\" excede el límite de {fileSize}MB.", + "7ba76385c920f66c": "Archivo \"{fileName}\" listo para adjuntar.", + "e3c4e9c6ec5c9490": "Archivo eliminado del borrador de comentario.", + "f1223b968bbb9809": "No se pudieron encontrar los datos del archivo.", + "97128ea67529a4b3": "¡Comentario agregado!", + "f3bf69a47b45706d": "Error al agregar comentario", + "0527dd2978caa527": "Deja un comentario...", + "00d2def46b19915a": "Agregar otro archivo adjunto", + "584c3539b9524b9a": "Agregar adjunto", + "5a728cdfad4b49e7": "Enviar comentario", + "5a238640ebe37a35": { + "i": 1, + "c": "Archivos Pendientes:" + }, + "8c65e0cfbe7c43cc": "hace {seconds}s", + "e5e7393da2286e73": "hace {minutes}m", + "c0fbd8a3ddfe25c2": "hace {hours}h", + "8eee7a642a02f5ca": "hace {days}d", + "b2b7df62dda45896": "No se detectaron cambios.", + "e1bc141381c02126": "Comentario actualizado exitosamente.", + "fcc6e922847b174f": "Error al guardar los cambios del comentario.", + "db8d6efdbe9a7393": "¿Está seguro de que desea eliminar este comentario?", + "6ea95fc2e598b2e2": "Comentario eliminado exitosamente.", + "869abcfdface0a9e": "Error al eliminar el comentario.", + "25680b5460511396": "Editando...", + "96f27adf6d8717a5": "Opciones de comentario", + "da137a49a07f55be": "Editar comentario...", + "c9ebab10d50bb311": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Editar" + ] + }, + "426ca0db185f5926": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Eliminar" + ] + }, + "62567f82e191e63a": { + "i": 1, + "c": "Archivos adjuntos:" + }, + "078eb7f64edb1177": { + "i": 1, + "c": "Cancelar" + }, + "0ce15b45d2c94267": { + "i": 1, + "c": "Guardar Cambios" + }, + "8b77482b4eab1134": { + "i": 1, + "c": "Comentarios" + }, + "aad7ea0629c4ad11": { + "i": 1, + "c": [ + "Deja un comentario en este ", + { + "i": 2, + "k": "_gt_value_2", + "v": "v" + } + ] + }, + "381802942652805d": "Ascend", + "4c5ff33870428bd4": "Descender", + "6d49999f19231b80": "Restablecer", + "b7f10562a5e40934": "Seleccionar rango de fechas", + "0d6d75c54cdde940": "Seleccionar fecha", + "dfbc7f4ecef8c63a": "Limpiar filtro de {title}", + "8d8aabc001c6ffc2": [ + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + }, + " seleccionado" + ], + "4fc1bbbd387f353d": "No se encontraron resultados.", + "3fe9728594a7340c": "Limpiar filtros", + "449fd5d15bf93047": "Contiene", + "6d8769baa1c5249d": "No contiene", + "65b9cc0a1d3c957e": "Es", + "dd0579dd7d27b66e": "No es", + "8895f4a5673d13b1": "Está vacío", + "a035cbd2cf175dca": "No está vacío", + "6fc14a93492c3831": "Es menor que", + "b9abca20d78a14d4": "Es menor o igual que", + "440338bfe6e21f9a": "Es mayor que", + "ae1e8b8dea981519": "Es mayor que o igual a", + "2db59c290cb161a5": "Está entre", + "2848493b2998ec02": "Es anterior a", + "421032cdc38ae5da": "Es después de", + "d10c483695eb36c2": "Es en o antes de", + "e2c961c4d6f84e38": "Es en o después de", + "ce01e6360cef71d8": "Es relativo a hoy", + "f6af6f6d703e087a": "¿Alguno de", + "651ae384b1363135": "No tiene ninguno de", + "f56519e5cd5d1b96": "Asc", + "cbc2349b0272b98b": "Desc", + "635b007acecacfb0": "Seleccionar operador de unión", + "312229757f6c2451": "Seleccionar campo", + "10b37fa1e1f1b5e2": "Buscar campos...", + "dc10cd9afcfbce60": "No se encontraron campos.", + "09e409a914e00842": "Filtrar", + "231876f4e3131d51": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "false": "No se han aplicado filtros", + "true": "Filtros" + } + } + } + }, + "9c90cf3672c82c6b": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": "Modifica los filtros para refinar tus filas.", + "false": "Agregue filtros para refinar sus filas." + } + } + } + }, + "1abe7fdace9bed26": "Agregar filtro", + "9d67576540538403": "Restablecer filtros", + "d796b60ce9483a56": { + "i": 1, + "c": "Donde" + }, + "01da1181a33d4d8a": "Restablecer todos los filtros", + "c1803444cffc26a9": "Abrir menú de comandos de filtro", + "c5279ee546271b16": "No se encontraron opciones.", + "0ef0be291355927d": "Verdadero", + "a064adbe549976ce": "Falso", + "c7353e2d81aae187": "Escriba para agregar filtro...", + "b9973b758dc4bf48": "Filtrar por \"{value}\"", + "c5da7990ef2f009d": "Ingrese valor...", + "5df677862609f8e4": "Seleccionar opciones...", + "bbc784ba97dcb7d3": "Seleccionar opción...", + "02155a6c2ff34c49": "{count} seleccionado", + "720d54196c45c4d9": "Buscar opciones...", + "4109343a769931cc": "Seleccionar fecha...", + "c04bf99655977ace": "No se encontraron campos.", + "9a981833fab4b1af": "{count} elementos", + "f148ed5275b3ef02": "Página {current} de {total}", + "f9298317ca78df9e": { + "i": 1, + "c": "por página" + }, + "4f113bb0bc732b3d": "Limpiar", + "ca2fbe7d996582c0": "Ordenar", + "a2100d559a1be5fc": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": "Ordenar por", + "false": "Sin ordenamiento aplicado" + } + } + } + }, + "5152c47f0f71e526": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "false": "Agregue ordenamiento para organizar sus filas.", + "true": "Modifica la ordenación para organizar tus filas." + } + } + } + }, + "2c87311c1d6334de": "Agregar ordenamiento", + "73c1ea24b39f4431": "Restablecer ordenación", + "01936d3df6fe9c73": { + "i": 1, + "c": "Restablecer" + }, + "f0e2c30e6c995e01": "Alternar columnas", + "6d3fb684b4dcb478": "Buscar columnas...", + "00513a3b28638818": "Ver", + "02ef66777b107f17": "No se encontraron columnas.", + "135b07d7c6d7f96b": "Sin resultados.", + "8ecd1d75efba5973": "¡Pago Exitoso! 🎉", + "adfdc86a02a67b79": "Su factura ha sido pagada. Para comenzar, por favor continúe con la incorporación para que nuestra IA pueda ponerse a trabajar.", + "d02253d85cc0ad42": "Factura Pagada", + "2630e7b58f5f271b": "Continuar", + "346d6e30764bfc08": "Su suscripción Starter ya está activa.", + "a989702ea031ac0f": "Completar Incorporación", + "9cf577c8890f5192": "Comienza a escribir...", + "1dbfcccee2f41735": "El contenido del comentario es obligatorio", + "17c95d7bde7079d6": "El contenido del comentario debe tener como máximo 1000 caracteres", + "fc11ea376d8d263a": "El ID de entidad es obligatorio", + "430a9feec5c2f85a": "Comentario agregado exitosamente", + "3e2dda1a9354f4b4": "Error al agregar comentario", + "32ad6b9868508897": "Comentario", + "baaf07d893736ec8": "Crear", + "0c56aa61e7ca3a68": "Control", + "21d0ef064eefa159": "Estado", + "817047065121ef0c": "Artefactos", + "3b86d4676ea1a73d": "Configuración actualizada", + "12044995fed2a1f7": "Error al actualizar la configuración", + "a04f909aa23919f1": "Ingrese su {key}", + "182b96027decf5f9": "Guardar configuración", + "3f4688ea903c56a0": { + "i": 1, + "c": "No hay configuraciones disponibles" + }, + "cfed4c336b385bd1": "Integración desconectada exitosamente", + "2ab078dd0d62306a": "Error al desconectar la integración", + "0e437fdfddcc151b": "Configuración actualizada exitosamente", + "90525fafec26e041": "Por favor, ingrese una clave de API", + "12b3af3da3ed9af3": "Clave API actualizada exitosamente", + "b917b9367d2548db": "Clave API guardada exitosamente", + "9847083891d48ecd": "Error al conectar la integración", + "ccf357d79dd190c0": "Ejecutándose pronto", + "dc4cea90f4c6f350": "Se ejecuta en {minutes} minuto{s}", + "92eeb1032fac1f0d": "Se ejecuta en {hours} hora{s}", + "96a63a2f4d195ea6": "Se ejecuta en {hours} hora{hourS} y {minutes} minuto{minS}", + "3d7cdab1909733c0": "Gestionar", + "5d973795984549fc": "Instalar", + "c420251537d9d5f0": "Desconectando...", + "c291bdeadbb22b96": "Desconectar", + "8d2eb5921047c347": "Actualizar Clave API", + "cce695cd20f84e9f": "Ingrese la Clave API", + "5de921252d2ab383": "Ingrese su clave API de Deel", + "00ad8aeed0a6c461": "Cancelar", + "3414b088dde8621b": "Guardando...", + "08f54865b364957d": "Actualizar", + "762fcce45b65eea9": "Guardar", + "fd5ce4511efb138c": { + "i": 1, + "d": { + "t": "b", + "b": { + "true": { + "i": 2, + "c": "Conectado" + }, + "false": { + "i": 2, + "d": { + "t": "b" + } + } + } + } + }, + "8e9d617e99bb1d59": { + "i": 1, + "c": "Conectado" + }, + "d1a860782a64a1ce": { + "i": 1, + "c": "Cómo obtener credenciales" + }, + "b8be7548baaa2f1a": { + "i": 1, + "c": "Información" + }, + "26fb935cea964ddb": { + "i": 1, + "c": "Estado de Sincronización" + }, + "23b9df9912a04011": { + "i": 1, + "c": "Última Sincronización" + }, + "372351dac94cefcb": { + "i": 1, + "c": "Las fechas se muestran en su zona horaria local" + }, + "b9d3f4f1354a2755": { + "i": 1, + "c": "Nunca ejecutar" + }, + "d01d5f62b22c9884": { + "i": 1, + "c": "Próxima Sincronización" + }, + "fd4311b736f2b65e": { + "i": 1, + "c": "UTC 00:00" + }, + "b88cd8b450fdecc4": { + "i": 1, + "c": "Esta integración se ejecuta a medianoche UTC (00:00). Los horarios se convierten a su zona horaria local para su visualización." + }, + "51e43283b5c6d9a5": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": { + "i": 3, + "c": "Calculando..." + }, + "false": { + "i": 3, + "c": "Se ejecutará a la próxima medianoche UTC" + } + } + } + } + }, + "bb18b5e756a1fff8": { + "i": 1, + "c": { + "i": 2, + "c": "Esta integración se sincroniza automáticamente todos los días a medianoche UTC (00:00)." + } + }, + "7f3f1617bd786b1a": { + "i": 1, + "c": "Configuración" + }, + "6b1da89bf7daab6e": "Actualizar", + "cf321df2b8a4b9c1": { + "i": 1, + "c": "Puedes encontrar tu clave de API en la configuración de tu cuenta de Deel." + }, + "8303f721ddd31eee": { + "i": 1, + "c": "Todas las integraciones en la tienda de Comp AI son de código abierto y revisadas por pares." + }, + "be171400741ca722": { + "i": 1, + "c": "Informe" + }, + "d09e927be9527f55": "Buscar integraciones", + "045741aa62df51f8": "Todos", + "f84879ada73428c9": "Instalado", + "5036ffda17a1fd59": { + "i": 1, + "c": [ + { + "i": 2, + "c": "No se encontraron integraciones" + }, + { + "i": 3, + "c": "No se encontraron integraciones para su búsqueda, háganoslo saber si desea ver una integración específica." + }, + { + "i": 4, + "c": "Limpiar búsqueda" + } + ] + }, + "76824883c3c1a8f5": { + "i": 1, + "c": [ + { + "i": 2, + "c": "No hay integraciones instaladas" + }, + { + "i": 3, + "c": "Aún no has instalado ninguna integración. Ve a la pestaña 'Todas las Integraciones' para explorar las integraciones disponibles." + } + ] + }, + "44ee1d07a46cbc73": "su organización", + "ba52388b69e452f8": { + "i": 1, + "c": "Comp AI" + }, + "b27b0340381fb1cf": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "false": { + "i": 3, + "c": "Volver a sus organizaciones" + }, + "true": { + "i": 3, + "c": [ + "Continuar con ", + { + "i": 4, + "k": "_gt_value_4", + "v": "v" + } + ] + } + } + } + } + }, + "94374ac393dcdf6e": { + "i": 1, + "c": "Atrás" + }, + "831a5c1c57c96db0": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Crear Organización" + ] + }, + "a1f8c2a2f8687542": "Cerrando sesión...", + "7053c5ede3a89e26": { + "i": 1, + "c": "Tema" + }, + "f509cc6b2846e115": { + "i": 1, + "c": "Asistente" + }, + "a7a31d42573cac7c": { + "i": 1, + "c": "Crear Nueva Política" + }, + "39417513d6d882ad": { + "i": 1, + "c": "Crear Nuevo Riesgo" + }, + "aad2ff24e992abad": { + "i": 1, + "c": "Actualizar Riesgo" + }, + "6104cc04d1ac1b1c": { + "i": 1, + "c": "Actualizar detalles y metadatos del riesgo" + }, + "cc907abfabc0fc5f": "Actualizar Tarea", + "f6b2f3d41c5c87ce": "Actualizar detalles y metadatos de la tarea", + "fb595704fdbe074f": "Actualizar Riesgo", + "d254b4e6e7d3e824": "Vista previa del archivo: {fileName}", + "fa7abddc6635a0c7": "Vista previa", + "8274eed7365bd5b7": "Vista previa no disponible para este tipo de archivo", + "928461cd6c4f3aeb": "Abrir Archivo", + "aa7ab7e425f84725": "Eliminar Archivo", + "6d50f14d5cde241c": "Esta acción no se puede deshacer. El archivo será eliminado permanentemente.", + "d15d4ce9981708e9": "Cancelar", + "aba29e77e52bca46": "Eliminar", + "2fd3dcae73edb555": "Capacitación en Concienciación de Seguridad - Parte 1", + "4ceb900ff4549a04": "Capacitación en Concienciación de Seguridad - Parte 2", + "8245671c8d083fab": "Capacitación en Concienciación de Seguridad - Parte 3", + "24908aa2bee25d40": "Capacitación en Concienciación de Seguridad - Parte 4", + "e03daad7ef5f1c59": "Capacitación en Concienciación de Seguridad - Parte 5", + "345c45b9e5a16595": "Se requiere el ID de tarea", + "701cd1ffc237a3e1": "El estado de la tarea es obligatorio", + "1624c4d0fd4f78a1": { + "i": 1, + "c": "Acceso Denegado" + }, + "840744edc4a0ee10": { + "i": 1, + "c": [ + "Los ", + { + "i": 2, + "c": "empleados" + }, + " no tienen acceso a app.trycomp.ai, ¿querías ir a ", + { + "i": 3, + "c": "portal.trycomp.ai" + }, + "?" + ] + }, + "0009a40d430bf2c7": { + "i": 1, + "c": "Por favor, seleccione otra organización o contacte al administrador de su organización." + }, + "24c8274e58f2c71d": { + "i": 1, + "c": "¡Algo salió mal!" + }, + "ef76210f4825a1f3": "Iniciar sesión | Comp AI", + "da095f18d5389d85": { + "i": 1, + "c": "Comience con Comp AI" + }, + "d6e1a9b25d836b5f": { + "i": 1, + "c": "Automatiza el cumplimiento de SOC 2, ISO 27001 y GDPR con IA." + }, + "d41dc5c1b5633678": { + "i": 1, + "c": [ + "Al hacer clic en continuar, reconoces que has leído y aceptas los ", + { + "i": 2, + "c": "Términos y Condiciones" + }, + " y la ", + { + "i": 3, + "c": "Política de Privacidad", + "t": "Link" + }, + "." + ] + }, + "0adfba7b19447fbc": "Error al obtener marcos de trabajo", + "7ad8430955eb0da5": "Organización eliminada", + "3d3e3549b89579f3": "Error al eliminar la organización", + "0eed7bfc26aaf228": { + "i": 1, + "c": "Eliminar organización" + }, + "785131ef1b25f3b7": { + "i": 1, + "c": { + "i": 2, + "c": "Eliminar permanentemente su organización y todo su contenido de la plataforma Comp AI. Esta acción no es reversible - por favor continúe con precaución." + } + }, + "49f4d8412c2bb38a": { + "i": 1, + "c": "Eliminar" + }, + "52448275282cc69d": { + "i": 1, + "c": "¿Está completamente seguro?" + }, + "8497cc456ff119fe": { + "i": 1, + "c": "Esta acción no se puede deshacer. Esto eliminará permanentemente su organización y removerá sus datos de nuestros servidores." + }, + "e1131e0039f11b24": { + "i": 1, + "c": "Escriba 'delete' para confirmar" + }, + "1f8dee316c83968c": "Nombre de la organización actualizado", + "963a44e63f2e2cdc": "Error al actualizar el nombre de la organización", + "9be88615357a72ad": { + "i": 1, + "c": "Nombre de la organización" + }, + "f194b3501d5b9c31": { + "i": 1, + "c": { + "i": 2, + "c": "Este es el nombre visible de su organización. Debe usar el nombre legal de su organización." + } + }, + "a5fea9ab37808712": { + "i": 1, + "c": "Por favor, utilice un máximo de 32 caracteres." + }, + "9eb2dbf6073def42": "Guardar", + "7499eb925aaf4410": "Sitio web de la organización actualizado", + "11c92ddd40fd858d": "Error al actualizar el sitio web de la organización", + "f97106541b6f524a": "https://example.com", + "59e7c49dfa4ca2f7": { + "i": 1, + "c": "Sitio Web de la Organización" + }, + "ff492def2ef4fda3": { + "i": 1, + "c": { + "i": 2, + "c": "Este es el sitio web oficial de su organización. Incluya https:// en la URL." + } + }, + "ffa9e1e0ab1ed157": { + "i": 1, + "c": "Por favor, ingrese una URL válida que incluya https://" + }, + "c34dfd6509db4aa4": "Política creada exitosamente", + "c3ea24ac809a5ed5": "Título", + "c0d3dd0975eb2f22": "Descripción", + "bbecac783d914b1d": "Crear", + "1f4ac9f59db33dcf": { + "i": 1, + "c": "Detalles de la Política" + }, + "a09c076df3c6fb05": { + "i": 1, + "c": "Título" + }, + "83b7bf5f7dd25b5a": { + "i": 1, + "c": "Descripción" + }, + "cee94a280b207444": "Política actualizada exitosamente", + "0dd6aa027c61a84e": "Seleccionar un estado", + "e5cd24a4faa039bf": "Seleccionar una frecuencia", + "16b37c89dad005cc": "Seleccionar un departamento", + "e0b4460d13982a86": { + "i": 1, + "c": "Estado" + }, + "08a4592d8ac6f46d": { + "i": 1, + "c": "Frecuencia de Revisión" + }, + "15c51f3205be03f7": { + "i": 1, + "c": "Departamento" + }, + "ea602b6fe57b1a5e": { + "i": 1, + "c": "Fecha de Revisión" + }, + "17deebad0588a9d6": { + "i": 1, + "c": "Seleccionar una fecha" + }, + "8e98ba6e5b9a4950": { + "i": 1, + "c": "Requisito de Firma" + }, + "b428d68cc6df1424": "Título de la Política", + "f62c5a67dc572245": "Un breve resumen del propósito de la política.", + "fe8b28f3f7797f15": "Seleccionar requisito de firma", + "19b1099378b97c94": { + "i": 1, + "c": "Política" + }, + "6ea5518896ba60d9": { + "i": 1, + "c": "Título de la Política" + }, + "452e423ef3b1caa4": { + "i": 1, + "c": "Obligatorio" + }, + "543af7699033a35a": { + "i": 1, + "c": "No Requerido" + }, + "5b374d8ac95407a1": "Muy Improbable", + "ddc9d4fdefecfc30": "Improbable", + "f7a915907975711a": "Posible", + "3812234f11f87b2e": "Probable", + "e3a71fbae1958029": "Muy Probable", + "33aee9cfb8429f58": "Insignificante", + "73d154f8ce68a76c": "Menor", + "b7614c51a9106906": "Moderado", + "b5077e9a87d6a611": "Mayor", + "f5a2fbf87cbc6882": "Grave", + "b66ceb2b175ed3ff": "Riesgo inherente actualizado exitosamente", + "6d23a43ccbc325ae": "Error al actualizar el riesgo inherente", + "0880b07a97426672": "Probabilidad", + "de229ed912476c1d": "Seleccionar una probabilidad", + "d03b9c7162592814": "Impacto", + "296eab7a34588461": "Seleccionar un impacto", + "2c026dc953650c86": "Riesgo residual actualizado exitosamente", + "cfe1af7de2f7e897": "Error al actualizar el riesgo residual", + "c0a26be534ec147a": "Riesgo creado exitosamente", + "c3be61741170e906": "Error al crear el riesgo", + "474f812397038a8c": "Un título breve y descriptivo para el riesgo.", + "af92901d4bdeb787": "Una descripción detallada del riesgo, su impacto potencial y sus causas.", + "1daed81aed823529": "Seleccionar una categoría", + "0579b2dbbb2fdb89": { + "i": 1, + "c": "Detalles del Riesgo" + }, + "aed2b1d4ca4a0c82": { + "i": 1, + "c": "Título del Riesgo" + }, + "91fc99c9c961902f": { + "i": 1, + "c": "Categoría" + }, + "ca37c5921378d33e": "Riesgo actualizado exitosamente", + "9e70271886da2142": "Error al actualizar el riesgo", + "e15c1a5064076784": "Asignado", + "e44870bff2865e77": "Categoría", + "050846063baa10bc": "Departamento", + "f83687d6120a040c": "Riesgo", + "81fcd6a1464fb197": "Título del Riesgo", + "24f1d772b511c736": "Política por Estado", + "913610499506d15c": "Sin datos", + "cec2d2c5ec79a2d6": "Riesgo Inherente", + "57aa11e883f694b7": "Nivel de riesgo inicial antes de aplicar cualquier control", + "b45b5056e77fd53d": "Riesgo Residual", + "6d49e6d6bd8de31e": "Nivel de riesgo residual después de aplicar los controles", + "5d6c5af01d1c1615": "Muy Probable (5)", + "d22959e7888c99c4": "Probable (4)", + "556a0e50bbdce353": "Posible (3)", + "2c330a1ddd236199": "Improbable (2)", + "c842da92ac94aeba": "Muy Improbable (1)", + "d786931b34ba90a5": "Riesgos por Responsable", + "00f7eee3c4b54a8a": "riesgos", + "dc1162bb6d3eda32": "Ene", + "eb7b909d3ae68f60": "Feb", + "ae0c4d02f05f0e84": "Mar", + "7bbcf2d2dac3dcbc": "Abr", + "c98e8db24999ff4d": "Mayo", + "962ef73f590b4bac": "Jun", + "9b0078a37517f25a": "Evaluación de Riesgos", + "c869dfd76fe37206": "No se encontraron departamentos con riesgos", + "9f0d929f8cdaf6e4": "Riesgos por Departamento", + "ad65546404c8cae9": "Riesgos por Estado", + "165a2b6aa8dd1dc1": "No se encontraron estados con riesgos", + "29f0824614831f53": "Resumen", + "8d7d9b32d90faf5c": "TablaDeMiembros", + "370aa80e162adbf6": "Activo", + "8e589c76d9bd6f9a": "Inactivo", + "02e7306761f0fbf9": "Nombre de la Política", + "ab8a9d2b0781f78e": "Última Actualización", + "b0aaf84101bcbd44": "{currentPage} de {pageCount}", + "e2d79fce6935c853": { + "i": 1, + "c": "Aún no hay políticas" + }, + "2cc118e7b7172e91": { + "i": 1, + "c": "Comience creando su primera política" + }, + "6598bf2ab7593769": { + "i": 1, + "c": "Crear primera política" + }, + "459babb2e8e622a8": { + "i": 1, + "c": "No se encontraron resultados" + }, + "fbc10b887f49921a": { + "i": 1, + "c": "Intenta otra búsqueda o ajusta los filtros" + }, + "5660de4bd8741f42": "Buscar políticas...", + "9d951b898dff2988": "Agregar Nuevo", + "47e8de45a3f55334": "Filtrar por estado", + "a499c791eb893aff": "Todos los estados", + "5d0c71caa8f328b0": "Limpiar", + "63d06637bba79431": "Crear Nueva Política", + "8fccb5dc828545da": "de", + "f1d3a4bf3c4655af": { + "i": 1, + "c": "Aún no hay riesgos" + }, + "5cbaaaa55a691579": { + "i": 1, + "c": "Comience creando su primer riesgo" + }, + "e1a353c384c700f1": "Buscar...", + "b125e6c1a265869f": "Filtrar por asignado", + "2e8be4c2763558d0": "Fecha de Vencimiento", + "91ab316b10fba806": "Asignado a", + "fb976abe5b22bbe6": "Tareas", + "204167c06dc9fb6e": "Estado", + "25e9479630195fcb": "Fecha de Vencimiento", + "bb9af02ac3d7e79e": "Asignado A", + "fb4e2a3ef3e189e9": "Limpiar filtros", + "80b0d46f4645688c": [ + { + "i": 1, + "c": "No se encontraron resultados" + }, + { + "i": 2, + "c": { + "i": 3, + "d": { + "t": "b", + "b": { + "false": { + "i": 4, + "c": "Crear una tarea para comenzar" + }, + "true": { + "i": 4, + "c": "Intenta otra búsqueda o ajusta los filtros" + } + } + } + } + } + ], + "49009f2a0b4d25b5": [ + { + "i": 1, + "c": "No se encontraron tareas" + }, + { + "i": 2, + "c": "Crea una tarea para comenzar" + } + ], + "1b86372aee9bfe20": "Pruebas por Asignado", + "14aa7708e8307f26": "{count} Pruebas", + "edb9bc067fe3638c": "Aprobado", + "d4d89c139de812a8": "Falló", + "d82511ccb6662fab": "No compatible", + "81a2234abad438f9": "Información", + "af3d81238e8baedd": "Bajo", + "b2ac3183408cf717": "Medio", + "31e4cd294be7530f": "Alto", + "1de36ca50e73ce0d": "Crítico", + "15e6350cc86327ae": "Distribución de Severidad de Pruebas", + "6c93d221af65a9b3": "No se encontraron datos.", + "af7eb4f9c7f0f575": "Filtros", + "e8f9d8610e636387": "Limpiar todos los filtros", + "e0e9858003517b37": "{count} {count, plural, one {elemento} other {elementos}}", + "bdea6c1c521ade55": "Página {page} de {totalPages}", + "d41deca9ff1e9db8": "Aprendiendo sobre su empresa...", + "24e246759b17530a": "Creando Riesgos...", + "116c9f0527f902ed": "Creando Proveedores...", + "bb657cc41552a031": "Personalizando Políticas...", + "255e48f78089ee59": "Estado de Incorporación", + "3b6716a52e102b26": "La configuración de la organización aún no ha comenzado.", + "7e259037b6105500": "Ocurrió un problema inesperado.", + "cf69675106e4bf46": "Configuración", + "bdd7ee2107a3511e": { + "i": 1, + "c": "No se pudo cargar el rastreador de incorporación." + }, + "d83129a09341af16": [ + { + "i": 1, + "c": "Esperando Inicio" + }, + { + "i": 2, + "c": "No se ha iniciado ningún proceso de incorporación.", + "t": "p" + } + ], + "aaf571cedae7f727": [ + { + "i": 1, + "c": "Inicializando Estado" + }, + { + "i": 2, + "c": "Verificando el estado actual de incorporación...", + "t": "p" + } + ], + "7cb32bd872d0a718": [ + { + "i": 1, + "c": "Estado No Disponible" + }, + " ", + { + "i": 2, + "c": "No se pudo obtener el estado actual de incorporación.", + "t": "p" + } + ], + "2543628d6abae535": { + "i": 1, + "c": "Estamos configurando su organización. Esto puede tomar unos momentos." + }, + "460e43a75a49cf3a": [ + { + "i": 1, + "c": "Configuración Completada" + }, + { + "i": 2, + "c": "Su organización está lista.", + "t": "p" + } + ], + "c2c98dd21de6b44c": [ + { + "i": 1, + "c": "Estado Desconocido" + }, + { + "i": 2, + "c": [ + "Se recibió un estado no controlado: ", + { + "i": 3, + "k": "_gt_value_3", + "v": "v" + } + ], + "t": "p" + } + ], + "aac1baa16eee9388": { + "i": 1, + "c": [ + { + "i": 2, + "c": "¡Algo salió mal!" + }, + { + "i": 3, + "c": "Intentar de nuevo" + } + ] + }, + "f50b0cccdb56b748": "Tareas de Empleados", + "43bf30fd45b98d16": "Dispositivos de Empleados", + "c83fb5e64f34fb1a": "Resumen", + "81d0d1e78b196630": "General", + "49ab0247cf08bc51": "Portal de Confianza", + "8fcd54ccbde728bd": "Contexto", + "6cdc3f32f802d98b": "API", + "2139f3a924c1e3f9": { + "i": 1, + "c": "Cargando..." + }, + "3030c2f5c8b74986": "Cumplimiento en la Nube", + "2fb56915089207b1": "Pruebe y valide la seguridad de su infraestructura en la nube con pruebas automatizadas e informes.", + "54f1bd6e422167f1": "Gestión en la Nube", + "b1b2701c579cde29": "Conectar Nube", + "6e9619a12c1b7d1f": "¿Qué son las pruebas de cumplimiento en la nube?", + "a94b9ebe1c0b7df2": "Las pruebas de cumplimiento en la nube son verificaciones automatizadas que validan su entorno de nube contra las mejores prácticas de seguridad y estándares de cumplimiento.", + "221abd8fbf5e0ff2": "¿Por qué son importantes?", + "8a323db2d79b1452": "Ayudan a garantizar que su infraestructura en la nube sea segura, identifican configuraciones incorrectas y proporcionan evidencia para auditorías como SOC 2 e ISO 27001.", + "74208aa0a99c5e94": "¿Cómo empiezo?", + "65c722f981ac4d7e": "Conecte su proveedor de nube (AWS, GCP, Azure) en la página de Integraciones, y comenzaremos automáticamente a ejecutar pruebas y generar informes.", + "850705273bf8f8f0": "Discrepancia de organización", + "5767b3de281f2914": "Acceso denegado", + "83468cc3c415e656": "¿Qué marcos de cumplimiento necesita?", + "2fb479cfedf71ff8": "Seleccione los marcos de trabajo que se aplican a su negocio", + "2ed3f72fa5bd59b4": "¿Cuál es el nombre de su empresa?", + "f9b12e07d811daa7": "p. ej., Acme Inc.", + "68b398bbde98884b": "¿Cuál es el sitio web de su empresa?", + "b0195983bba2c1f6": "example.com", + "fd629eface765e5b": "Describe su empresa en unas pocas oraciones", + "6e0fe18f6d4a5028": "p. ej., Somos una empresa de software que desarrolla herramientas para que las empresas gestionen a sus empleados.", + "eb2c9533e5ce3d0d": "¿En qué industria opera su empresa?", + "6542a7eba5e7cfd2": "p. ej., SaaS", + "b003a30f37585307": "SaaS", + "b59fa0c75e198c6a": "FinTech", + "4d882c5aa86eee02": "Atención médica", + "d2668a105aa5a0ff": "Comercio electrónico", + "0ba4f8589e36ca76": "Educación", + "78344b60f407928e": "Otro", + "6d03085f5d52ac45": "¿Cuántos empleados tiene?", + "81612b13d1369c48": "p. ej., 11-50", + "69ea76204e65d7e2": "1-10", + "3abddd0e81bf5395": "11-50", + "c9da9b9fc508857f": "51-200", + "8a7ee8d5a8a8e26b": "201-500", + "22a6b84393e38a9a": "500+", + "4304f213930da3d8": "¿Qué dispositivos utilizan los miembros de su equipo?", + "af8d1e05f9f62a11": "p. ej., Laptops de la empresa", + "68d075529466d386": "Laptops proporcionadas por la empresa", + "76a65ca20b718bd0": "Laptops personales", + "7edc8196691dcef6": "Teléfonos de la empresa", + "b630fe86908c8b4e": "Teléfonos personales", + "6906cc67fff52eca": "Tabletas", + "e4239de2725cbaaf": "¿Cómo inician sesión los miembros de su equipo en las herramientas de trabajo?", + "e92b67416295e096": "p. ej., Google Workspace", + "d5b059f100aaa718": "Google Workspace", + "e5b23f2954cd9ba5": "Microsoft 365", + "c393f815e7b109ef": "Okta", + "8afb6397e4ba2c35": "Auth0", + "9ebeecd45cd6f477": "Correo electrónico/Contraseña", + "f39f5197138ae8a8": "¿Qué software utilizas?", + "aba1a9dd2af5ad69": "p. ej., Rippling", + "30a49a6185880451": "Rippling", + "e5db0944ce1865fd": "Gusto", + "ec1bb2356055bb7a": "Salesforce", + "639c662f4d1027ff": "HubSpot", + "3a2f1cc0b79c6e6d": "Slack", + "b3c7b7342f92fb6b": "Zoom", + "d99bfbc557fae743": "Notion", + "53b4ef4917317632": "Linear", + "5e6ec559838201ee": "Jira", + "8dc0fe8ee20d2129": "Confluence", + "28a45948473da7f8": "GitHub", + "91d3187705f84ca0": "GitLab", + "440c0c5050b2ca0f": "Figma", + "38f3ba97c2a10775": "Stripe", + "8637a479074e8bae": "¿Cómo trabaja su equipo?", + "a3c09f8b0d2f46d9": "p. ej., Remoto", + "a0ac713fe42e9726": "Completamente remoto", + "8f1146a8bd6f77a4": "Híbrido (oficina + remoto)", + "d437c7126c949aca": "Basado en oficina", + "15d169987dd8e7be": "¿Dónde aloja sus aplicaciones y datos?", + "733c8a624b10a18b": "p. ej., AWS", + "de6163810c24040e": "AWS", + "2a9203ffbfeb8ae4": "Google Cloud", + "1d7582c4d5ce2dbc": "Microsoft Azure", + "342e8805b3000ae6": "Heroku", + "df7a7366e8b8de84": "Vercel", + "d2e6b7fd699af7cf": "¿Qué tipos de datos manejan?", + "13be69efb27d7329": "p. ej., Información del cliente", + "1031ea9f5f405f70": "PII del Cliente", + "355e653719481e02": "Información de pago", + "54c67c1b205e67ee": "Datos de empleados", + "c1b295f4860e0404": "Registros de salud", + "0ee6edd73e7c1f8b": "Propiedad intelectual", + "1f85816d34cf4400": "Error al completar la incorporación", + "67a9f5d5f1bc5302": "Paso {stepIndex} de {totalSteps}", + "d894f6cb29f7a3ea": "Atrás", + "3261bdd5219e0d67": "Configurando...", + "baedcf3c90943f63": "Completar Configuración", + "7f8cb8174c6d288f": "Siguiente", + "f1745ecb9fcc5475": { + "i": 1, + "c": { + "i": 2, + "c": [ + { + "i": 3, + "c": { + "t": "path", + "i": 4 + } + }, + { + "i": 5, + "c": "La IA personaliza tu plan basándose en tus respuestas" + } + ] + } + }, + "8ffdbac510933fb1": "Por favor seleccione al menos un marco de trabajo", + "38cc8fb525636f32": "El nombre de la organización debe tener al menos 2 caracteres", + "bb73baacb058f369": "Por favor, ingrese una URL válida", + "b8b9932ac29ceec8": "Por favor, proporcione una descripción general breve de lo que hace su empresa", + "74008c49dd307563": "La descripción debe tener menos de 300 caracteres", + "8a12c1624aaf8b02": "Por favor seleccione su industria", + "821e74635406f83d": "Por favor, seleccione el tamaño de su equipo", + "114cfa56648e8d09": "Por favor seleccione el software que utiliza", + "c1177b795f58cf8f": "Por favor seleccione su infraestructura", + "fb60f25bc9674c75": "Por favor, seleccione los tipos de datos que maneja", + "8ac98bb3e5897e70": "Por favor seleccione los tipos de dispositivo", + "1531d3483c320ccd": "Por favor, seleccione los métodos de autenticación", + "10a18bf683920ec1": "Por favor seleccione el tipo de trabajo", + "45bc97dadbeaf5f6": "Configurar Su Organización | Comp AI", + "df0b9f31a64b209f": "No autorizado.", + "5d2fa23d2b62ab3a": "Error al crear la organización", + "54535efc190ac8a2": "Error al crear o actualizar la estructura organizacional", + "d6845667c3278af2": "No autenticado", + "32ad42fee0387dab": "Sesión de configuración inválida", + "9db16884c1c9d324": "Atrás", + "f6108edf33ab9122": "Finalizar", + "77b5f98f1f9a4ca7": "Siguiente", + "24799c7ffe906821": "{organizationName} es una empresa que...", + "3fccc23a353c649b": "Buscar o agregar personalizado (presiona Enter) • {placeholder}", + "12312d44ca61e89d": "Paso {current} de {total}", + "e20699daee977796": { + "i": 1, + "c": "La IA personaliza tu plan basándose en tus respuestas" + }, + "7f6a31a97deccd72": "Continuar a Planes", + "aa5e50f6e39e8baa": { + "i": 1, + "c": { + "i": 2, + "c": "Configurando espacio de trabajo...", + "d": { + "t": "b", + "b": { + "false": "Configuración del espacio de trabajo de IA en progreso... (2-7 minutos)", + "true": "La IA está trabajando en segundo plano (esto tomará de 2 a 7 minutos)" + } + } + } + }, + "2de12268251f2cb7": { + "i": 1, + "c": { + "i": 2, + "c": "Procesando...", + "d": { + "t": "b", + "b": { + "false": "Analizando su infraestructura y requisitos de cumplimiento", + "true": "Puedes continuar con seguridad - te notificaremos cuando todo esté listo" + } + } + } + }, + "fa4d7d98f102845e": "Error al aceptar la invitación", + "58a4dcfea8d4fa0e": "una organización", + "5becae6d1423db34": "Aceptando...", + "245da6e5a718031d": "Aceptar Invitación", + "1646b4de052320f1": { + "i": 1, + "c": "Has sido invitado a unirte" + }, + "74975d5107703da8": { + "i": 1, + "c": "Por favor, acepta la invitación para unirte a la organización." + }, + "5e74581cc1f0344a": "Tarea actualizada exitosamente", + "6d5c21ba339d1de7": "Algo salió mal, por favor inténtelo de nuevo.", + "92e3632c2611b200": "Seleccionar responsable", + "e6d9dd453ace53ca": "Seleccionar una fecha", + "4bc83c8380fe06ac": "Un título breve y descriptivo para la tarea.", + "ed82f606aecb96d5": "Proporcione una descripción detallada de lo que necesita hacerse.", + "309159a7f2b7260d": { + "i": 1, + "c": "Título de la Tarea" + }, + "df5bc46f3786b524": "Nombre del Control", + "9131eedb6ec4a42f": "Buscar un control...", + "de39b5fb3e51a5e8": "Buscar estado...", + "f705382c27dbd9c2": "Se agregó correctamente {count} marco{plural}", + "469a70952145255c": "Se produjo un error de validación", + "b71330c16213d5f9": "Error al agregar marcos de trabajo", + "781e9bcce5b9fcca": { + "i": 1, + "c": "Agregar Marcos de Trabajo" + }, + "07161279615778df": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": "Seleccione los marcos de cumplimiento para agregar a su organización.", + "false": "No hay nuevos marcos de trabajo disponibles para agregar en este momento." + } + } + } + }, + "5501eb474caf51fc": { + "i": 1, + "c": "Marcos de Trabajo Disponibles" + }, + "e5b8aa42c1792580": { + "i": 1, + "c": [ + { + "i": 2, + "k": "_gt_value_2", + "v": "v" + }, + "Agregar Seleccionados" + ] + }, + "e1fc30c95f246c48": { + "i": 1, + "c": "Todos los marcos de trabajo disponibles ya están habilitados en su organización." + }, + "daf080584b6c4e5f": { + "i": 1, + "c": "Cerrar" + }, + "44601907f9d10a04": { + "i": 1, + "c": [ + { + "i": 2 + }, + { + "i": 3, + "c": "Agregando marcos de trabajo..." + } + ] + }, + "a08738990422f423": "Conforme", + "3ad7aa90a39e55ed": "Casi Conforme", + "3e4bc10ab2e86ea9": "Requiere Atención", + "a68dd8483e37c3de": { + "i": 1, + "c": [ + { + "i": 2 + }, + { + "i": 3, + "c": "Progreso" + } + ] + }, + "7e4ee78f467179c4": { + "i": 1, + "c": [ + { + "i": 2, + "c": [ + { + "i": 3, + "k": "_gt_n_3", + "v": "n" + }, + " completadas" + ] + }, + { + "i": 4, + "c": [ + { + "i": 5, + "k": "_gt_n_5", + "v": "n" + }, + " activas" + ], + "t": "span" + }, + { + "i": 6, + "c": [ + { + "i": 7, + "k": "_gt_n_7", + "v": "n" + }, + " total" + ], + "t": "span" + } + ] + }, + "8d06621440a9c59e": { + "i": 1, + "c": [ + { + "i": 2 + }, + { + "i": 3, + "c": [ + "Actualizado ", + { + "i": 4, + "k": "_gt_value_4", + "v": "v" + } + ] + } + ] + }, + "1b7b3f38765c23cd": "Agregar Marco de Trabajo", + "41cdef95ef07d7ac": "Personas", + "ad8a4bea7e6ce80b": "Detalles del Empleado", + "762eae628c205496": "Política", + "76d1c8638f446afc": "Resumen de Políticas", + "684e119e379a1f6d": { + "i": 1, + "c": "No hay token disponible. Por favor, intente actualizar la página." + }, + "c2c1d0917484258d": "Crear Riesgo", + "3385709b69f39695": "Gestión de Riesgos", + "3d23fd91b0e98dee": "Identifique, evalúe y mitigue riesgos para proteger los activos de su organización y garantizar el cumplimiento.", + "5415898707ecd028": "Crear riesgo", + "49a66c603c0ba374": "¿Qué es la gestión de riesgos?", + "17db514baaaf3491": "La gestión de riesgos es el proceso de identificar, evaluar y controlar las amenazas al capital y las ganancias de una organización.", + "95511573114a0343": "¿Por qué es importante la gestión de riesgos?", + "7c2420c9171e1d34": "Ayuda a las organizaciones a proteger sus activos, garantizar la estabilidad y lograr sus objetivos minimizando las posibles interrupciones.", + "1d6c6f44857d9daf": "¿Cuáles son los pasos clave en la gestión de riesgos?", + "944708d4741e8acd": "Los pasos clave son la identificación de riesgos, el análisis de riesgos, la evaluación de riesgos, el tratamiento de riesgos, y el monitoreo y revisión de riesgos.", + "f233f44e0dcc1bae": "Resumen de Riesgos", + "d658405ba21e48f1": "Agregar Entrada", + "ad1b5391919e9f78": "Error al actualizar la tarea", + "4a9496c30ba02e3b": "Error al actualizar el orden de las tareas", + "e760456be40a26ab": "Soltar tarea aquí", + "edaa0be7951d26a9": "15 abr", + "d8e24415e55bbff1": "Gestión de Proveedores", + "fcdac99e065446e7": "Gestione sus proveedores y asegure que su organización esté protegida.", + "efa02eb8a3f21d9e": "Agregar proveedor", + "8cb168ac0da534ac": "¿Qué es la gestión de proveedores?", + "4aed173b94f00d5a": "La gestión de proveedores es el proceso de administrar y controlar las relaciones y acuerdos con proveedores externos de bienes y servicios.", + "0a26ec0337f419d5": "¿Por qué es importante la gestión de proveedores?", + "e28738b0d97ec8d1": "Ayuda a garantizar que obtenga el máximo valor de sus proveedores, al mismo tiempo que minimiza los riesgos y mantiene el cumplimiento normativo.", + "acaeb06361f69866": "¿Cuáles son los pasos clave en la gestión de proveedores?", + "cd34404757675bd8": "Los pasos clave incluyen la selección de proveedores, negociación de contratos, monitoreo del desempeño, gestión de riesgos y gestión de relaciones.", + "084c640a56dbc2f5": "Debe ser una URL válida", + "e98f9fa86e22a4ca": "Error al crear el proveedor", + "af6c2787e681b839": "Error al buscar proveedores globales", + "833d725acc309572": "Gestione sus proveedores y asegure que la cadena de suministro de su organización sea segura y cumpla con las normativas.", + "04e9652c409230e5": "Proveedor creado exitosamente", + "1e4f18781f51e533": "Buscar o ingresar nombre del proveedor...", + "c34ec213fbfe6e8e": "Ingrese una descripción para el proveedor...", + "688ace37fa12f7b5": "Seleccionar una categoría...", + "8a0229de388b5c42": "Seleccionar un estado...", + "d53abb5663682a3c": "Detalles del Proveedor", + "af7d87845636137a": "Nombre del Proveedor", + "5cd89cfd9ef6a613": "Cargando...", + "e84c63c6805cf289": "Sugerencias", + "27c827064768bf37": [ + "Crear \"", + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + }, + "\"" + ], + "52ee190cc093eee9": "Sitio web", + "ab8222ce9132da09": "Descripción", + "d9da39b1e9b26f71": "Categoría", + "08247dd39b0c85ff": "Asignado", + "bda71051f48de641": "Crear Proveedor", + "a1f2a4dc17d26b98": { + "i": 1, + "c": "Incorporación No Encontrada" + }, + "73ed17cbadf2bbde": { + "i": 1, + "c": "No se encontró ningún proceso de incorporación para esta organización." + }, + "9114b5b0318105b8": { + "i": 1, + "c": "Incorporación en progreso" + }, + "087f710f6a3b5219": { + "i": 1, + "c": "Esto puede tomar unos minutos." + }, + "f7f2ca3ad83c9874": "La IA está analizando sus necesidades de cumplimiento", + "f43d6c285a35ffbe": "Personalizando su marco de seguridad", + "3319a243ebfbd937": "Construyendo su hoja de ruta de cumplimiento", + "b57715f5c68049fe": "Optimizando para los requisitos de su industria", + "8867e24157f4df51": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": "Contáctanos para actualizar", + "false": { + "i": 3, + "c": [ + "Aprobemos ", + { + "i": 4, + "k": "_gt_value_4", + "v": "v" + } + ] + } + } + } + } + }, + "01fdd958afcf2cd0": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "false": "Una llamada rápida de 20 minutos con nuestro equipo para entender sus necesidades de cumplimiento y aprobar el acceso de su organización.", + "true": "Una llamada rápida de 20 minutos con nuestro equipo para entender sus necesidades de cumplimiento y actualizar su plan." + } + } + } + }, + "4fbe9105a34a656e": { + "i": 1, + "c": [ + { + "i": 2, + "d": { + "t": "b", + "b": { + "true": "Reservar una Llamada", + "false": "Reserva tu Demo" + } + } + }, + " ", + { + "i": 3 + } + ] + }, + "b4bcc883251843a2": { + "i": 1, + "c": "¿Ya tuviste una demostración? Solicita a tu punto de contacto que active tu cuenta." + }, + "95a7958453487882": "¡Control eliminado! Redirigiendo a la lista de controles...", + "ad300274f72c5971": "Error al eliminar el control.", + "c4d1b302af837104": "Eliminando...", + "da49b8271b9dffa0": "Eliminar", + "2f10d97acacce7cc": { + "i": 1, + "c": "Eliminar Control" + }, + "1a631abda5fe31a2": { + "i": 1, + "c": "¿Está seguro de que desea eliminar este control? Esta acción no se puede deshacer." + }, + "3070a60b00ad489a": "Nombre", + "096224a4f82a6fa0": "Creado el", + "431e4fefe760f1fd": "Buscar requisitos...", + "4730f0952f7a36fe": { + "i": 1, + "c": "Requisitos" + }, + "e17cd2600d6bf447": { + "i": 1, + "c": "Políticas" + }, + "9a17d0f84193f9f9": { + "i": 1, + "c": "Tareas" + }, + "52abe19223865f18": { + "i": 1, + "c": "Dominio" + }, + "45b60762299b6873": "Buscar tareas...", + "0229a1299560addc": "Instancia de marco no encontrada", + "a0754deed725bac5": "Error al eliminar la instancia del marco de trabajo", + "1858a1e6f9e87941": "¡Marco de trabajo eliminado! Redirigiendo a la lista de marcos de trabajo...", + "fceb50a9d97c829c": "Error al eliminar el marco de trabajo.", + "ea8e47a73ceff762": "Eliminar Marco de Trabajo", + "192936817313e22a": "¿Está seguro de que desea eliminar este marco de trabajo? Esta acción no se puede deshacer.", + "e8f4f303cea351b7": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Eliminando..." + ] + }, + "758907dc7a83633e": "Progreso de Cumplimiento", + "958749c7dd56e65a": "% completado", + "b66fd30986031831": "completado", + "6ba37274c0fe4a31": "restante", + "6902d6425b5bc22d": "total", + "65401fd9c2fc1158": "Estado del Control", + "04cfc0a0f896cfa6": "Completado", + "b901d8a75fdd232b": "En Progreso", + "7853bd9e23f25bb6": "Total", + "04475595e6bffc77": "Nombre del Requisito", + "992b8be3ef4a1e89": "Requisitos", + "da2ac3f4150be0d1": "Empleado no encontrado", + "29f28b94285537b4": "No está autorizado para ver este empleado", + "96bf710e86e123c5": "Se produjo un error inesperado", + "9605573953e7fa07": "ID de organización no encontrado", + "55d24da93ca92aef": "Entrada inválida", + "3d0b8b23bb19ee03": "La dirección de correo electrónico ya está en uso.", + "edf44df5be41e0a0": "Administrador", + "7c905f5c373b9d01": "Gobernanza", + "05627a3ce4ab4d79": "RRHH", + "995a54376f33210d": "TI", + "4722675ddbbe78d5": "Gestión de Servicios de TI", + "85f91c5713d08f3f": "Gestión de Calidad", + "7579efd3af57fd19": "Departamento actualizado exitosamente", + "21b4fcf54d1fe43f": "Error al actualizar el departamento", + "0bc3abd5590c867d": "Seleccionar departamento", + "e053ca7a2cd892db": { + "i": 1, + "c": "Nombre" + }, + "0ef9464dc819731b": { + "i": 1, + "c": "Correo electrónico" + }, + "706a44da30da06fa": "Estado del empleado actualizado exitosamente", + "ef8b54e1a1f5c9ef": "Error al actualizar el estado del empleado", + "fa07b2258f948afd": "Seleccionar estado", + "1da2bce8da9dc4c8": "Detalles del empleado actualizados exitosamente", + "02beae082fdfdb4e": "Error al actualizar los detalles del empleado", + "91cca38108b84e60": "No hay cambios que guardar", + "4abb13af9b3921ec": "Detalles del Empleado", + "196cea3ec2bc91aa": { + "i": 1, + "c": "Gestionar información de empleados y asignación de departamentos" + }, + "665b4f6b6e9904ce": "'s Políticas", + "b9efda236d7237f7": { + "i": 1, + "c": "Tareas de Empleados" + }, + "3f2d7004255baa67": { + "i": 1, + "c": "Ver y gestionar las tareas de los empleados y su estado" + }, + "f389dd4aaef1c1c2": "Políticas", + "56049f09fd5af61b": "Videos de Capacitación", + "cdf1acc35d738b49": "Dispositivo", + "6171dd941937f9b9": { + "i": 1, + "c": "No hay políticas que requieran firma." + }, + "0e7b83c61d4f8cde": { + "i": 1, + "c": "No hay videos de capacitación requeridos para ver." + }, + "19f7dc6f1b1743e2": { + "i": 1, + "c": [ + "Completado - ", + { + "i": 2, + "k": "_gt_date_2", + "v": "d" + } + ] + }, + "77f239944bdf5f58": { + "i": 1, + "c": "Aprobado" + }, + "580543c2d219a989": { + "i": 1, + "c": "Falla" + }, + "5672ac39095692d7": { + "i": 1, + "c": "No se encontró ningún dispositivo." + }, + "705c8aa755045b82": "Error al agregar empleado", + "11ccfacb48c741b7": "Autenticación requerida.", + "31c30ea43a1b15c2": "Archivo CSV no proporcionado o inválido.", + "b46040d0ab0b883a": "El tamaño del archivo CSV excede el límite de 5MB.", + "35819278bb7b3f97": "Tipo de archivo inválido. Solo se permite CSV.", + "e230e8c2516a0448": "El archivo CSV está vacío o contiene solo un encabezado.", + "a57737a31eaccf09": "Fila CSV: {email}, {role}", + "ef726f53ffd7c38d": "[missing]", + "12c2ddc621acda04": "Rol especificado inválido: {role}", + "5691d29b90421516": "Error en el procesamiento", + "4ef4780ecbf5aedb": "Datos de invitación manual no proporcionados.", + "27f722b76fa5e8e8": "Formato inválido para los datos de invitación manual.", + "0565e178454756cb": "No se enviaron invitaciones manuales.", + "fb1b36967048d9bd": "Error en la invitación", + "a13c62c26c5712e4": "Tipo de envío inválido.", + "171caeda50580ba0": "Todas las invitaciones fallaron.", + "54e5b7f520cfdda2": "Ocurrió un error inesperado del servidor al procesar las invitaciones.", + "3011a5c0dfc1151e": "No tienes permisos para eliminar miembros", + "b58b641907c274bd": "Miembro no encontrado en esta organización", + "3ea297e7fb1c4340": "No se puede eliminar al propietario de la organización", + "1b579c911ea11d62": "No puedes eliminarte a ti mismo de la organización", + "7c8b772c1a8a1828": "Error al eliminar miembro", + "7e8142c4f846be70": "Invitación no encontrada o ya aceptada", + "2bd3738ccdb61d40": "Error al revocar la invitación", + "fc5ef3d2a53ab94b": "No tienes permisos para actualizar los roles de los miembros", + "082fad64b81d71c3": "No se pueden cambiar los roles para el propietario de la organización.", + "4fc707159d98591e": "Error al actualizar el/los rol(es) del miembro", + "88fd5400a830ee7e": "Por favor, agregue al menos un miembro para invitar.", + "c2819a241f8b062a": "Por favor, seleccione al menos un rol para: {emails}", + "e7fc4a7a6b0af46c": "Se invitó exitosamente a {count} miembro(s).", + "6fa0293f3e53c4a5": "Error al invitar {count} miembro(s): {emails}", + "380de5f5e624547f": "Se requiere un archivo CSV válido.", + "9874fe4ddcdafd82": "El archivo debe ser un CSV.", + "ae0dce5c990ec64c": "El tamaño del archivo debe ser menor a 5MB.", + "652fbe55154f9887": "Formato CSV inválido. La primera fila debe incluir las columnas 'email' y 'role'.", + "e01e2a1ee232d656": "El CSV debe contener las columnas 'email' y 'role'.", + "f9390534897da70d": "El archivo CSV no contiene ninguna fila de datos.", + "e80ae0b3c233ad1c": "Rol(es) inválido(s): {roles}. Debe ser uno de: {validRoles}", + "77a6df47ea53202e": "Error al analizar el archivo CSV. Por favor, verifique el formato.", + "3086e2190f3835f0": "Ocurrió un error inesperado al procesar las invitaciones.", + "d4fe5f33acefa47a": "Ingrese dirección de correo electrónico", + "1153b531fd2dcd5e": "Seleccionar un rol", + "1025836e0c0b9576": "Eliminar invitación", + "135ce1648dc76499": "Ningún archivo seleccionado", + "0ab08f746265bc60": "Agregando Empleado...", + "853f2aeef0f1de98": "Invitar", + "7d4e0a8ae4fb8d67": "Agregar Usuario", + "7338cdd78153f034": "Agregar un empleado a su organización.", + "503855211a14e0ae": "Manual", + "b29f123c54cebb95": "CSV", + "effdcd633af15789": "Correo electrónico", + "882899ce03a4baa3": "Rol", + "f1e9d66591c78a34": "Agregar Otro", + "ea561bc7153d2e0e": "Archivo CSV", + "5006be3f38fec89d": "Elegir archivo", + "025cb89517dc7f04": "Sube un archivo CSV con las columnas 'email' y 'role'. Usa la barra vertical (|) para separar múltiples roles (ej., employee|admin).", + "2d97dc10cf7f8d3b": "Descargar plantilla CSV", + "6502097c2be1269b": "??", + "07e9a4c88da946a2": "Ver Perfil", + "e97e409bef7c68db": "Propietario", + "95b50799111c5781": "Auditor", + "736cee0ff2dc22bf": "Empleado", + "99a1e3accbba4a7e": "Editar Roles", + "4599a44a20334d8c": "Editar Roles de Miembros", + "3a8ffc4722287317": "Roles", + "21df418b12667c52": "Eliminar Miembro", + "35e65129e5dfd4f7": "Eliminar Miembro del Equipo", + "2c5674665579a329": "Eliminar", + "5b53f04b14512a66": [ + "Cambiar roles para ", + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + } + ], + "c495bd7adae083cf": { + "i": 1, + "c": "El rol de propietario no se puede eliminar." + }, + "0638680d454714b7": { + "i": 1, + "c": "Los miembros deben tener al menos un rol." + }, + "32cf62366ecd3b38": [ + "¿Está seguro de que desea eliminar a ", + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + }, + "? Ya no tendrá acceso a esta organización." + ], + "0150cbf873be7714": "Seleccionar rol(es)", + "19296c088d2032f0": "Puede gestionar usuarios, políticas, tareas y configuraciones, y eliminar la organización.", + "0ce4041ea6bc76b8": "Puede gestionar usuarios, políticas, tareas y configuraciones.", + "c68efed9abce3d18": "Acceso de solo lectura para verificaciones de cumplimiento.", + "74e80634d6bd26d5": "Puede firmar políticas y completar capacitación.", + "3d9d54ce106919e8": "No se encontraron resultados", + "469aae33a5a5bc74": "Bloqueado", + "374dac09151d4578": "Pendiente", + "957a85ca76596fcd": "Abrir menú", + "d06b708e50a8ef11": "Cancelar Invitación", + "80c084fe3d0ae4a5": [ + "¿Está seguro de que desea cancelar la invitación para ", + { + "i": 1, + "k": "_gt_value_1", + "v": "v" + }, + "?" + ], + "fc55702435f9adce": "Esta acción no se puede deshacer.", + "e74139c17fab1fa1": "Confirmar", + "84a40672baefdbd1": "Error al agregar usuario", + "491632b005f7b437": "ha sido eliminado de la organización", + "7cbe8f027db4796f": "El rol de Propietario no se puede eliminar.", + "9a52169a47e5813a": "Por favor, seleccione al menos un rol.", + "730a9463d948bf5c": "Roles de miembros actualizados exitosamente.", + "d88b7a4ffd327790": "Error al actualizar los roles de miembro", + "7ab50cb8a90419cd": "Buscar personas...", + "0451e2efa0e95403": "Todos los Roles", + "71676a695ec33aed": "Todos los Roles", + "5e0c0354a6c8b204": "Propietario", + "83d5f21bd2e8f44a": "Administrador", + "4cd6aabc43eb8a11": "Auditor", + "bf833ea7594cf25a": "Empleado", + "8f25fe65b4e2d152": "Aún no hay empleados", + "7006be19c2f64ed1": "Comience invitando a su primer miembro del equipo.", + "ba0192fdd891269f": "Completadas: {count}", + "1ae7bdd9e93a8ad9": "Incompleto: {count}", + "7d318286377920bd": "Finalización de Tareas de Empleados", + "c1987d3d0923fc6f": "No hay datos de empleados disponibles", + "4216d314e55d71ce": "No hay tareas disponibles para completar", + "9c1793838e258d8a": "tareas", + "496f4b9a48eba645": "Completado", + "d00ce138ed5abfab": "No Completado", + "79ee400a066192a6": "No Conforme", + "9cc48e5c9ee46026": "Dispositivos", + "cf19ed14b2b34058": { + "i": 1, + "c": "Cumplimiento de Dispositivos" + }, + "a4d9225d52d1bdad": { + "i": 1, + "c": "No hay datos de dispositivos disponibles. Por favor, asegúrese de que sus empleados accedan al portal e instalen el agente de dispositivo." + }, + "8907ed1203d36cec": "Nombre del Dispositivo", + "8ff705813edac77c": "Es Conforme", + "89595e69cb926a4c": "Políticas de {name}", + "e511a672455032e6": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Atrás" + ] + }, + "3128dd9c0f40f364": { + "i": 1, + "c": "No se encontraron políticas para este dispositivo." + }, + "4171d5c4fb58fcd3": { + "i": 1, + "c": "Políticas por Responsable" + }, + "74e01763fc0c9940": { + "i": 1, + "c": "Distribución" + }, + "ca6ff36c8d5ff187": { + "i": 1, + "c": "No hay políticas asignadas a usuarios" + }, + "49b85d58db4c9123": { + "i": 1, + "c": [ + "Superior: ", + { + "i": 2, + "k": "_gt_value_2", + "v": "v" + } + ] + }, + "a6edc1e5fa4fec4e": { + "i": 1, + "c": "Cantidad de Políticas" + }, + "65c1841897037bc0": "Recuento", + "c71cec3d156b0adc": { + "i": 1, + "c": [ + "Cantidad: ", + { + "i": 2, + "c": { + "i": 3, + "k": "_gt_value_3", + "v": "v" + } + } + ] + }, + "97e9a2ebdc3912e5": { + "i": 1, + "c": "Políticas por Estado" + }, + "c8ec0af9da595356": { + "i": 1, + "c": "Resumen" + }, + "e7230ceab152cf77": { + "i": 1, + "c": "No hay datos de políticas disponibles" + }, + "95a7d24d35770dd3": { + "i": 1, + "c": [ + "Máximo: ", + { + "i": 2, + "k": "_gt_value_2", + "v": "v" + } + ] + }, + "2057c0147fe1aa60": "Agregar comentario o razón opcional (se agregará como comentario)", + "4dc2fec97b3c2d75": "Política archivada exitosamente", + "deef48194e077375": "Política restaurada exitosamente", + "9e8f2e41dc424ee0": "¿Está seguro de que desea restaurar esta política?", + "26cc0877673f89f1": "¿Está seguro de que desea archivar esta política?", + "90e19ab2fba5da15": "Restaurar", + "2a546119bf3879ee": "Archivar", + "a18b109d050bd5af": "Restaurar Política", + "c8faad822a32784b": "Archivar Política", + "9cd0be0c89f32a9f": "Control: {controlName} desvinculado exitosamente de la política {policyId}", + "f1fb56b9deb392b8": "Error al desvincular el control", + "2dfec7cc55776188": { + "i": 1, + "c": "Confirmar Desvinculación" + }, + "d469f6fe80d55209": { + "i": 1, + "c": [ + "¿Está seguro de que desea desvincular ", + { + "i": 2, + "c": { + "i": 3, + "k": "_gt_value_3", + "v": "v" + } + }, + " de esta política? Puede vincularlo nuevamente más tarde." + ] + }, + "c2f6f9b3e3e43357": { + "i": 1, + "c": "Desasignar" + }, + "a7768d6211c8aa39": "Los controles {controlNames} se mapearon exitosamente a la política {policyId}", + "81dfee79a961b703": "Error al mapear controles", + "2828fc07c61a26bb": "Buscar o seleccionar controles...", + "09fcc17a28a0da3a": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Vincular Controles" + ] + }, + "93cb69b30dbf2d57": { + "i": 1, + "c": "Vincular Nuevos Controles" + }, + "bb0dd05e7c9fd180": { + "i": 1, + "c": "Seleccione los controles que desea vincular a esta política" + }, + "32449c016322cf37": { + "i": 1, + "c": "Mapear" + }, + "8b400aae6490f6a8": "Controles mapeados exitosamente", + "4281d2b727030f4e": "Controles desmapeados exitosamente", + "201f46ce6bc6ae5c": "Error al desasignar control", + "24eb06ffb1e875d1": "Error al actualizar los controles", + "4d1b2dd36a850c76": "Controles del Mapa", + "56ea37c9068c9dec": "Mapee los controles que son relevantes para esta política.", + "fbfd7cc76af0d1dd": "Buscar controles...", + "c5437eb43ede54ba": "¡Política eliminada! Redirigiendo a la lista de políticas...", + "7a2c947462260b1f": "Error al eliminar la política.", + "76cfb2deb26bfbe9": "Eliminar Política", + "8a63f70e3dc315d9": "¿Está seguro de que desea eliminar esta política? Esta acción no se puede deshacer.", + "f245e79bcfab1f84": "¡Cambios de política denegados!", + "c0853b2ec9df8f4b": "Error al denegar los cambios de política.", + "cc9a68c6954a09b1": "¡Cambios de política aceptados y publicados!", + "815385a22799580a": "Error al aceptar los cambios de política.", + "ad0a13150160c1e5": "Rechazar Cambios", + "ac48cc297029db41": "Aprobar", + "034b7fe9d2331125": "Editar política", + "5af1694d385ce51d": "Restaurar política", + "8b633b46e57db958": "Archivar política", + "b0a332f4cf888f64": "Aprobar Cambios de Políticas", + "7dbc6122a36cd10b": "¿Está seguro de que desea aprobar estos cambios de política? Opcionalmente puede agregar un comentario que será visible en el historial de la política.", + "89dceedb1ff1f474": "Denegar Cambios de Políticas", + "7e18ce8955f879b4": "¿Está seguro de que desea rechazar estos cambios de política? Opcionalmente puede agregar un comentario explicando su decisión que será visible en el historial de la política.", + "af63f05ed2f5eeb3": "Denegar", + "b5e58d41522e6f8d": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "false": { + "i": 3, + "c": "Pendiente de Aprobación" + }, + "true": { + "i": 3, + "c": "Acción Requerida de Su Parte" + } + } + } + } + }, + "c7cdcd8295ffc276": { + "i": 1, + "c": [ + "Esta política está esperando la aprobación de ", + { + "i": 2, + "c": { + "i": 3, + "d": { + "t": "b", + "b": { + "true": { + "i": 4, + "c": "usted" + }, + "false": { + "i": 4, + "c": [ + { + "i": 5, + "k": "_gt_value_5", + "v": "v" + }, + " (", + { + "i": 6, + "k": "_gt_value_6", + "v": "v" + }, + ")" + ] + } + } + } + } + }, + "." + ] + }, + "e2a10b31faf09c9e": { + "i": 1, + "d": { + "t": "b", + "b": { + "true": { + "i": 2, + "c": "Por favor, revise los detalles y apruebe o rechace los cambios." + }, + "false": { + "i": 2, + "c": " Todos los campos están deshabilitados hasta que se ejecute la política." + } + } + } + }, + "09a58159a34fb404": { + "i": 1, + "c": "Esta política está archivada" + }, + "e672216bacb316fa": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": { + "i": 3, + "c": [ + "Archivado el ", + { + "i": 4, + "k": "_gt_value_4", + "v": "v" + } + ] + }, + "false": { + "i": 3 + } + } + } + } + }, + "209597041689fa1a": "Actualizar Política", + "cfd9df270d1ab870": "Actualizar detalles, contenido y metadatos de la política.", + "2d789753cc9093ef": "Actividad Reciente", + "0fb870258a5c11fa": { + "i": 1, + "c": "Cambios:" + }, + "0a0f1759fd7d55b5": { + "i": 1, + "c": [ + { + "i": 2 + }, + { + "i": 3, + "c": "Sin actividad reciente" + }, + { + "i": 4, + "c": "La actividad aparecerá aquí cuando se realicen cambios en esta política", + "t": "p" + } + ] + }, + "4a5a2d169cfa2e66": "Enviar para Aprobación", + "35eeeaac38859c2a": "Por favor, seleccione un aprobador para esta política.", + "e4c3331d3f6ca23f": "Confirmar y Enviar", + "67024d5cc3a2b238": "¡Política enviada para aprobación exitosamente!", + "c371b99a5cf1a514": "Error al enviar la política para aprobación.", + "e5db1ee627ea339d": "Se requiere un aprobador.", + "4f5d2ab132df784a": "Frecuencia de Revisión", + "6effd4a13cf948bc": "Seleccionar frecuencia de revisión", + "2749541e66817f9a": "Fecha de Revisión", + "d95420a0657189f5": "Seleccionar fecha de revisión", + "2aa1601a9da01659": "Confirmar Fecha", + "2008a148875696d1": "Requisito de Firma del Empleado", + "9314145c13cdff15": "Obligatorio", + "07082dd600bd971b": "No Requerido", + "c14dd32508155b52": "Buscar una política...", + "c898e6ae70f171b4": "Búsqueda actualizada por última vez...", + "8c21197a61c318a8": "Crear Política", + "a67aaf8e38d9b253": "Error al crear la clave API", + "18e2e29c59c39725": "Clave API copiada al portapapeles", + "afc8fb892e52f932": "Error", + "98bad2483621d390": "Clave de API", + "2cd247d0cc8a8ed9": "Ingrese un nombre para esta clave de API", + "18c1ab9df1956453": "Vencimiento", + "ef351828ad652b83": "Seleccionar vencimiento", + "d15f079ebaf277ae": "Nunca", + "53493056306708cf": "30 días", + "c719359d18d37ad3": "90 días", + "6f8d4d7274376cf6": "1 año", + "991114b9d655cc85": "Esta clave solo se mostrará una vez. Asegúrese de copiarla ahora.", + "463bb4910ba81579": "Clave de API Creada", + "bcb1fbddbee9eff8": "Nueva Clave de API", + "ea31e51a6647239b": "Su clave API ha sido creada. Asegúrese de copiarla ahora ya que no podrá verla nuevamente.", + "db360fd0a3ceb743": "Crear una nueva clave de API para acceso programático a los datos de su organización.", + "f3f36b8e1ef0fe71": "Agregar Entrada de Contexto", + "e28b38fdaf763781": "Proporciona contexto adicional a Comp AI sobre tu organización.", + "2c9aff0ef152c355": "Entrada de contexto actualizada", + "9c2bd23d4bdeb3b4": "Entrada de contexto creada", + "a380d1dccfc9f3b1": "Algo salió mal", + "2d213598fd05b6ed": "¿Cuál es la misión de la empresa?", + "8f2d31f8441ae9bf": "Nuestra misión es brindar el mejor servicio posible a nuestros clientes.", + "da28c1c64c40fe26": "Entrada de Contexto", + "6cb24609520cffac": "Pregunta", + "74a70575448e22cd": "Respuesta", + "c0c5b3c974c59ef0": { + "i": 1, + "c": "Crear", + "d": { + "t": "b", + "b": { + "true": { + "i": 2, + "c": "Actualizar" + }, + "false": { + "i": 2, + "c": "Crear" + } + } + } + }, + "84df5207349bf367": { + "i": 1, + "c": "Contexto" + }, + "78fcb8d22c79db46": { + "i": 1, + "c": "Puedes agregar contexto a la plataforma Comp AI para ayudarla a comprender mejor tu organización/procesos." + }, + "8b4eabdfc58b5fa0": "Editar", + "b8854093e1ea172c": "Entrada de contexto eliminada", + "7d00b78c04cbf13e": { + "i": 1, + "c": "Entradas de Contexto" + }, + "239fae3b9217ef4b": { + "i": 1, + "c": "Agregue, edite o elimine entradas de contexto para su organización." + }, + "4e91597e118b90b0": { + "i": 1, + "c": [ + { + "i": 2 + }, + "Agregar Entrada" + ] + }, + "59145c615002ac9a": { + "i": 1, + "c": "Agregar Nueva Entrada" + }, + "c38590681d61f395": { + "i": 1, + "c": "Crear una nueva entrada de contexto" + }, + "1ab7b22099bf8346": { + "i": 1, + "c": "Aún no hay entradas de contexto" + }, + "c7cb8dd8116ca2ec": { + "i": 1, + "c": "Editar Entrada" + }, + "f07e51a11732f730": { + "i": 1, + "c": "Actualiza tu entrada de contexto" + }, + "db387260d0b1fd65": { + "i": 1, + "c": "¿Está seguro?" + }, + "69aa87f10b8e94fb": { + "i": 1, + "c": "Esta acción no se puede deshacer." + }, + "47ae9fafd2a74c0c": "La verificación del registro DNS falló, verifique que los registros sean válidos o intente nuevamente más tarde.", + "e609240795029b8d": "Error al verificar los registros DNS. Asegúrese de que tanto los registros CNAME como TXT estén configurados correctamente, o espere unos minutos e inténtelo de nuevo.", + "b36eefd49878b594": "El ID del proyecto de Vercel no está configurado.", + "b45d1d5ff18c8d3b": "El dominio ya está siendo utilizado por otra organización", + "0e8cb1c634ba3bb8": "Error al actualizar el dominio personalizado", + "314f351393f73336": "Error al actualizar la configuración del portal de confianza", + "56ba3cbad0eddcbe": "Actualización de dominio personalizado enviada, por favor verifique sus registros DNS.", + "02753d12f011fbbc": "Error al actualizar el dominio personalizado.", + "55013f5501b9ce4b": "{type} copiado al portapapeles", + "f171c153f7dfdb0e": "trust.example.com", + "f045efc0656c1a9f": "Valor", + "1eef65ec03649821": [ + { + "i": 1, + "c": "Configurar Dominio Personalizado" + }, + { + "i": 2, + "c": "Puedes usar un dominio personalizado (como trust.example.com) para personalizar la marca de tu portal de confianza." + } + ], + "bbc58bed17606f8f": "Dominio Personalizado", + "002915c4e8dffc7e": { + "i": 1, + "c": "El dominio está verificado" + }, + "1e302a6e9966ab57": { + "i": 1, + "c": "El dominio aún no está verificado" + }, + "d81797013ad75aa5": "Verificar registro DNS", + "d462eb430631e76d": { + "i": 1, + "c": "Verificado" + }, + "2bc786a3e42f5520": { + "i": 1, + "c": "Tipo" + }, + "90ce6a40bc073f6c": { + "i": 1, + "c": "Valor" + }, + "8c1753cbe491136d": { + "i": 1, + "c": "Tipo:" + }, + "5045e704be73bb8b": { + "i": 1, + "c": "Nombre:" + }, + "dc78404d73f37576": { + "i": 1, + "c": "Valor:" + }, + "6ab9c102b1981fa8": { + "i": 1, + "c": "Configure un dominio personalizado para su portal de confianza." + }, + "1199aec0e16713bb": "Estado del portal de confianza actualizado", + "94e0e520c1ada9e0": "Error al actualizar el estado del portal de confianza", + "39250cf5c5e82116": "mi-org", + "f82c0ee66c808f7b": "Verificando disponibilidad...", + "c535df83edb370e4": "¡Esta URL está disponible!", + "016ef46f759d6287": "Esta URL ya está en uso.", + "9d40a0f05fd025a7": "contact@example.com", + "5cedf492c05531d2": "Un marco de cumplimiento enfocado en la seguridad, disponibilidad y confidencialidad de los datos.", + "4261c3bcb77da053": "Estado de SOC 2 actualizado", + "bf23afdcd60c6bda": "Error al actualizar el estado de SOC 2", + "bd28556c668275eb": "Un estándar internacional para la gestión de sistemas de seguridad de la información.", + "9492787876f15d73": "Estado de ISO 27001 actualizado", + "0ca88698b3832828": "Error al actualizar el estado de ISO 27001", + "30f5692d08629d3a": "Una regulación europea que gobierna la protección de datos personales y la privacidad del usuario.", + "7db16e49bfe473bc": "Estado de GDPR actualizado", + "4ae21dd1f976b3b3": "Error al actualizar el estado de GDPR", + "885ae33187be0420": "Una regulación estadounidense que protege la información confidencial de salud del paciente y los datos médicos.", + "d495d343f27c0394": "Estado de HIPAA actualizado", + "f4475aa5dabfa9a1": "Error al actualizar el estado de HIPAA", + "bc19f807949b8b85": "Portal de Confianza", + "ca5344fe4aa94b6c": "Cree un portal de confianza público para su organización.", + "29e50c0230810f14": "Configuración del Portal de Confianza", + "d8ae2c2dd6235793": "URL personalizada", + "a4da57c04cc78961": "Correo Electrónico de Contacto", + "02f843de63269080": "Marcos de Cumplimiento", + "a91a70a571a67a3a": "Comparte los marcos de cumplimiento con los que tu organización cumple o hacia los que está trabajando.", + "8f34226efbd18a89": "Iniciado", + "eee8c7213077d5c2": "Conforme", + "1c1ddbbd4567f2ab": "Deshabilitado", + "b42f6ad7f25a1114": "Eliminar archivo adjunto {name}", + "d912a5e4e58ab905": { + "i": 1, + "c": { + "i": 2, + "c": { + "i": 3 + }, + "d": { + "t": "b", + "b": { + "true": "Esta acción no se puede deshacer. Esto eliminará permanentemente el archivo adjunto.", + "false": "Esto eliminará el archivo adjunto de tu lista pendiente. No se incluirá cuando envíes." + } + } + } + }, + "45c063246b34dde6": "No se encontraron resultados.", + "bd8e87512fd9bdf2": "Sin asignar", + "4e582f82f0d2be8a": "El archivo \"{filename}\" excede el límite de {sizeLimit}MB.", + "b2bc5e7fff4fb566": "Archivo \"{filename}\" cargado exitosamente.", + "25a2e65b37989d4a": "Error al cargar el archivo. Por favor, inténtelo de nuevo.", + "4979e41a8b2447f7": "Error al cargar {filename}: {errorMessage}", + "cd6f16d40f2c75aa": "Error al obtener la URL de descarga. Por favor, inténtelo de nuevo.", + "34c1980f6da9ac88": "Archivo adjunto eliminado exitosamente.", + "b3559ce71529fccf": "Error al eliminar el archivo adjunto. Por favor, inténtelo de nuevo.", + "5383bd14ef4ef0ec": "Título de la Tarea", + "6ba706fcdeb1ed9f": "Agregar descripción...", + "18a25ea6621be8f1": { + "i": 1, + "c": "Archivos adjuntos" + }, + "6952fb82a00934a9": { + "i": 1, + "c": "Error al cargar los archivos adjuntos. Por favor, inténtelo de nuevo." + }, + "92bd6ef0f835efd3": { + "i": 1, + "c": [ + "Aún no hay archivos adjuntos. Haz clic en el ícono ", + { + "i": 2 + }, + " de arriba para agregar uno." + ] + }, + "07f2f442713ef1d3": { + "i": 1, + "c": [ + { + "i": 2, + "d": { + "t": "b", + "b": { + "false": { + "i": 3 + }, + "true": { + "i": 3 + } + } + } + }, + "Agregar Adjunto" + ], + "d": { + "arl": "t('Add attachment')" + } + }, + "2652eba8913a8215": "¡Tarea eliminada! Redirigiendo a la lista de tareas...", + "60f3614885809992": "Error al eliminar la tarea.", + "10c490dc40e23d5f": { + "i": 1, + "c": "Eliminar Tarea" + }, + "816aa1e2404312fe": { + "i": 1, + "c": "¿Está seguro de que desea eliminar esta tarea? Esta acción no se puede deshacer." + }, + "9f884fe869bd505f": { + "i": 1, + "c": { + "i": 2, + "d": { + "t": "b", + "b": { + "true": { + "i": 3, + "c": [ + { + "i": 4 + }, + "Eliminando..." + ] + }, + "false": { + "i": 3, + "c": [ + { + "i": 4 + }, + "Eliminar" + ] + } + } + } + } + }, + "08d0f390f574e468": { + "i": 1, + "c": "Error al cargar los comentarios. Por favor, inténtelo de nuevo." + }, + "016cac202a0dc7c1": "Cambiar estado...", + "43189be83912423d": "No se encontró ningún estado.", + "446e0c2f4e0343a4": "Cambiar asignado...", + "66c7d029b8271817": "No se encontró ningún miembro.", + "dcae0a82dd1f6c7e": "Cambiar frecuencia...", + "36b9778b701af2bc": "No se encontró frecuencia.", + "43c874709ca55fa7": "Cambiar departamento...", + "aea5c10ba6e38f6b": "No se encontró ningún departamento.", + "682bc5dfaa20b284": { + "i": 1, + "c": "Propiedades" + }, + "6fdd367d7ef140c0": { + "i": 1, + "c": "Sin asignar" + }, + "08d8896274a9f8b6": { + "i": 1, + "c": "Frecuencia" + }, + "c757594812e9faac": { + "i": 1, + "c": "Ninguno" + }, + "29448d012f59eca0": "Última verificación: {date}", + "6dc02528f9e2338b": "Prueba Sin Título", + "42605ab1ed97886e": "Estado: {status}", + "897f03dc0e2b5227": { + "i": 1, + "c": "Remediación" + }, + "220bde1fb25176c8": { + "i": 1, + "c": [ + "Remediar ", + { + "i": 2 + } + ] + }, + "938a63a981945ed4": "Próxima ejecución programada en {hours}h {minutes}m (diariamente a las 5:00 AM UTC)", + "6ddfdd2d184effb9": "Error al ejecutar las pruebas, por favor inténtelo de nuevo más tarde o contacte al soporte técnico.", + "dd1d7b8154d1e7cb": "Resultados de Pruebas en la Nube", + "67b35f4fdf0210aa": "Todos los Estados", + "ce5547bfb43f614e": "No se encontraron resultados de prueba de {providerName} con estado \"{status}\"", + "e6a3a1c30eccbfe8": "No se encontraron resultados de prueba de {providerName}", + "7a27134aa10d07ed": { + "i": 1, + "c": "Resultados de Pruebas en la Nube" + }, + "d1d897b858f8a82d": { + "i": 1, + "c": [ + "Ejecutar Pruebas Nuevamente ", + { + "i": 2 + } + ] + }, + "e7921b06eca884c2": "AWS", + "215e519a98f88b0a": "GCP", + "503f8715b91fe2fb": "Azure", + "182b9e070e56d26f": { + "i": 1, + "c": "No hay proveedores de nube configurados. Por favor, agregue una integración." + }, + "c9c75f60b6c15be4": "Nombre del Proveedor", + "773f5747de61e40e": "Buscar nombre del proveedor...", + "58b50e864a391838": "Buscar por estado...", + "c3bbd7411a2dc811": "Nube", + "43b1ff0da173f934": "Infraestructura", + "f879b14001421a68": "Finanzas", + "1fe9b13ddddb8238": "Marketing", + "0e42225378d411d1": "Ventas", + "cf472c2c77272c21": "Buscar por categoría...", + "bac7b72e4afdef23": "Buscar por responsable...", + "b88dc2936f589408": "Agregar Proveedor", + "4c6164973ee2af57": "Seleccione el nivel de riesgo inherente para este proveedor", + "1e4e175763ef2d9a": { + "i": 1, + "c": "Actualizar Riesgo Inherente" + }, + "578f620a5d3ce432": { + "i": 1, + "c": "Seleccione el nivel de riesgo inherente para este proveedor" + }, + "bf783f24128efb7e": "Seleccione el nivel de riesgo residual para este proveedor", + "00651fa87998893d": "Actualizar Riesgo Residual", + "774181ddb805dd85": { + "i": 1, + "c": "Redirigiendo" + }, + "c88b4a4a9e24640d": "No se encontraron requisitos.", + "d3264cfb310b4030": "Tipo", + "0216959a95a6473a": "política", + "1c7eb0b7b3024ebd": "tarea", + "5e301665b8bb3b02": "No se encontraron controles.", + "02678b8c9459404c": "Progreso: {progress}%", + "e9a1e63b8eda1b8a": "Completadas: {completed}/{total} políticas", + "6948c952ed73ed5c": "Departamento", + "6162f0e0f91033b1": "Correo electrónico del empleado", + "5a5eeec8b2809c44": "CORREO ELECTRÓNICO", + "4c0d7b0ceb38cbdd": "Fecha de Incorporación", + "d14c5eaf4a1143dc": "Nombre del empleado", + "2248d69f935a18c4": "NOMBRE", + "f9ac5ca36bc7dec9": "No está autorizado para ver esta política", + "1c47511afef4d6ae": "Resumen de Tareas", + "37991603abeba431": "Revocar Clave API", + "35dd0d7bf99b8a1e": "¿Está seguro de que desea revocar esta clave de API? Esta acción no se puede deshacer.", + "4e745dee79a5b94b": "Revocando...", + "224e9839195b63b2": "Revocar", + "fc36a21e5cc53b73": "Nunca", + "c55dcb8c0cd7cb3e": "Creado", + "b6d4f8c301c67fb6": "Vence", + "9977688d38424e1a": "Último Uso", + "177000bd1643352a": "Acciones", + "50dabb0a1d3ad5b1": "Agregar Clave API", + "482cd570aa1b2713": "Eliminar Entrada", + "b03da6e8c7a5bfaa": "¿Está seguro de que desea eliminar esta entrada? Esta acción no se puede deshacer.", + "20217495d08d5a3b": "Eliminando...", + "4109fa880275dedc": "El ID del proveedor es obligatorio", + "05a9a111421ebabb": "La fecha de vencimiento es obligatoria", + "df0e1a07797cc68c": "Proveedor actualizado exitosamente", + "25a10b11a993b306": "Error al actualizar el proveedor", + "47cc1e085a3174ae": "Tarea creada exitosamente", + "55342ab7a97ffbfe": "Error al crear la tarea", + "5e3517d29bec3e21": "Detalles de la Tarea", + "8b4593ffbe8f90d0": "Título de la Tarea", + "c775f9599f0eda2f": "Crear Tarea de Proveedor", + "1c169c5945a461d8": "Un nombre corto y descriptivo para el proveedor.", + "863f4c8e2939f0db": "Una descripción detallada del proveedor y sus servicios.", + "3dae548a2b4e4b45": "Proveedor", + "4614a422f1430cd7": "Nombre", + "6522ea4ccbbb997e": "Actualizar Proveedor", + "f81c412fd2c691d2": "Actualizar los detalles de su proveedor", + "0115c954700d4418": "Éxito", + "8c9eb95498b3fdbf": "Probabilidad Inherente", + "ccd7ef18ad932135": "Muy Probable", + "0c76cb57843615d1": "Probable", + "9ab8756337c8e8cf": "Posible", + "8f576fd8182fd55d": "Improbable", + "2475cdecc71231e4": "Muy Improbable", + "8bbd0df3cd3d180c": "Impacto Inherente", + "8bfe2f2ac1568adb": "Insignificante", + "aaab716cee2fcafa": "Menor", + "ba5f6120cdca2370": "Moderado", + "dcfc66f6d60d34bb": "Mayor", + "1d1a48b01d37d7bc": "Severo", + "f7102f88eb6338fe": "Probabilidad Residual", + "35c82a8ec6911300": "Impacto Residual", + "39a1b174cf523b9e": { + "i": 1, + "c": "No se encontraron categorías con riesgos" + }, + "98f6c3ebe2e84d61": { + "i": 1, + "c": "No se encontraron estados con riesgos" + }, + "9735eee043166dfa": "Proveedores por Categoría", + "f3debdcf93ceb149": { + "i": 1, + "c": "Estado del Proveedor" + }, + "1c372fd54187d192": { + "i": 1, + "c": "Controles" + }, + "f8d88331e2a1ec4c": "Sin resultados.", + "2d3ed639882b255f": { + "i": 1, + "c": [ + { + "i": 2, + "c": "No se encontraron resultados" + }, + { + "i": 3, + "c": { + "i": 4, + "d": { + "t": "b", + "b": { + "false": { + "i": 5, + "c": "Crear una tarea para comenzar" + }, + "true": { + "i": 5, + "c": "Intenta otra búsqueda o ajusta los filtros" + } + } + } + } + } + ] + }, + "c87be293f8b9abb7": { + "i": 1, + "c": "No se encontraron tareas" + }, + "6eb7ab0dcaae01b3": { + "i": 1, + "c": "Crear una tarea para comenzar" + }, + "65345f6829f01d2a": "No Iniciado", + "54002bf654ddbab1": "Completado", + "172683fc76b5e064": "Todos los Asignados", + "ac2c1a39608fc635": "abrir", + "6d9e50c042a3ee43": "en progreso", + "0dba9273844f718f": "completado", + "58d901bb0c6d37c6": "cancelado", + "ac2ac17bdfb977f6": { + "i": 1, + "c": "Detalles de la Tarea" + }, + "1719f7091d606c2f": "Detalles de la Tarea", + "279b0dc45b250e2f": "Ingrese el título", + "63028840be654541": "Ingrese descripción", + "5c8772e727c04218": "Pregunta", + "29ea79cb3aeabe1a": "Respuesta" +} diff --git a/apps/app/src/actions/change-organization.ts b/apps/app/src/actions/change-organization.ts index 32e768bb0..e679a6fb5 100644 --- a/apps/app/src/actions/change-organization.ts +++ b/apps/app/src/actions/change-organization.ts @@ -2,6 +2,7 @@ import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; @@ -32,9 +33,10 @@ export const changeOrganizationAction = authActionClient }); if (!organizationMember) { + const t = await getGT(); return { success: false, - error: 'Unauthorized', + error: t('Unauthorized'), }; } @@ -46,9 +48,10 @@ export const changeOrganizationAction = authActionClient }); if (!organization) { + const t = await getGT(); return { success: false, - error: 'Organization not found', + error: t('Organization not found'), }; } @@ -68,9 +71,10 @@ export const changeOrganizationAction = authActionClient } catch (error) { console.error('Error changing organization:', error); + const t = await getGT(); return { success: false, - error: 'Failed to change organization', + error: t('Failed to change organization'), }; } }); diff --git a/apps/app/src/actions/context-hub/create-context-entry-action.ts b/apps/app/src/actions/context-hub/create-context-entry-action.ts index 261ed96de..1a39e5683 100644 --- a/apps/app/src/actions/context-hub/create-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/create-context-entry-action.ts @@ -1,18 +1,23 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; -import { createContextEntrySchema } from '../schema'; +import { getCreateContextEntrySchema } from '../schema'; export const createContextEntryAction = authActionClient - .inputSchema(createContextEntrySchema) + .inputSchema(async () => { + const t = await getGT(); + return getCreateContextEntrySchema(t); + }) .metadata({ name: 'create-context-entry' }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { question, answer, tags } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error(t('No active organization')); await db.context.create({ data: { @@ -21,7 +26,7 @@ export const createContextEntryAction = authActionClient tags: tags ? tags .split(',') - .map((t) => t.trim()) + .map((tag) => tag.trim()) .filter(Boolean) : [], organizationId, diff --git a/apps/app/src/actions/context-hub/delete-context-entry-action.ts b/apps/app/src/actions/context-hub/delete-context-entry-action.ts index a04488d35..1b41ecc75 100644 --- a/apps/app/src/actions/context-hub/delete-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/delete-context-entry-action.ts @@ -1,18 +1,23 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; -import { deleteContextEntrySchema } from '../schema'; +import { getDeleteContextEntrySchema } from '../schema'; export const deleteContextEntryAction = authActionClient - .inputSchema(deleteContextEntrySchema) + .inputSchema(async () => { + const t = await getGT(); + return getDeleteContextEntrySchema(t); + }) .metadata({ name: 'delete-context-entry' }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error(t('No active organization')); await db.context.delete({ where: { id, organizationId }, diff --git a/apps/app/src/actions/context-hub/update-context-entry-action.ts b/apps/app/src/actions/context-hub/update-context-entry-action.ts index a7c103b88..be4f28f76 100644 --- a/apps/app/src/actions/context-hub/update-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/update-context-entry-action.ts @@ -1,18 +1,23 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { authActionClient } from '../safe-action'; -import { updateContextEntrySchema } from '../schema'; +import { getUpdateContextEntrySchema } from '../schema'; export const updateContextEntryAction = authActionClient - .inputSchema(updateContextEntrySchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdateContextEntrySchema(t); + }) .metadata({ name: 'update-context-entry' }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, question, answer, tags } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error(t('No active organization')); await db.context.update({ where: { id, organizationId }, @@ -22,7 +27,7 @@ export const updateContextEntryAction = authActionClient tags: tags ? tags .split(',') - .map((t) => t.trim()) + .map((tag) => tag.trim()) .filter(Boolean) : [], }, diff --git a/apps/app/src/actions/files/upload-file.ts b/apps/app/src/actions/files/upload-file.ts index e4f90ff1e..c19453746 100644 --- a/apps/app/src/actions/files/upload-file.ts +++ b/apps/app/src/actions/files/upload-file.ts @@ -9,6 +9,7 @@ import { logger } from '@/utils/logger'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { AttachmentEntityType, AttachmentType, db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; @@ -54,13 +55,14 @@ export const uploadFile = async (input: z.infer) logger.info(`[uploadFile] Starting upload for ${input.fileName}`); console.log('[uploadFile] Checking S3 client availability'); + const t = await getGT(); try { // Check if S3 client is available if (!s3Client) { logger.error('[uploadFile] S3 client not initialized - check environment variables'); return { success: false, - error: 'File upload service is currently unavailable. Please contact support.', + error: t('File upload service is currently unavailable. Please contact support.'), } as const; } @@ -68,7 +70,7 @@ export const uploadFile = async (input: z.infer) logger.error('[uploadFile] S3 bucket name not configured'); return { success: false, - error: 'File upload service is not properly configured.', + error: t('File upload service is not properly configured.'), } as const; } @@ -84,7 +86,7 @@ export const uploadFile = async (input: z.infer) logger.error('[uploadFile] Not authorized - no organization found'); return { success: false, - error: 'Not authorized - no organization found', + error: t('Not authorized - no organization found'), } as const; } @@ -101,7 +103,7 @@ export const uploadFile = async (input: z.infer) ); return { success: false, - error: `File exceeds the ${MAX_FILE_SIZE_MB}MB limit.`, + error: t('File exceeds the {maxSize}MB limit.', { maxSize: MAX_FILE_SIZE_MB }), } as const; } @@ -157,7 +159,7 @@ export const uploadFile = async (input: z.infer) logger.error(`[uploadFile] Error during upload process:`, error); return { success: false, - error: error instanceof Error ? error.message : 'An unknown error occurred.', + error: error instanceof Error ? error.message : t('An unknown error occurred.'), } as const; } }; diff --git a/apps/app/src/actions/integrations/delete-integration-connection.ts b/apps/app/src/actions/integrations/delete-integration-connection.ts index d47db08c9..3f6504734 100644 --- a/apps/app/src/actions/integrations/delete-integration-connection.ts +++ b/apps/app/src/actions/integrations/delete-integration-connection.ts @@ -3,12 +3,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { deleteIntegrationConnectionSchema } from '../schema'; +import { getDeleteIntegrationConnectionSchema } from '../schema'; export const deleteIntegrationConnectionAction = authActionClient - .inputSchema(deleteIntegrationConnectionSchema) + .inputSchema(async () => { + const t = await getGT(); + return getDeleteIntegrationConnectionSchema(t); + }) .metadata({ name: 'delete-integration-connection', track: { @@ -19,11 +23,12 @@ export const deleteIntegrationConnectionAction = authActionClient .action(async ({ parsedInput, ctx }) => { const { integrationName } = parsedInput; const { session } = ctx; + const t = await getGT(); if (!session.activeOrganizationId) { return { success: false, - error: 'Unauthorized', + error: t('Unauthorized'), }; } @@ -37,7 +42,7 @@ export const deleteIntegrationConnectionAction = authActionClient if (!integration) { return { success: false, - error: 'Integration not found', + error: t('Integration not found'), }; } diff --git a/apps/app/src/actions/integrations/retrieve-integration-session-token.ts b/apps/app/src/actions/integrations/retrieve-integration-session-token.ts index 3eaa60a2f..16086187d 100644 --- a/apps/app/src/actions/integrations/retrieve-integration-session-token.ts +++ b/apps/app/src/actions/integrations/retrieve-integration-session-token.ts @@ -2,11 +2,15 @@ 'use server'; +import { getGT } from 'gt-next/server'; import { authActionClient } from '../safe-action'; -import { createIntegrationSchema } from '../schema'; +import { getCreateIntegrationSchema } from '../schema'; export const retrieveIntegrationSessionTokenAction = authActionClient - .inputSchema(createIntegrationSchema) + .inputSchema(async () => { + const t = await getGT(); + return getCreateIntegrationSchema(t); + }) .metadata({ name: 'retrieve-integration-session-token', track: { diff --git a/apps/app/src/actions/integrations/update-integration-settings-action.ts b/apps/app/src/actions/integrations/update-integration-settings-action.ts index e4e2a6d2b..804404aa7 100644 --- a/apps/app/src/actions/integrations/update-integration-settings-action.ts +++ b/apps/app/src/actions/integrations/update-integration-settings-action.ts @@ -2,6 +2,7 @@ import { encrypt } from '@/lib/encryption'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -24,9 +25,10 @@ export const updateIntegrationSettingsAction = authActionClient }, }) .action(async ({ parsedInput: { integration_id, option }, ctx: { session } }) => { + const t = await getGT(); try { if (!session.activeOrganizationId) { - throw new Error('User organization not found'); + throw new Error(t('User organization not found')); } let existingIntegration = await db.integration.findFirst({ @@ -51,7 +53,7 @@ export const updateIntegrationSettingsAction = authActionClient const userSettings = existingIntegration.userSettings; if (!userSettings) { - throw new Error('User settings not found'); + throw new Error(t('User settings not found')); } const updatedUserSettings = { @@ -87,7 +89,7 @@ export const updateIntegrationSettingsAction = authActionClient console.error('Failed to update integration settings:', error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to update integration settings', + error: error instanceof Error ? error.message : t('Failed to update integration settings'), }; } }); diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index e80edd1f9..95f4511ce 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -1,6 +1,7 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { Resend } from 'resend'; import { z } from 'zod'; @@ -52,16 +53,17 @@ export const completeInvitation = authActionClientWithoutOrg > => { const { inviteCode } = parsedInput; const user = ctx.user; + const t = await getGT(); if (!user || !user.email) { - throw new Error('Unauthorized'); + throw new Error(t('Unauthorized')); } try { const invitation = await validateInviteCode(inviteCode, user.email); if (!invitation) { - throw new Error('Invitation either used or expired'); + throw new Error(t('Invitation either used or expired')); } const existingMembership = await db.member.findFirst({ @@ -98,7 +100,7 @@ export const completeInvitation = authActionClientWithoutOrg } if (!invitation.role) { - throw new Error('Invitation role is required'); + throw new Error(t('Invitation role is required')); } await db.member.create({ diff --git a/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts b/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts index 37068764e..e96fda6a0 100644 --- a/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts +++ b/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts @@ -1,7 +1,8 @@ 'use server'; -import { addFrameworksSchema } from '@/actions/schema'; +import { getAddFrameworksSchema } from '@/actions/schema'; import { db, Prisma } from '@db'; +import { getGT } from 'gt-next/server'; import { authWithOrgAccessClient } from '../safe-action'; import { _upsertOrgFrameworkStructureCore } from './lib/initialize-organization'; @@ -11,7 +12,10 @@ import { _upsertOrgFrameworkStructureCore } from './lib/initialize-organization' * already exist (e.g., from a shared template or a previous addition). */ export const addFrameworksToOrganizationAction = authWithOrgAccessClient - .inputSchema(addFrameworksSchema) + .inputSchema(async () => { + const t = await getGT(); + return getAddFrameworksSchema(t); + }) .metadata({ name: 'add-frameworks-to-organization', track: { @@ -23,6 +27,7 @@ export const addFrameworksToOrganizationAction = authWithOrgAccessClient .action(async ({ parsedInput, ctx }) => { const { user, member, organizationId } = ctx; const { frameworkIds } = parsedInput; + const t = await getGT(); await db.$transaction(async (tx) => { // 1. Fetch FrameworkEditorFrameworks and their requirements for the given frameworkIds, filtering by visible: true @@ -37,7 +42,7 @@ export const addFrameworksToOrganizationAction = authWithOrgAccessClient }); if (frameworksAndRequirements.length === 0) { - throw new Error('No valid or visible frameworks found for the provided IDs.'); + throw new Error(t('No valid or visible frameworks found for the provided IDs.')); } const finalFrameworkEditorIds = frameworksAndRequirements.map((f) => f.id); diff --git a/apps/app/src/actions/organization/bulk-invite-employees.ts b/apps/app/src/actions/organization/bulk-invite-employees.ts index cbc4e67db..873917667 100644 --- a/apps/app/src/actions/organization/bulk-invite-employees.ts +++ b/apps/app/src/actions/organization/bulk-invite-employees.ts @@ -2,16 +2,23 @@ import { auth } from '@/utils/auth'; import { authClient } from '@/utils/auth-client'; +import { getGT } from 'gt-next/server'; import { createSafeActionClient } from 'next-safe-action'; import { headers } from 'next/headers'; import { z } from 'zod'; -const emailSchema = z.string().email({ message: 'Invalid email format' }); +const createSchemas = async () => { + const t = await getGT(); -const schema = z.object({ - organizationId: z.string(), - emails: z.array(emailSchema).min(1, { message: 'At least one email is required.' }), -}); + const emailSchema = z.string().email({ message: t('Invalid email format') }); + + const schema = z.object({ + organizationId: z.string(), + emails: z.array(emailSchema).min(1, { message: t('At least one email is required.') }), + }); + + return { emailSchema, schema }; +}; interface InviteResult { email: string; @@ -20,15 +27,19 @@ interface InviteResult { } export const bulkInviteEmployees = createSafeActionClient() - .inputSchema(schema) + .inputSchema(async () => { + const { schema } = await createSchemas(); + return schema; + }) .action(async ({ parsedInput }) => { const { organizationId, emails } = parsedInput; + const t = await getGT(); const session = await auth.api.getSession({ headers: await headers() }); if (session?.session.activeOrganizationId !== organizationId) { return { success: false, - error: 'Unauthorized or invalid organization.', + error: t('Unauthorized or invalid organization.'), }; } @@ -45,7 +56,7 @@ export const bulkInviteEmployees = createSafeActionClient() } catch (error) { allSuccess = false; console.error(`Failed to invite ${email}:`, error); - const errorMessage = error instanceof Error ? error.message : 'Invitation failed'; + const errorMessage = error instanceof Error ? error.message : t('Invitation failed'); results.push({ email, success: false, error: errorMessage }); } } diff --git a/apps/app/src/actions/organization/create-api-key-action.ts b/apps/app/src/actions/organization/create-api-key-action.ts index 3b9389368..97928a2d5 100644 --- a/apps/app/src/actions/organization/create-api-key-action.ts +++ b/apps/app/src/actions/organization/create-api-key-action.ts @@ -1,13 +1,17 @@ 'use server'; import { authActionClient } from '@/actions/safe-action'; -import { apiKeySchema } from '@/actions/schema'; +import { getApiKeySchema } from '@/actions/schema'; import { generateApiKey, generateSalt, hashApiKey } from '@/lib/api-key'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; export const createApiKeyAction = authActionClient - .inputSchema(apiKeySchema) + .inputSchema(async () => { + const t = await getGT(); + return getApiKeySchema(t); + }) .metadata({ name: 'createApiKey', track: { @@ -16,6 +20,8 @@ export const createApiKeyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); + try { const { name, expiresAt } = parsedInput; console.log(`Creating API key "${name}" with expiration: ${expiresAt}`); @@ -87,7 +93,7 @@ export const createApiKeyAction = authActionClient success: false, error: { code: 'DUPLICATE_NAME', - message: 'An API key with this name already exists', + message: t('An API key with this name already exists'), }, }; } @@ -97,7 +103,7 @@ export const createApiKeyAction = authActionClient success: false, error: { code: 'INVALID_ORGANIZATION', - message: "The organization does not exist or you don't have access", + message: t("The organization does not exist or you don't have access"), }, }; } @@ -107,7 +113,7 @@ export const createApiKeyAction = authActionClient success: false, error: { code: 'INTERNAL_ERROR', - message: 'An unexpected error occurred while creating the API key', + message: t('An unexpected error occurred while creating the API key'), }, }; } diff --git a/apps/app/src/actions/organization/delete-organization-action.ts b/apps/app/src/actions/organization/delete-organization-action.ts index 232f301d4..93d29d1af 100644 --- a/apps/app/src/actions/organization/delete-organization-action.ts +++ b/apps/app/src/actions/organization/delete-organization-action.ts @@ -3,9 +3,10 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { deleteOrganizationSchema } from '../schema'; +import { getDeleteOrganizationSchema } from '../schema'; type DeleteOrganizationResult = { success: boolean; @@ -13,7 +14,10 @@ type DeleteOrganizationResult = { }; export const deleteOrganizationAction = authActionClient - .inputSchema(deleteOrganizationSchema) + .inputSchema(async () => { + const t = await getGT(); + return getDeleteOrganizationSchema(t); + }) .metadata({ name: 'delete-organization', track: { diff --git a/apps/app/src/actions/organization/get-api-keys-action.ts b/apps/app/src/actions/organization/get-api-keys-action.ts index 85149c4e7..9ecb792c6 100644 --- a/apps/app/src/actions/organization/get-api-keys-action.ts +++ b/apps/app/src/actions/organization/get-api-keys-action.ts @@ -3,6 +3,7 @@ import type { ActionResponse } from '@/actions/types'; import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; export const getApiKeysAction = async (): Promise< @@ -17,6 +18,8 @@ export const getApiKeysAction = async (): Promise< }[] > > => { + const t = await getGT(); + try { const session = await auth.api.getSession({ headers: await headers(), @@ -27,7 +30,7 @@ export const getApiKeysAction = async (): Promise< success: false, error: { code: 'UNAUTHORIZED', - message: 'You must be logged in to perform this action', + message: t('You must be logged in to perform this action'), }, }; } @@ -65,7 +68,7 @@ export const getApiKeysAction = async (): Promise< success: false, error: { code: 'INTERNAL_ERROR', - message: 'An error occurred while fetching API keys', + message: t('An error occurred while fetching API keys'), }, }; } diff --git a/apps/app/src/actions/organization/get-organization-users-action.ts b/apps/app/src/actions/organization/get-organization-users-action.ts index dcd33e6a8..f63ec9433 100644 --- a/apps/app/src/actions/organization/get-organization-users-action.ts +++ b/apps/app/src/actions/organization/get-organization-users-action.ts @@ -1,6 +1,7 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { authActionClient } from '../safe-action'; interface User { @@ -15,10 +16,12 @@ export const getOrganizationUsersAction = authActionClient }) .action( async ({ parsedInput, ctx }): Promise<{ success: boolean; error?: string; data?: User[] }> => { + const t = await getGT(); + if (!ctx.session.activeOrganizationId) { return { success: false, - error: 'User does not have an organization', + error: t('User does not have an organization'), }; } @@ -54,7 +57,7 @@ export const getOrganizationUsersAction = authActionClient } catch (error) { return { success: false, - error: 'Failed to fetch organization users', + error: t('Failed to fetch organization users'), }; } }, diff --git a/apps/app/src/actions/organization/invite-employee.ts b/apps/app/src/actions/organization/invite-employee.ts index 241fa7d24..fa1ba223a 100644 --- a/apps/app/src/actions/organization/invite-employee.ts +++ b/apps/app/src/actions/organization/invite-employee.ts @@ -1,6 +1,7 @@ 'use server'; import { authClient } from '@/utils/auth-client'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -22,11 +23,12 @@ export const inviteEmployee = authActionClient .inputSchema(inviteEmployeeSchema) .action(async ({ parsedInput, ctx }): Promise> => { const organizationId = ctx.session.activeOrganizationId; + const t = await getGT(); if (!organizationId) { return { success: false, - error: 'Organization not found', + error: t('Organization not found'), }; } @@ -48,7 +50,7 @@ export const inviteEmployee = authActionClient }; } catch (error) { console.error('Error inviting employee:', error); - const errorMessage = error instanceof Error ? error.message : 'Failed to invite employee'; + const errorMessage = error instanceof Error ? error.message : t('Failed to invite employee'); return { success: false, error: errorMessage, diff --git a/apps/app/src/actions/organization/invite-member.ts b/apps/app/src/actions/organization/invite-member.ts index 6ffe84644..b094e5644 100644 --- a/apps/app/src/actions/organization/invite-member.ts +++ b/apps/app/src/actions/organization/invite-member.ts @@ -1,6 +1,7 @@ 'use server'; import { authClient } from '@/utils/auth-client'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -21,12 +22,13 @@ export const inviteMember = authActionClient }) .inputSchema(inviteMemberSchema) .action(async ({ parsedInput, ctx }): Promise> => { + const t = await getGT(); const organizationId = ctx.session.activeOrganizationId; if (!organizationId) { return { success: false, - error: 'Organization not found', + error: t('Organization not found'), }; } @@ -47,7 +49,7 @@ export const inviteMember = authActionClient }; } catch (error) { console.error('Error inviting member:', error); - const errorMessage = error instanceof Error ? error.message : 'Failed to invite member'; + const errorMessage = error instanceof Error ? error.message : t('Failed to invite member'); return { success: false, error: errorMessage, diff --git a/apps/app/src/actions/organization/remove-employee.ts b/apps/app/src/actions/organization/remove-employee.ts index 9387ec46b..dd1ebb20b 100644 --- a/apps/app/src/actions/organization/remove-employee.ts +++ b/apps/app/src/actions/organization/remove-employee.ts @@ -1,6 +1,7 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -24,13 +25,14 @@ export const removeEmployeeRoleOrMember = authActionClient parsedInput, ctx, }): Promise> => { + const t = await getGT(); const organizationId = ctx.session.activeOrganizationId; const currentUserId = ctx.user.id; if (!organizationId) { return { success: false, - error: 'Organization not found', + error: t('Organization not found'), }; } @@ -48,7 +50,7 @@ export const removeEmployeeRoleOrMember = authActionClient if (!currentUserMember || !['admin', 'owner'].includes(currentUserMember.role)) { return { success: false, - error: 'Permission denied: Only admins or owners can remove employees.', + error: t('Permission denied: Only admins or owners can remove employees.'), }; } @@ -63,7 +65,7 @@ export const removeEmployeeRoleOrMember = authActionClient if (!targetMember) { return { success: false, - error: 'Target employee not found in this organization.', + error: t('Target employee not found in this organization.'), }; } @@ -72,7 +74,7 @@ export const removeEmployeeRoleOrMember = authActionClient if (!roles.includes('employee')) { return { success: false, - error: 'Target member does not have the employee role.', + error: t('Target member does not have the employee role.'), }; } @@ -84,14 +86,14 @@ export const removeEmployeeRoleOrMember = authActionClient if (targetMember.role === 'owner') { return { success: false, - error: 'Cannot remove the organization owner.', + error: t('Cannot remove the organization owner.'), }; } // Cannot remove self if (targetMember.userId === currentUserId) { return { success: false, - error: 'You cannot remove yourself.', + error: t('You cannot remove yourself.'), }; } @@ -128,7 +130,7 @@ export const removeEmployeeRoleOrMember = authActionClient } catch (error) { console.error('Error removing employee role/member:', error); const errorMessage = - error instanceof Error ? error.message : 'Failed to remove employee role or member.'; + error instanceof Error ? error.message : t('Failed to remove employee role or member.'); return { success: false, error: errorMessage, diff --git a/apps/app/src/actions/organization/revoke-api-key-action.ts b/apps/app/src/actions/organization/revoke-api-key-action.ts index f3a4d6e6c..f965c5b2c 100644 --- a/apps/app/src/actions/organization/revoke-api-key-action.ts +++ b/apps/app/src/actions/organization/revoke-api-key-action.ts @@ -2,6 +2,7 @@ import { authActionClient } from '@/actions/safe-action'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; @@ -19,6 +20,7 @@ export const revokeApiKeyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); try { const { id } = parsedInput; @@ -35,7 +37,7 @@ export const revokeApiKeyAction = authActionClient if (result.count === 0) { return { success: false, - error: 'API key not found or not authorized to revoke', + error: t('API key not found or not authorized to revoke'), }; } @@ -43,13 +45,13 @@ export const revokeApiKeyAction = authActionClient return { success: true, - message: 'API key revoked successfully', + message: t('API key revoked successfully'), }; } catch (error) { console.error('Error revoking API key:', error); return { success: false, - error: 'An error occurred while revoking the API key', + error: t('An error occurred while revoking the API key'), }; } }); diff --git a/apps/app/src/actions/organization/update-organization-name-action.ts b/apps/app/src/actions/organization/update-organization-name-action.ts index 24b402f0c..743ccc8db 100644 --- a/apps/app/src/actions/organization/update-organization-name-action.ts +++ b/apps/app/src/actions/organization/update-organization-name-action.ts @@ -3,12 +3,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { organizationNameSchema } from '../schema'; +import { getOrganizationNameSchema } from '../schema'; export const updateOrganizationNameAction = authActionClient - .inputSchema(organizationNameSchema) + .inputSchema(async () => { + const t = await getGT(); + return getOrganizationNameSchema(t); + }) .metadata({ name: 'update-organization-name', track: { @@ -17,15 +21,16 @@ export const updateOrganizationNameAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { name } = parsedInput; const { activeOrganizationId } = ctx.session; if (!name) { - throw new Error('Invalid user input'); + throw new Error(t('Invalid user input')); } if (!activeOrganizationId) { - throw new Error('No active organization'); + throw new Error(t('No active organization')); } try { @@ -44,6 +49,6 @@ export const updateOrganizationNameAction = authActionClient }; } catch (error) { console.error(error); - throw new Error('Failed to update organization name'); + throw new Error(t('Failed to update organization name')); } }); diff --git a/apps/app/src/actions/organization/update-organization-website-action.ts b/apps/app/src/actions/organization/update-organization-website-action.ts index fbcf37869..e4b74078f 100644 --- a/apps/app/src/actions/organization/update-organization-website-action.ts +++ b/apps/app/src/actions/organization/update-organization-website-action.ts @@ -3,12 +3,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { organizationWebsiteSchema } from '../schema'; +import { getOrganizationWebsiteSchema } from '../schema'; export const updateOrganizationWebsiteAction = authActionClient - .inputSchema(organizationWebsiteSchema) + .inputSchema(async () => { + const t = await getGT(); + return getOrganizationWebsiteSchema(t); + }) .metadata({ name: 'update-organization-website', track: { @@ -17,15 +21,16 @@ export const updateOrganizationWebsiteAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { website } = parsedInput; const { activeOrganizationId } = ctx.session; if (!website) { - throw new Error('Invalid user input'); + throw new Error(t('Invalid user input')); } if (!activeOrganizationId) { - throw new Error('No active organization'); + throw new Error(t('No active organization')); } try { @@ -44,6 +49,6 @@ export const updateOrganizationWebsiteAction = authActionClient }; } catch (error) { console.error(error); - throw new Error('Failed to update organization website'); + throw new Error(t('Failed to update organization website')); } }); diff --git a/apps/app/src/actions/people/create-employee-action.ts b/apps/app/src/actions/people/create-employee-action.ts index 2717cfdef..57812a220 100644 --- a/apps/app/src/actions/people/create-employee-action.ts +++ b/apps/app/src/actions/people/create-employee-action.ts @@ -2,12 +2,16 @@ import { completeEmployeeCreation } from '@/lib/db/employee'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { getGT } from 'gt-next/server'; import { authActionClient } from '../safe-action'; -import { createEmployeeSchema } from '../schema'; +import { getCreateEmployeeSchema } from '../schema'; import type { ActionResponse } from '../types'; export const createEmployeeAction = authActionClient - .inputSchema(createEmployeeSchema) + .inputSchema(async () => { + const t = await getGT(); + return getCreateEmployeeSchema(t); + }) .metadata({ name: 'create-employee', track: { @@ -16,13 +20,14 @@ export const createEmployeeAction = authActionClient }, }) .action(async ({ parsedInput, ctx }): Promise => { + const t = await getGT(); const { name, email, department, externalEmployeeId } = parsedInput; const { user, session } = ctx; if (!session.activeOrganizationId) { return { success: false, - error: 'Not authorized - no organization found', + error: t('Not authorized - no organization found'), }; } @@ -45,13 +50,13 @@ export const createEmployeeAction = authActionClient if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') { return { success: false, - error: 'An employee with this email already exists in your organization', + error: t('An employee with this email already exists in your organization'), }; } return { success: false, - error: error instanceof Error ? error.message : 'Failed to create employee', + error: error instanceof Error ? error.message : t('Failed to create employee'), }; } }); diff --git a/apps/app/src/actions/policies/accept-requested-policy-changes.ts b/apps/app/src/actions/policies/accept-requested-policy-changes.ts index 554299c30..56d4fdcd5 100644 --- a/apps/app/src/actions/policies/accept-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/accept-requested-policy-changes.ts @@ -1,6 +1,7 @@ 'use server'; import { db, PolicyStatus } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -23,15 +24,16 @@ export const acceptRequestedPolicyChangesAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, approverId, comment } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error(t('Unauthorized')); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error(t('Approver is required')); } try { @@ -43,11 +45,11 @@ export const acceptRequestedPolicyChangesAction = authActionClient }); if (!policy) { - throw new Error('Policy not found'); + throw new Error(t('Policy not found')); } if (policy.approverId !== approverId) { - throw new Error('Approver is not the same'); + throw new Error(t('Approver is not the same')); } // Update policy status @@ -74,7 +76,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient if (member) { await db.comment.create({ data: { - content: `Policy changes accepted: ${comment}`, + content: t('Policy changes accepted: {comment}', { comment }), entityId: id, entityType: 'policy', organizationId: session.activeOrganizationId, diff --git a/apps/app/src/actions/policies/archive-policy.ts b/apps/app/src/actions/policies/archive-policy.ts index 48dd1bf26..329259b6c 100644 --- a/apps/app/src/actions/policies/archive-policy.ts +++ b/apps/app/src/actions/policies/archive-policy.ts @@ -1,6 +1,7 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -22,13 +23,14 @@ export const archivePolicyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, action } = parsedInput; const { activeOrganizationId } = ctx.session; if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -43,7 +45,7 @@ export const archivePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: t('Policy not found'), }; } @@ -70,7 +72,7 @@ export const archivePolicyAction = authActionClient console.error(error); return { success: false, - error: 'Failed to update policy archive status', + error: t('Failed to update policy archive status'), }; } }); diff --git a/apps/app/src/actions/policies/create-new-policy.ts b/apps/app/src/actions/policies/create-new-policy.ts index 9f7e75a82..ec9a31957 100644 --- a/apps/app/src/actions/policies/create-new-policy.ts +++ b/apps/app/src/actions/policies/create-new-policy.ts @@ -1,12 +1,16 @@ 'use server'; import { db, Departments, Frequency } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { createPolicySchema } from '../schema'; +import { getCreatePolicySchema } from '../schema'; export const createPolicyAction = authActionClient - .inputSchema(createPolicySchema) + .inputSchema(async () => { + const t = await getGT(); + return getCreatePolicySchema(t); + }) .metadata({ name: 'create-policy', track: { @@ -16,6 +20,7 @@ export const createPolicyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { title, description, controlIds } = parsedInput; const { activeOrganizationId } = ctx.session; const { user } = ctx; @@ -23,14 +28,14 @@ export const createPolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } if (!user) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -45,7 +50,7 @@ export const createPolicyAction = authActionClient if (!member) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -68,7 +73,7 @@ export const createPolicyAction = authActionClient ...(controlIds && controlIds.length > 0 && { controls: { - connect: controlIds.map((id) => ({ id })), + connect: controlIds.map((id: string) => ({ id })), }, }), }, @@ -116,7 +121,7 @@ export const createPolicyAction = authActionClient return { success: false, - error: 'Failed to create policy', + error: t('Failed to create policy'), }; } }); diff --git a/apps/app/src/actions/policies/delete-policy.ts b/apps/app/src/actions/policies/delete-policy.ts index 436c310fe..3b9713fc7 100644 --- a/apps/app/src/actions/policies/delete-policy.ts +++ b/apps/app/src/actions/policies/delete-policy.ts @@ -1,6 +1,7 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -21,13 +22,14 @@ export const deletePolicyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id } = parsedInput; const { activeOrganizationId } = ctx.session; if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -42,7 +44,7 @@ export const deletePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: t('Policy not found'), }; } @@ -63,7 +65,7 @@ export const deletePolicyAction = authActionClient console.error(error); return { success: false, - error: 'Failed to delete policy', + error: t('Failed to delete policy'), }; } }); diff --git a/apps/app/src/actions/policies/deny-requested-policy-changes.ts b/apps/app/src/actions/policies/deny-requested-policy-changes.ts index 5c937edbe..f7bc6e66a 100644 --- a/apps/app/src/actions/policies/deny-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/deny-requested-policy-changes.ts @@ -1,6 +1,7 @@ 'use server'; import { db, PolicyStatus } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; import { authActionClient } from '../safe-action'; @@ -23,15 +24,16 @@ export const denyRequestedPolicyChangesAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, approverId, comment } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error(t('Unauthorized')); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error(t('Approver is required')); } try { @@ -43,11 +45,11 @@ export const denyRequestedPolicyChangesAction = authActionClient }); if (!policy) { - throw new Error('Policy not found'); + throw new Error(t('Policy not found')); } if (policy.approverId !== approverId) { - throw new Error('Approver is not the same'); + throw new Error(t('Approver is not the same')); } // Update policy status @@ -74,7 +76,7 @@ export const denyRequestedPolicyChangesAction = authActionClient if (member) { await db.comment.create({ data: { - content: `Policy changes denied: ${comment}`, + content: t('Policy changes denied: {comment}', { comment }), entityId: id, entityType: 'policy', organizationId: session.activeOrganizationId, diff --git a/apps/app/src/actions/policies/submit-policy-for-approval-action.ts b/apps/app/src/actions/policies/submit-policy-for-approval-action.ts index 214046415..00deba7c3 100644 --- a/apps/app/src/actions/policies/submit-policy-for-approval-action.ts +++ b/apps/app/src/actions/policies/submit-policy-for-approval-action.ts @@ -1,12 +1,16 @@ 'use server'; import { db, PolicyStatus } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updatePolicyFormSchema } from '../schema'; +import { getUpdatePolicyFormSchema } from '../schema'; export const submitPolicyForApprovalAction = authActionClient - .inputSchema(updatePolicyFormSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdatePolicyFormSchema(t); + }) .metadata({ name: 'submit-policy-for-approval', track: { @@ -16,6 +20,7 @@ export const submitPolicyForApprovalAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, assigneeId, @@ -28,11 +33,11 @@ export const submitPolicyForApprovalAction = authActionClient const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error(t('Unauthorized')); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error(t('Approver is required')); } try { diff --git a/apps/app/src/actions/policies/update-policy-action.ts b/apps/app/src/actions/policies/update-policy-action.ts index 940885d85..df006a15b 100644 --- a/apps/app/src/actions/policies/update-policy-action.ts +++ b/apps/app/src/actions/policies/update-policy-action.ts @@ -2,9 +2,10 @@ import { db } from '@db'; import { logger } from '@trigger.dev/sdk/v3'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updatePolicySchema } from '../schema'; +import { getUpdatePolicySchema } from '../schema'; interface ContentNode { type: string; @@ -52,7 +53,10 @@ function processContent(content: ContentNode | ContentNode[]): ContentNode | Con } export const updatePolicyAction = authActionClient - .inputSchema(updatePolicySchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdatePolicySchema(t); + }) .metadata({ name: 'update-policy', track: { @@ -62,6 +66,7 @@ export const updatePolicyAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, content } = parsedInput; const { activeOrganizationId } = ctx.session; const { user } = ctx; @@ -69,14 +74,14 @@ export const updatePolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } if (!user) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -88,7 +93,7 @@ export const updatePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: t('Policy not found'), }; } @@ -115,7 +120,7 @@ export const updatePolicyAction = authActionClient }); return { success: false, - error: error instanceof Error ? error.message : 'Failed to update policy', + error: error instanceof Error ? error.message : t('Failed to update policy'), }; } }); diff --git a/apps/app/src/actions/policies/update-policy-form-action.ts b/apps/app/src/actions/policies/update-policy-form-action.ts index cc6dc6e9c..19d97351f 100644 --- a/apps/app/src/actions/policies/update-policy-form-action.ts +++ b/apps/app/src/actions/policies/update-policy-form-action.ts @@ -3,9 +3,10 @@ 'use server'; import { db, PolicyStatus } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updatePolicyFormSchema } from '../schema'; +import { getUpdatePolicyFormSchema } from '../schema'; // Helper function to calculate next review date based on frequency function calculateNextReviewDate(frequency: string, baseDate: Date = new Date()): Date { @@ -30,7 +31,10 @@ function calculateNextReviewDate(frequency: string, baseDate: Date = new Date()) } export const updatePolicyFormAction = authActionClient - .inputSchema(updatePolicyFormSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdatePolicyFormSchema(t); + }) .metadata({ name: 'update-policy-form', track: { @@ -40,12 +44,13 @@ export const updatePolicyFormAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, status, assigneeId, department, review_frequency, review_date, isRequiredToSign } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error(t('Unauthorized')); } try { diff --git a/apps/app/src/actions/policies/update-policy-overview-action.ts b/apps/app/src/actions/policies/update-policy-overview-action.ts index 59f363768..da749de9a 100644 --- a/apps/app/src/actions/policies/update-policy-overview-action.ts +++ b/apps/app/src/actions/policies/update-policy-overview-action.ts @@ -3,12 +3,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updatePolicyOverviewSchema } from '../schema'; +import { getUpdatePolicyOverviewSchema } from '../schema'; export const updatePolicyOverviewAction = authActionClient - .inputSchema(updatePolicyOverviewSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdatePolicyOverviewSchema(t); + }) .metadata({ name: 'update-policy-overview', track: { @@ -18,20 +22,21 @@ export const updatePolicyOverviewAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { id, title, description, isRequiredToSign } = parsedInput; const { user, session } = ctx; if (!user) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } if (!session.activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -43,7 +48,7 @@ export const updatePolicyOverviewAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: t('Policy not found'), }; } @@ -72,7 +77,7 @@ export const updatePolicyOverviewAction = authActionClient } catch (error) { return { success: false, - error: 'Failed to update policy overview', + error: t('Failed to update policy overview'), }; } }); diff --git a/apps/app/src/actions/research-vendor.ts b/apps/app/src/actions/research-vendor.ts index b9896df11..4fbab4cac 100644 --- a/apps/app/src/actions/research-vendor.ts +++ b/apps/app/src/actions/research-vendor.ts @@ -2,6 +2,7 @@ import { researchVendor } from '@/jobs/tasks/scrape/research'; import { tasks } from '@trigger.dev/sdk/v3'; +import { getGT } from 'gt-next/server'; import { z } from 'zod'; import { authActionClient } from './safe-action'; @@ -19,9 +20,10 @@ export const researchVendorAction = authActionClient const { activeOrganizationId } = session; if (!activeOrganizationId) { + const t = await getGT(); return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -36,10 +38,11 @@ export const researchVendorAction = authActionClient } catch (error) { console.error('Error in researchVendorAction:', error); + const t = await getGT(); return { success: false, error: { - message: error instanceof Error ? error.message : 'An unexpected error occurred.', + message: error instanceof Error ? error.message : t('An unexpected error occurred.'), }, }; } diff --git a/apps/app/src/actions/risk/create-risk-action.ts b/apps/app/src/actions/risk/create-risk-action.ts index 29eb5a03c..ba975d758 100644 --- a/apps/app/src/actions/risk/create-risk-action.ts +++ b/apps/app/src/actions/risk/create-risk-action.ts @@ -3,12 +3,16 @@ 'use server'; import { db, Impact, Likelihood } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { createRiskSchema } from '../schema'; +import { getCreateRiskSchema } from '../schema'; export const createRiskAction = authActionClient - .inputSchema(createRiskSchema) + .inputSchema(async () => { + const t = await getGT(); + return getCreateRiskSchema(t); + }) .metadata({ name: 'create-risk', track: { @@ -17,11 +21,12 @@ export const createRiskAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { + const t = await getGT(); const { title, description, category, department, assigneeId } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error(t('Invalid user input')); } try { diff --git a/apps/app/src/actions/risk/task/revalidate-upload.ts b/apps/app/src/actions/risk/task/revalidate-upload.ts index f25f3a5fc..b0ad559a8 100644 --- a/apps/app/src/actions/risk/task/revalidate-upload.ts +++ b/apps/app/src/actions/risk/task/revalidate-upload.ts @@ -1,11 +1,15 @@ 'use server'; import { authActionClient } from '@/actions/safe-action'; -import { uploadTaskFileSchema } from '@/actions/schema'; +import { getUploadTaskFileSchema } from '@/actions/schema'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; export const revalidateUpload = authActionClient - .inputSchema(uploadTaskFileSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUploadTaskFileSchema(t); + }) .metadata({ name: 'upload-task-file', track: { diff --git a/apps/app/src/actions/risk/task/update-task-action.ts b/apps/app/src/actions/risk/task/update-task-action.ts index dd185312a..117d72f6c 100644 --- a/apps/app/src/actions/risk/task/update-task-action.ts +++ b/apps/app/src/actions/risk/task/update-task-action.ts @@ -4,12 +4,16 @@ import type { TaskStatus } from '@db'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../../safe-action'; -import { updateTaskSchema } from '../../schema'; +import { getUpdateTaskSchema } from '../../schema'; export const updateTaskAction = authActionClient - .inputSchema(updateTaskSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdateTaskSchema(t); + }) .metadata({ name: 'update-task', track: { diff --git a/apps/app/src/actions/risk/update-inherent-risk-action.ts b/apps/app/src/actions/risk/update-inherent-risk-action.ts index b21f5f27e..7c8858cd9 100644 --- a/apps/app/src/actions/risk/update-inherent-risk-action.ts +++ b/apps/app/src/actions/risk/update-inherent-risk-action.ts @@ -1,12 +1,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updateInherentRiskSchema } from '../schema'; +import { getUpdateInherentRiskSchema } from '../schema'; export const updateInherentRiskAction = authActionClient - .inputSchema(updateInherentRiskSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdateInherentRiskSchema(t); + }) .metadata({ name: 'update-inherent-risk', track: { diff --git a/apps/app/src/actions/risk/update-residual-risk-action.ts b/apps/app/src/actions/risk/update-residual-risk-action.ts index d7b8101db..824d2d0af 100644 --- a/apps/app/src/actions/risk/update-residual-risk-action.ts +++ b/apps/app/src/actions/risk/update-residual-risk-action.ts @@ -1,9 +1,10 @@ 'use server'; import { db, Impact, Likelihood } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updateResidualRiskSchema } from '../schema'; +import { getUpdateResidualRiskSchema } from '../schema'; function mapNumericToImpact(value: number): Impact { if (value <= 2) return Impact.insignificant; @@ -22,7 +23,10 @@ function mapNumericToLikelihood(value: number): Likelihood { } export const updateResidualRiskAction = authActionClient - .inputSchema(updateResidualRiskSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdateResidualRiskSchema(t); + }) .metadata({ name: 'update-residual-risk', track: { diff --git a/apps/app/src/actions/risk/update-residual-risk-enum-action.ts b/apps/app/src/actions/risk/update-residual-risk-enum-action.ts index 12adc136d..f21cfe141 100644 --- a/apps/app/src/actions/risk/update-residual-risk-enum-action.ts +++ b/apps/app/src/actions/risk/update-residual-risk-enum-action.ts @@ -1,12 +1,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updateResidualRiskEnumSchema } from '../schema'; // Use the new enum schema +import { getUpdateResidualRiskEnumSchema } from '../schema'; // Use the new enum schema export const updateResidualRiskEnumAction = authActionClient - .inputSchema(updateResidualRiskEnumSchema) // Use the new enum schema + .inputSchema(async () => { + const t = await getGT(); + return getUpdateResidualRiskEnumSchema(t); + }) // Use the new enum schema .metadata({ name: 'update-residual-risk-enum', // New name track: { diff --git a/apps/app/src/actions/risk/update-risk-action.ts b/apps/app/src/actions/risk/update-risk-action.ts index 69809efef..20b5ed629 100644 --- a/apps/app/src/actions/risk/update-risk-action.ts +++ b/apps/app/src/actions/risk/update-risk-action.ts @@ -3,12 +3,16 @@ 'use server'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { authActionClient } from '../safe-action'; -import { updateRiskSchema } from '../schema'; +import { getUpdateRiskSchema } from '../schema'; export const updateRiskAction = authActionClient - .inputSchema(updateRiskSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdateRiskSchema(t); + }) .metadata({ name: 'update-risk', track: { diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 3814b4f65..814798355 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -5,6 +5,7 @@ import { logger } from '@/utils/logger'; import { client } from '@comp/kv'; import { AuditLogEntityType, db } from '@db'; import { Ratelimit } from '@upstash/ratelimit'; +import { getGT } from 'gt-next/server'; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from 'next-safe-action'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; @@ -58,7 +59,8 @@ export const authActionClient = actionClientWithMeta const { session, user } = response ?? {}; if (!session) { - throw new Error('Unauthorized'); + const t = await getGT(); + throw new Error(t('Unauthorized')); } const result = await next({ @@ -89,7 +91,8 @@ export const authActionClient = actionClientWithMeta ); if (!success) { - throw new Error('Too many requests'); + const t = await getGT(); + throw new Error(t('Too many requests')); } remaining = rateLimitRemaining; @@ -111,7 +114,8 @@ export const authActionClient = actionClientWithMeta }); if (!session) { - throw new Error('Unauthorized'); + const t = await getGT(); + throw new Error(t('Unauthorized')); } if (metadata.track) { @@ -140,15 +144,18 @@ export const authActionClient = actionClientWithMeta }); if (!session) { - throw new Error('Unauthorized'); + const t = await getGT(); + throw new Error(t('Unauthorized')); } if (!session.session.activeOrganizationId) { - throw new Error('Organization not found'); + const t = await getGT(); + throw new Error(t('Organization not found')); } if (!member) { - throw new Error('Member not found'); + const t = await getGT(); + throw new Error(t('Member not found')); } const { fileData: _, ...inputForAuditLog } = clientInput as any; @@ -229,7 +236,8 @@ export const authWithOrgAccessClient = authActionClient.use(async ({ next, clien const organizationId = (clientInput as { organizationId?: string })?.organizationId; if (!organizationId) { - throw new Error('Organization ID is required'); + const t = await getGT(); + throw new Error(t('Organization ID is required')); } // Check if user is a member of the organization @@ -241,7 +249,8 @@ export const authWithOrgAccessClient = authActionClient.use(async ({ next, clien }); if (!member) { - throw new Error('You do not have access to this organization'); + const t = await getGT(); + throw new Error(t('You do not have access to this organization')); } return next({ @@ -262,7 +271,8 @@ export const authActionClientWithoutOrg = actionClientWithMeta const { session, user } = response ?? {}; if (!session) { - throw new Error('Unauthorized'); + const t = await getGT(); + throw new Error(t('Unauthorized')); } const result = await next({ @@ -293,7 +303,8 @@ export const authActionClientWithoutOrg = actionClientWithMeta ); if (!success) { - throw new Error('Too many requests'); + const t = await getGT(); + throw new Error(t('Too many requests')); } remaining = rateLimitRemaining; @@ -315,7 +326,8 @@ export const authActionClientWithoutOrg = actionClientWithMeta }); if (!session) { - throw new Error('Unauthorized'); + const t = await getGT(); + throw new Error(t('Unauthorized')); } if (metadata.track) { diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 246717025..84c90d042 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -11,398 +11,432 @@ import { } from '@db'; import { z } from 'zod'; -export const organizationSchema = z.object({ - frameworkIds: z - .array(z.string()) - .min(1, 'Please select at least one framework to get started with'), -}); - -export type OrganizationSchema = z.infer; +export const getOrganizationSchema = (t: (content: string) => string) => + z.object({ + frameworkIds: z + .array(z.string()) + .min(1, t('Please select at least one framework to get started with')), + }); -export const organizationNameSchema = z.object({ - name: z - .string() - .min(1, 'Organization name is required') - .max(255, 'Organization name cannot exceed 255 characters'), -}); +export type OrganizationSchema = z.infer>; -export const subdomainAvailabilitySchema = z.object({ - subdomain: z - .string() - .min(1, 'Subdomain is required') - .max(255, 'Subdomain cannot exceed 255 characters') - .regex(/^[a-z0-9-]+$/, { - message: 'Subdomain can only contain lowercase letters, numbers, and hyphens', - }), -}); +export const getOrganizationNameSchema = (t: (content: string) => string) => + z.object({ + name: z + .string() + .min(1, t('Organization name is required')) + .max(255, t('Organization name cannot exceed 255 characters')), + }); -export const deleteOrganizationSchema = z.object({ - id: z.string(), - organizationId: z.string(), -}); +export const getSubdomainAvailabilitySchema = (t: (content: string) => string) => + z.object({ + subdomain: z + .string() + .min(1, t('Subdomain is required')) + .max(255, t('Subdomain cannot exceed 255 characters')) + .regex(/^[a-z0-9-]+$/, { + message: t('Subdomain can only contain lowercase letters, numbers, and hyphens'), + }), + }); -export const sendFeedbackSchema = z.object({ - feedback: z.string(), -}); +export const getDeleteOrganizationSchema = (t: (content: string) => string) => + z.object({ + id: z.string(), + organizationId: z.string(), + }); -export const updaterMenuSchema = z.array( +export const getSendFeedbackSchema = (t: (content: string) => string) => z.object({ - path: z.string(), - name: z.string(), - }), -); + feedback: z.string(), + }); + +export const getUpdaterMenuSchema = (t: (content: string) => string) => + z.array( + z.object({ + path: z.string(), + name: z.string(), + }), + ); -export const organizationWebsiteSchema = z.object({ - website: z - .string() - .url({ - message: 'Please enter a valid website that starts with https://', - }) - .max(255, 'Website cannot exceed 255 characters'), -}); +export const getOrganizationWebsiteSchema = (t: (content: string) => string) => + z.object({ + website: z + .string() + .url({ + message: t('Please enter a valid website that starts with https://'), + }) + .max(255, t('Website cannot exceed 255 characters')), + }); // Risks -export const createRiskSchema = z.object({ - title: z - .string({ - required_error: 'Risk name is required', - }) - .min(1, { - message: 'Risk name should be at least 1 character', - }) - .max(100, { - message: 'Risk name should be at most 100 characters', +export const getCreateRiskSchema = (t: (content: string) => string) => + z.object({ + title: z + .string({ + required_error: t('Risk name is required'), + }) + .min(1, { + message: t('Risk name should be at least 1 character'), + }) + .max(100, { + message: t('Risk name should be at most 100 characters'), + }), + description: z + .string({ + required_error: t('Risk description is required'), + }) + .min(1, { + message: t('Risk description should be at least 1 character'), + }) + .max(255, { + message: t('Risk description should be at most 255 characters'), + }), + category: z.nativeEnum(RiskCategory, { + required_error: t('Risk category is required'), }), - description: z - .string({ - required_error: 'Risk description is required', - }) - .min(1, { - message: 'Risk description should be at least 1 character', - }) - .max(255, { - message: 'Risk description should be at most 255 characters', + department: z.nativeEnum(Departments, { + required_error: t('Risk department is required'), }), - category: z.nativeEnum(RiskCategory, { - required_error: 'Risk category is required', - }), - department: z.nativeEnum(Departments, { - required_error: 'Risk department is required', - }), - assigneeId: z.string().optional().nullable(), -}); + assigneeId: z.string().optional().nullable(), + }); -export const updateRiskSchema = z.object({ - id: z.string().min(1, { - message: 'Risk ID is required', - }), - title: z.string().min(1, { - message: 'Risk title is required', - }), - description: z.string().min(1, { - message: 'Risk description is required', - }), - category: z.nativeEnum(RiskCategory, { - required_error: 'Risk category is required', - }), - department: z.nativeEnum(Departments, { - required_error: 'Risk department is required', - }), - assigneeId: z.string().optional().nullable(), - status: z.nativeEnum(RiskStatus, { - required_error: 'Risk status is required', - }), -}); +export const getUpdateRiskSchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, { + message: t('Risk ID is required'), + }), + title: z.string().min(1, { + message: t('Risk title is required'), + }), + description: z.string().min(1, { + message: t('Risk description is required'), + }), + category: z.nativeEnum(RiskCategory, { + required_error: t('Risk category is required'), + }), + department: z.nativeEnum(Departments, { + required_error: t('Risk department is required'), + }), + assigneeId: z.string().optional().nullable(), + status: z.nativeEnum(RiskStatus, { + required_error: t('Risk status is required'), + }), + }); -export const createRiskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: 'Risk ID is required', - }), - content: z - .string() - .min(1, { - message: 'Comment content is required', - }) - .max(1000, { - message: 'Comment content should be at most 1000 characters', - }) - .transform((val) => { - // Remove any HTML tags by applying the replacement repeatedly until no changes occur - let sanitized = val; - let previousValue; - - do { - previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } while (sanitized !== previousValue); - - return sanitized; +export const getCreateRiskCommentSchema = (t: (content: string) => string) => + z.object({ + riskId: z.string().min(1, { + message: t('Risk ID is required'), }), -}); + content: z + .string() + .min(1, { + message: t('Comment content is required'), + }) + .max(1000, { + message: t('Comment content should be at most 1000 characters'), + }) + .transform((val) => { + // Remove any HTML tags by applying the replacement repeatedly until no changes occur + let sanitized = val; + let previousValue; + + do { + previousValue = sanitized; + sanitized = sanitized.replace(/<[^>]*>/g, ''); + } while (sanitized !== previousValue); + + return sanitized; + }), + }); -export const createTaskSchema = z.object({ - riskId: z.string().min(1, { - message: 'Risk ID is required', - }), - title: z.string().min(1, { - message: 'Task title is required', - }), - description: z.string().min(1, { - message: 'Task description is required', - }), - dueDate: z.date().optional(), - assigneeId: z.string().optional().nullable(), -}); +export const getCreateTaskSchema = (t: (content: string) => string) => + z.object({ + riskId: z.string().min(1, { + message: t('Risk ID is required'), + }), + title: z.string().min(1, { + message: t('Task title is required'), + }), + description: z.string().min(1, { + message: t('Task description is required'), + }), + dueDate: z.date().optional(), + assigneeId: z.string().optional().nullable(), + }); -export const updateTaskSchema = z.object({ - id: z.string().min(1, { - message: 'Task ID is required', - }), - title: z.string().optional(), - description: z.string().optional(), - dueDate: z.date().optional(), - status: z.nativeEnum(TaskStatus, { - required_error: 'Task status is required', - }), - assigneeId: z.string().optional().nullable(), -}); +export const getUpdateTaskSchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, { + message: t('Task ID is required'), + }), + title: z.string().optional(), + description: z.string().optional(), + dueDate: z.date().optional(), + status: z.nativeEnum(TaskStatus, { + required_error: t('Task status is required'), + }), + assigneeId: z.string().optional().nullable(), + }); -export const createTaskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: 'Risk ID is required', - }), - taskId: z.string().min(1, { - message: 'Task ID is required', - }), - content: z - .string() - .min(1, { - message: 'Comment content is required', - }) - .max(1000, { - message: 'Comment content should be at most 1000 characters', - }) - .transform((val) => { - // Remove any HTML tags by applying the replacement repeatedly until no changes occur - let sanitized = val; - let previousValue; - - do { - previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } while (sanitized !== previousValue); - - return sanitized; +export const getCreateTaskCommentSchema = (t: (content: string) => string) => + z.object({ + riskId: z.string().min(1, { + message: t('Risk ID is required'), }), -}); + taskId: z.string().min(1, { + message: t('Task ID is required'), + }), + content: z + .string() + .min(1, { + message: t('Comment content is required'), + }) + .max(1000, { + message: t('Comment content should be at most 1000 characters'), + }) + .transform((val) => { + // Remove any HTML tags by applying the replacement repeatedly until no changes occur + let sanitized = val; + let previousValue; + + do { + previousValue = sanitized; + sanitized = sanitized.replace(/<[^>]*>/g, ''); + } while (sanitized !== previousValue); + + return sanitized; + }), + }); -export const uploadTaskFileSchema = z.object({ - riskId: z.string().min(1, { - message: 'Risk ID is required', - }), - taskId: z.string().min(1, { - message: 'Task ID is required', - }), -}); +export const getUploadTaskFileSchema = (t: (content: string) => string) => + z.object({ + riskId: z.string().min(1, { + message: t('Risk ID is required'), + }), + taskId: z.string().min(1, { + message: t('Task ID is required'), + }), + }); // Integrations -export const deleteIntegrationConnectionSchema = z.object({ - integrationName: z.string().min(1, { - message: 'Integration name is required', - }), -}); +export const getDeleteIntegrationConnectionSchema = (t: (content: string) => string) => + z.object({ + integrationName: z.string().min(1, { + message: t('Integration name is required'), + }), + }); -export const createIntegrationSchema = z.object({ - integrationId: z.string().min(1, { - message: 'Integration ID is required', - }), -}); +export const getCreateIntegrationSchema = (t: (content: string) => string) => + z.object({ + integrationId: z.string().min(1, { + message: t('Integration ID is required'), + }), + }); // Seed Data export const seedDataSchema = z.object({ organizationId: z.string(), }); -export const updateInherentRiskSchema = z.object({ - id: z.string().min(1, { - message: 'Risk ID is required', - }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), -}); +export const getUpdateInherentRiskSchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, { + message: t('Risk ID is required'), + }), + probability: z.nativeEnum(Likelihood), + impact: z.nativeEnum(Impact), + }); -export const updateResidualRiskSchema = z.object({ - id: z.string().min(1, { - message: 'Risk ID is required', - }), - probability: z.number().min(1).max(10), - impact: z.number().min(1).max(10), -}); +export const getUpdateResidualRiskSchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, { + message: t('Risk ID is required'), + }), + probability: z.number().min(1).max(10), + impact: z.number().min(1).max(10), + }); // ADD START: Schema for enum-based residual risk update -export const updateResidualRiskEnumSchema = z.object({ - id: z.string().min(1, { - message: 'Risk ID is required', - }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), -}); +export const getUpdateResidualRiskEnumSchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, { + message: t('Risk ID is required'), + }), + probability: z.nativeEnum(Likelihood), + impact: z.nativeEnum(Impact), + }); + // ADD END // Policies -export const createPolicySchema = z.object({ - title: z.string({ required_error: 'Title is required' }).min(1, 'Title is required'), - description: z - .string({ required_error: 'Description is required' }) - .min(1, 'Description is required'), - frameworkIds: z.array(z.string()).optional(), - controlIds: z.array(z.string()).optional(), - entityId: z.string().optional(), -}); - -export type CreatePolicySchema = z.infer; - -export const updatePolicySchema = z.object({ - id: z.string(), - content: z.any(), - entityId: z.string(), -}); +export const getCreatePolicySchema = (t: (content: string) => string) => + z.object({ + title: z.string({ required_error: t('Title is required') }).min(1, t('Title is required')), + description: z + .string({ required_error: t('Description is required') }) + .min(1, t('Description is required')), + frameworkIds: z.array(z.string()).optional(), + controlIds: z.array(z.string()).optional(), + entityId: z.string().optional(), + }); + +export type CreatePolicySchema = z.infer>; + +export const getUpdatePolicySchema = (t: (content: string) => string) => + z.object({ + id: z.string(), + content: z.any(), + entityId: z.string(), + }); -export const addFrameworksSchema = z.object({ - organizationId: z.string().min(1, 'Organization ID is required'), - frameworkIds: z.array(z.string()).min(1, 'Please select at least one framework to add'), -}); +export const getAddFrameworksSchema = (t: (content: string) => string) => + z.object({ + organizationId: z.string().min(1, t('Organization ID is required')), + frameworkIds: z.array(z.string()).min(1, t('Please select at least one framework to add')), + }); export const assistantSettingsSchema = z.object({ enabled: z.boolean().optional(), }); -export const createEmployeeSchema = z.object({ - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email address'), - department: z.nativeEnum(Departments, { - required_error: 'Department is required', - }), - externalEmployeeId: z.string().optional(), - isActive: z.boolean().default(true), -}); - -export const updatePolicyOverviewSchema = z.object({ - id: z.string(), - title: z.string(), - description: z.string(), - isRequiredToSign: z.enum(['required', 'not_required']).optional(), - entityId: z.string(), -}); - -export const updatePolicyFormSchema = z.object({ - id: z.string(), - status: z.nativeEnum(PolicyStatus), - assigneeId: z.string().optional().nullable(), - department: z.nativeEnum(Departments), - review_frequency: z.nativeEnum(Frequency), - review_date: z.date(), - isRequiredToSign: z.enum(['required', 'not_required']), - approverId: z.string().optional().nullable(), // Added for selecting an approver - entityId: z.string(), -}); - -export const apiKeySchema = z.object({ - name: z - .string() - .min(1, { message: 'Name is required' }) - .max(64, { message: 'Name must be less than 64 characters' }), - expiresAt: z.enum(['30days', '90days', '1year', 'never']), -}); - -export const createPolicyCommentSchema = z.object({ - policyId: z.string().min(1, { - message: 'Policy ID is required', - }), - content: z - .string() - .min(1, { - message: 'Comment content is required', - }) - .max(1000, { - message: 'Comment content should be at most 1000 characters', - }) - .transform((val) => { - // Remove any HTML tags by applying the replacement repeatedly until no changes occur - let sanitized = val; - let previousValue; - - do { - previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } while (sanitized !== previousValue); - - return sanitized; +export const getCreateEmployeeSchema = (t: (content: string) => string) => + z.object({ + name: z.string().min(1, t('Name is required')), + email: z.string().email(t('Invalid email address')), + department: z.nativeEnum(Departments, { + required_error: t('Department is required'), }), -}); + externalEmployeeId: z.string().optional(), + isActive: z.boolean().default(true), + }); -export const addCommentSchema = z.object({ - content: z - .string() - .min(1, 'Comment content is required') - .max(1000, 'Comment content should be at most 1000 characters') - .transform((val) => { - // Remove any HTML tags by applying the replacement repeatedly until no changes occur - let sanitized = val; - let previousValue; - - do { - previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } while (sanitized !== previousValue); - - return sanitized; +export const getUpdatePolicyOverviewSchema = (t: (content: string) => string) => + z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + isRequiredToSign: z.enum(['required', 'not_required']).optional(), + entityId: z.string(), + }); + +export const getUpdatePolicyFormSchema = (t: (content: string) => string) => + z.object({ + id: z.string(), + status: z.nativeEnum(PolicyStatus), + assigneeId: z.string().optional().nullable(), + department: z.nativeEnum(Departments), + review_frequency: z.nativeEnum(Frequency), + review_date: z.date(), + isRequiredToSign: z.enum(['required', 'not_required']), + approverId: z.string().optional().nullable(), // Added for selecting an approver + entityId: z.string(), + }); + +export const getApiKeySchema = (t: (content: string) => string) => + z.object({ + name: z + .string() + .min(1, { message: t('Name is required') }) + .max(64, { message: t('Name must be less than 64 characters') }), + expiresAt: z.enum(['30days', '90days', '1year', 'never']), + }); + +export const getCreatePolicyCommentSchema = (t: (content: string) => string) => + z.object({ + policyId: z.string().min(1, { + message: t('Policy ID is required'), }), - entityId: z.string().min(1, 'Entity ID is required'), - entityType: z.nativeEnum(CommentEntityType), -}); - -export const createContextEntrySchema = z.object({ - question: z.string().min(1, 'Question is required'), - answer: z.string().min(1, 'Answer is required'), - tags: z.string().optional(), // comma separated -}); + content: z + .string() + .min(1, { + message: t('Comment content is required'), + }) + .max(1000, { + message: t('Comment content should be at most 1000 characters'), + }) + .transform((val) => { + // Remove any HTML tags by applying the replacement repeatedly until no changes occur + let sanitized = val; + let previousValue; + + do { + previousValue = sanitized; + sanitized = sanitized.replace(/<[^>]*>/g, ''); + } while (sanitized !== previousValue); + + return sanitized; + }), + }); -export const updateContextEntrySchema = z.object({ - id: z.string().min(1, 'ID is required'), - question: z.string().min(1, 'Question is required'), - answer: z.string().min(1, 'Answer is required'), - tags: z.string().optional(), -}); +export const getAddCommentSchema = (t: (content: string) => string) => + z.object({ + content: z + .string() + .min(1, t('Comment content is required')) + .max(1000, t('Comment content should be at most 1000 characters')) + .transform((val) => { + // Remove any HTML tags by applying the replacement repeatedly until no changes occur + let sanitized = val; + let previousValue; + + do { + previousValue = sanitized; + sanitized = sanitized.replace(/<[^>]*>/g, ''); + } while (sanitized !== previousValue); + + return sanitized; + }), + entityId: z.string().min(1, t('Entity ID is required')), + entityType: z.nativeEnum(CommentEntityType), + }); -export const deleteContextEntrySchema = z.object({ - id: z.string().min(1, 'ID is required'), -}); +export const getCreateContextEntrySchema = (t: (content: string) => string) => + z.object({ + question: z.string().min(1, t('Question is required')), + answer: z.string().min(1, t('Answer is required')), + tags: z.string().optional(), // comma separated + }); -// Comment schemas for the new generic comments API -export const createCommentSchema = z.object({ - content: z.string().min(1, 'Comment content is required'), - entityId: z.string(), - entityType: z.nativeEnum(CommentEntityType), - attachments: z - .array( - z.object({ - fileName: z.string(), - fileType: z.string(), - fileData: z.string(), // base64 - }), - ) - .optional(), -}); +export const getUpdateContextEntrySchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, t('ID is required')), + question: z.string().min(1, t('Question is required')), + answer: z.string().min(1, t('Answer is required')), + tags: z.string().optional(), + }); -export type CreateCommentSchema = z.infer; +export const getDeleteContextEntrySchema = (t: (content: string) => string) => + z.object({ + id: z.string().min(1, t('ID is required')), + }); -export const updateCommentSchema = z.object({ - commentId: z.string(), - content: z.string().min(1, 'Comment content is required'), -}); +// Comment schemas for the new generic comments API +export const getCreateCommentSchema = (t: (content: string) => string) => + z.object({ + content: z.string().min(1, t('Comment content is required')), + entityId: z.string(), + entityType: z.nativeEnum(CommentEntityType), + attachments: z + .array( + z.object({ + fileName: z.string(), + fileType: z.string(), + fileData: z.string(), // base64 + }), + ) + .optional(), + }); + +export type CreateCommentSchema = z.infer>; + +export const getUpdateCommentSchema = (t: (content: string) => string) => + z.object({ + commentId: z.string(), + content: z.string().min(1, t('Comment content is required')), + }); -export type UpdateCommentSchema = z.infer; +export type UpdateCommentSchema = z.infer>; export const deleteCommentSchema = z.object({ commentId: z.string(), diff --git a/apps/app/src/actions/send-feedback-action.ts b/apps/app/src/actions/send-feedback-action.ts index a200b2e68..65f6fcd32 100644 --- a/apps/app/src/actions/send-feedback-action.ts +++ b/apps/app/src/actions/send-feedback-action.ts @@ -2,11 +2,15 @@ import { env } from '@/env.mjs'; import axios from 'axios'; +import { getGT } from 'gt-next/server'; import { authActionClient } from './safe-action'; -import { sendFeedbackSchema } from './schema'; +import { getSendFeedbackSchema } from './schema'; export const sendFeebackAction = authActionClient - .inputSchema(sendFeedbackSchema) + .inputSchema(async () => { + const t = await getGT(); + return getSendFeedbackSchema(t); + }) .metadata({ name: 'send-feedback', }) diff --git a/apps/app/src/actions/update-menu-action.ts b/apps/app/src/actions/update-menu-action.ts index e43ca2c96..f6ba4f7db 100644 --- a/apps/app/src/actions/update-menu-action.ts +++ b/apps/app/src/actions/update-menu-action.ts @@ -2,12 +2,16 @@ import { Cookies } from '@/utils/constants'; import { addYears } from 'date-fns'; +import { getGT } from 'gt-next/server'; import { cookies } from 'next/headers'; import { authActionClient } from './safe-action'; -import { updaterMenuSchema } from './schema'; +import { getUpdaterMenuSchema } from './schema'; export const updateMenuAction = authActionClient - .inputSchema(updaterMenuSchema) + .inputSchema(async () => { + const t = await getGT(); + return getUpdaterMenuSchema(t); + }) .metadata({ name: 'update-menu', }) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 122310065..d28d12fb6 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -5,14 +5,15 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp import type { Onboarding } from '@db'; import { useRealtimeRun } from '@trigger.dev/react-hooks'; import { AnimatePresence, motion } from 'framer-motion'; +import { T, useGT, Var } from 'gt-next'; import { AlertTriangle, Rocket, ShieldAlert, Zap } from 'lucide-react'; import { useEffect, useState } from 'react'; -const PROGRESS_MESSAGES = [ - 'Learning about your company...', - 'Creating Risks...', - 'Creating Vendors...', - 'Tailoring Policies...', +const getProgressMessages = (t: (content: string) => string) => [ + t('Learning about your company...'), + t('Creating Risks...'), + t('Creating Vendors...'), + t('Tailoring Policies...'), ]; const IN_PROGRESS_STATUSES = [ @@ -33,6 +34,7 @@ const getFriendlyStatusName = (status: string): string => { }; export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => { + const t = useGT(); const [currentMessageIndex, setCurrentMessageIndex] = useState(0); const triggerJobId = onboarding.triggerJobId; @@ -47,35 +49,44 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => let interval: NodeJS.Timeout; if (run && IN_PROGRESS_STATUSES.includes(run.status)) { interval = setInterval(() => { - setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % PROGRESS_MESSAGES.length); + const progressMessages = getProgressMessages(t); + setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % progressMessages.length); }, 4000); } else { setCurrentMessageIndex(0); // Reset when not in progress } return () => clearInterval(interval); - }, [run, triggerJobId]); + }, [run, triggerJobId, t]); if (!triggerJobId) { - return
Unable to load onboarding tracker.
; + return ( + +
Unable to load onboarding tracker.
+
+ ); } if (!triggerJobId) { return ( - Onboarding Status + + {t('Onboarding Status')} + - Organization setup has not started yet. + {t('Organization setup has not started yet.')}
{/* Use theme warning color */}
-

Awaiting Initiation

-

- No onboarding process has been started. -

+ +

Awaiting Initiation

+

+ No onboarding process has been started. +

+
@@ -89,10 +100,12 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
-

Initializing Status

-

- Checking the current onboarding status... -

+ +

Initializing Status

+

+ Checking the current onboarding status... +

+
); @@ -102,11 +115,13 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
{/* Use theme warning color */}
-

Status Unavailable

{' '} - {/* Use theme warning color */} -

- Could not retrieve current onboarding status. -

+ +

Status Unavailable

{' '} + {/* Use theme warning color */} +

+ Could not retrieve current onboarding status. +

+
); @@ -134,12 +149,14 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.3 }} > - {PROGRESS_MESSAGES[currentMessageIndex]} + {getProgressMessages(t)[currentMessageIndex]} -

- We are setting up your organization. This may take a few moments. -

+ +

+ We are setting up your organization. This may take a few moments. +

+
); @@ -148,8 +165,10 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
-

Setup Complete

-

Your organization is ready.

+ +

Setup Complete

+

Your organization is ready.

+
); @@ -160,7 +179,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => case 'SYSTEM_FAILURE': case 'EXPIRED': case 'TIMED_OUT': { - const errorMessage = run.error?.message || 'An unexpected issue occurred.'; + const errorMessage = run.error?.message || t('An unexpected issue occurred.'); const truncatedMessage = errorMessage.length > 100 ? `${errorMessage.substring(0, 97)}...` : errorMessage; return ( @@ -168,7 +187,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {' '}

- Setup {friendlyStatus} + {t('Setup')} {friendlyStatus}

{truncatedMessage}

@@ -182,10 +201,12 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
-

Unknown Status

-

- Received an unhandled status: {exhaustiveCheck} -

+ +

Unknown Status

+

+ Received an unhandled status: {exhaustiveCheck} +

+
); diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/ControlDeleteDialog.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/ControlDeleteDialog.tsx index b9e77bd38..1147b4564 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/ControlDeleteDialog.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/ControlDeleteDialog.tsx @@ -13,6 +13,7 @@ import { import { Form } from '@comp/ui/form'; import { Control } from '@db'; import { zodResolver } from '@hookform/resolvers/zod'; +import { T, useGT } from 'gt-next'; import { Trash2 } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useRouter } from 'next/navigation'; @@ -34,6 +35,7 @@ interface ControlDeleteDialogProps { } export function ControlDeleteDialog({ isOpen, onClose, control }: ControlDeleteDialogProps) { + const t = useGT(); const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); @@ -46,12 +48,12 @@ export function ControlDeleteDialog({ isOpen, onClose, control }: ControlDeleteD const deleteControl = useAction(deleteControlAction, { onSuccess: () => { - toast.info('Control deleted! Redirecting to controls list...'); + toast.info(t('Control deleted! Redirecting to controls list...')); onClose(); router.push(`/${control.organizationId}/controls`); }, onError: () => { - toast.error('Failed to delete control.'); + toast.error(t('Failed to delete control.')); setIsSubmitting(false); }, }); @@ -68,27 +70,31 @@ export function ControlDeleteDialog({ isOpen, onClose, control }: ControlDeleteD !open && onClose()}> - Delete Control - - Are you sure you want to delete this control? This action cannot be undone. - + + Delete Control + + + + Are you sure you want to delete this control? This action cannot be undone. + +
diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx index 0f0c7aa21..f1fb13a43 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/PoliciesTable.tsx @@ -8,6 +8,7 @@ import { Icons } from '@comp/ui/icons'; import { Input } from '@comp/ui/input'; import { Policy } from '@db'; import { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import { useMemo, useState } from 'react'; interface PoliciesTableProps { @@ -18,12 +19,13 @@ interface PoliciesTableProps { export function PoliciesTable({ policies, orgId, controlId }: PoliciesTableProps) { const [searchTerm, setSearchTerm] = useState(''); + const t = useGT(); const columns = useMemo[]>( () => [ { accessorKey: 'name', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const name = row.original.name; return {name}; @@ -37,7 +39,7 @@ export function PoliciesTable({ policies, orgId, controlId }: PoliciesTableProps }, { accessorKey: 'createdAt', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => {new Date(row.original.createdAt).toLocaleDateString()}, enableSorting: true, sortingFn: (rowA, rowB) => { @@ -48,14 +50,14 @@ export function PoliciesTable({ policies, orgId, controlId }: PoliciesTableProps }, { accessorKey: 'status', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const rawStatus = row.original.status; return ; }, }, ], - [], + [t], ); const filteredPolicies = useMemo(() => { @@ -85,7 +87,7 @@ export function PoliciesTable({ policies, orgId, controlId }: PoliciesTableProps
setSearchTerm(e.target.value)} className="max-w-sm" diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx index 1a8be77fb..29e5a8480 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/RequirementsTable.tsx @@ -12,6 +12,7 @@ import type { RequirementMap, } from '@db'; import { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import { useMemo, useState } from 'react'; interface RequirementsTableProps { @@ -26,6 +27,7 @@ interface RequirementsTableProps { export function RequirementsTable({ requirements, orgId }: RequirementsTableProps) { const [searchTerm, setSearchTerm] = useState(''); + const t = useGT(); // Define columns for requirements table const columns = useMemo< @@ -42,7 +44,7 @@ export function RequirementsTable({ requirements, orgId }: RequirementsTableProp { id: 'reqName', accessorKey: 'requirement.name', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { return ( @@ -62,7 +64,7 @@ export function RequirementsTable({ requirements, orgId }: RequirementsTableProp { id: 'reqDescription', accessorKey: 'requirement.description', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { return ( @@ -79,7 +81,7 @@ export function RequirementsTable({ requirements, orgId }: RequirementsTableProp }, }, ], - [], + [t], ); // Filter requirements data based on search term @@ -116,7 +118,7 @@ export function RequirementsTable({ requirements, orgId }: RequirementsTableProp
setSearchTerm(e.target.value)} className="max-w-sm" diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx index 737f34c03..900a57c25 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControl.tsx @@ -18,6 +18,7 @@ import type { RequirementMap, Task, } from '@db'; +import { T, useGT } from 'gt-next'; import { MoreVertical, Trash2 } from 'lucide-react'; import { useParams } from 'next/navigation'; import { useMemo, useState } from 'react'; @@ -50,6 +51,7 @@ export function SingleControl({ }: SingleControlProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); + const t = useGT(); const params = useParams<{ orgId: string; controlId: string }>(); const orgIdFromParams = params.orgId; const controlIdFromParams = params.controlId; @@ -100,7 +102,7 @@ export function SingleControl({ className="text-destructive focus:text-destructive" > - Delete + Delete @@ -111,19 +113,25 @@ export function SingleControl({ - Requirements + + Requirements + {control.requirementsMapped.length} - Policies + + Policies + {relatedPolicies.length} - Tasks + + Tasks + {relatedTasks.length} diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControlSkeleton.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControlSkeleton.tsx index 4fbb1f5bb..0e2e0f49a 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControlSkeleton.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/SingleControlSkeleton.tsx @@ -1,6 +1,7 @@ import { CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { Card } from '@comp/ui/card'; +import { T } from 'gt-next'; export const SingleControlSkeleton = () => { return ( @@ -17,7 +18,9 @@ export const SingleControlSkeleton = () => { - Domain + + Domain +
diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx index ac56bbceb..a4160ade1 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/TasksTable.tsx @@ -8,6 +8,7 @@ import { Icons } from '@comp/ui/icons'; import { Input } from '@comp/ui/input'; import { Task } from '@db'; import { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import { useMemo, useState } from 'react'; interface TasksTableProps { @@ -18,13 +19,14 @@ interface TasksTableProps { export function TasksTable({ tasks, orgId, controlId }: TasksTableProps) { const [searchTerm, setSearchTerm] = useState(''); + const t = useGT(); // Define columns for tasks table const columns = useMemo[]>( () => [ { accessorKey: 'title', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const title = row.original.title; return {title}; @@ -38,7 +40,7 @@ export function TasksTable({ tasks, orgId, controlId }: TasksTableProps) { }, { accessorKey: 'description', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const description = row.original.description; return {description}; @@ -46,7 +48,7 @@ export function TasksTable({ tasks, orgId, controlId }: TasksTableProps) { }, { accessorKey: 'status', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const rawStatus = row.original.status; @@ -55,7 +57,7 @@ export function TasksTable({ tasks, orgId, controlId }: TasksTableProps) { }, }, ], - [], + [t], ); // Filter tasks data based on search term @@ -88,7 +90,7 @@ export function TasksTable({ tasks, orgId, controlId }: TasksTableProps) {
setSearchTerm(e.target.value)} className="max-w-sm" diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTable.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTable.tsx index 64e52dbea..504fca863 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTable.tsx @@ -4,6 +4,7 @@ import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-tabl import { Table, TableBody, TableCell, TableRow } from '@comp/ui/table'; import type { FrameworkEditorRequirement, Policy, Task } from '@db'; +import { useGT } from 'gt-next'; import { useParams, useRouter } from 'next/navigation'; import { ControlRequirementsTableColumns } from './ControlRequirementsTableColumns'; import { ControlRequirementsTableHeader } from './ControlRequirementsTableHeader'; @@ -19,12 +20,13 @@ interface DataTableProps { } export function ControlRequirementsTable({ data }: DataTableProps) { + const t = useGT(); const router = useRouter(); const { orgId } = useParams<{ orgId: string }>(); const table = useReactTable({ data, - columns: ControlRequirementsTableColumns, + columns: ControlRequirementsTableColumns(), getCoreRowModel: getCoreRowModel(), }); @@ -72,7 +74,7 @@ export function ControlRequirementsTable({ data }: DataTableProps) { colSpan={ControlRequirementsTableColumns.length} className="h-24 text-center" > - No requirements found. + {t('No requirements found.')} )} diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableColumns.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableColumns.tsx index bfe1c61af..6bf2e8ec2 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableColumns.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableColumns.tsx @@ -1,60 +1,67 @@ 'use client'; import type { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import { CheckCircle2, XCircle } from 'lucide-react'; import type { RequirementTableData } from './ControlRequirementsTable'; -export const ControlRequirementsTableColumns: ColumnDef[] = [ - { - id: 'type', - accessorKey: 'type', - header: 'Type', - cell: ({ row }) => { - const requirement = row.original; - return requirement.policy ? 'policy' : requirement.task ? 'task' : ''; +export function ControlRequirementsTableColumns(): ColumnDef[] { + const t = useGT(); + + return [ + { + id: 'type', + accessorKey: 'type', + header: t('Type'), + cell: ({ row }) => { + const requirement = row.original; + return requirement.policy ? t('policy') : requirement.task ? t('task') : ''; + }, + size: 100, }, - size: 100, - }, - { - id: 'description', - accessorKey: 'description', - header: 'Description', - size: 1000, - cell: ({ row }) => { - const description = row.original.description || ''; // Default to empty string if null - const maxLength = 300; // Increased character limit - const displayText = - description.length > maxLength ? `${description.substring(0, maxLength)}...` : description; + { + id: 'description', + accessorKey: 'description', + header: t('Description'), + size: 1000, + cell: ({ row }) => { + const description = row.original.description || ''; // Default to empty string if null + const maxLength = 300; // Increased character limit + const displayText = + description.length > maxLength + ? `${description.substring(0, maxLength)}...` + : description; - return ( -
- {displayText} -
- ); + return ( +
+ {displayText} +
+ ); + }, }, - }, - { - id: 'status', - accessorKey: 'status', - header: 'Status', - size: 80, - cell: ({ row }) => { - const requirement = row.original; - const isCompleted = requirement.policy - ? requirement.policy?.status === 'published' - : requirement.task - ? requirement.task?.status === 'done' - : false; + { + id: 'status', + accessorKey: 'status', + header: t('Status'), + size: 80, + cell: ({ row }) => { + const requirement = row.original; + const isCompleted = requirement.policy + ? requirement.policy?.status === 'published' + : requirement.task + ? requirement.task?.status === 'done' + : false; - return ( -
- {isCompleted ? ( - - ) : ( - - )} -
- ); + return ( +
+ {isCompleted ? ( + + ) : ( + + )} +
+ ); + }, }, - }, -]; + ]; +} diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableHeader.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableHeader.tsx index 2f95bf465..27481848a 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableHeader.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/table/ControlRequirementsTableHeader.tsx @@ -2,6 +2,7 @@ import { TableHead, TableHeader, TableRow } from '@comp/ui/table'; import type { Table } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import type { RequirementTableData } from './ControlRequirementsTable'; type Props = { @@ -10,6 +11,7 @@ type Props = { }; export function ControlRequirementsTableHeader({ table, loading }: Props) { + const t = useGT(); const isVisible = (id: string) => loading || table @@ -21,15 +23,19 @@ export function ControlRequirementsTableHeader({ table, loading }: Props) { {isVisible('type') && ( - Type + + {t('Type')} + )} {isVisible('description') && ( - Description + {t('Description')} )} {isVisible('status') && ( - Status + + {t('Status')} + )} diff --git a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/page.tsx b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/page.tsx index cce2a12d7..0f81c0200 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/[controlId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/[controlId]/page.tsx @@ -1,5 +1,6 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { auth } from '@/utils/auth'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { SingleControl } from './components/SingleControl'; @@ -52,10 +53,12 @@ export default async function ControlPage({ params }: ControlPageProps) { controlId: controlId, }); + const t = await getGT(); + return ( diff --git a/apps/app/src/app/(app)/[orgId]/controls/components/controls-table-columns.tsx b/apps/app/src/app/(app)/[orgId]/controls/components/controls-table-columns.tsx index 82f92bf1a..785738f5d 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/components/controls-table-columns.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/components/controls-table-columns.tsx @@ -2,17 +2,21 @@ import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'; import { StatusIndicator } from '@/components/status-indicator'; -import { ColumnDef } from '@tanstack/react-table'; +import { Column, ColumnDef, Row } from '@tanstack/react-table'; import { ControlWithRelations } from '../data/queries'; import { getControlStatus } from '../lib/utils'; -export function getControlColumns(): ColumnDef[] { +export const getGetControlColumns = ( + t: (content: string) => string, +): ColumnDef[] => { return [ { id: 'name', accessorKey: 'name', - header: ({ column }) => , - cell: ({ row }) => { + header: ({ column }: { column: Column }) => ( + + ), + cell: ({ row }: { row: Row }) => { return (
{row.getValue('name')} @@ -20,12 +24,12 @@ export function getControlColumns(): ColumnDef[] { ); }, meta: { - label: 'Control Name', - placeholder: 'Search for a control...', - variant: 'text', + label: t('Control Name'), + placeholder: t('Search for a control...'), + variant: 'text' as const, }, enableColumnFilter: true, - filterFn: (row, id, value) => { + filterFn: (row: Row, id: string, value: string) => { return value.length === 0 ? true : String(row.getValue(id)).toLowerCase().includes(String(value).toLowerCase()); @@ -34,19 +38,21 @@ export function getControlColumns(): ColumnDef[] { { id: 'status', accessorKey: '', - header: ({ column }) => , - cell: ({ row }) => { + header: ({ column }: { column: Column }) => ( + + ), + cell: ({ row }: { row: Row }) => { const control = row.original; const status = getControlStatus(control); return ; }, meta: { - label: 'Status', - placeholder: 'Search status...', - variant: 'text', + label: t('Status'), + placeholder: t('Search status...'), + variant: 'text' as const, }, enableSorting: false, }, ]; -} +}; diff --git a/apps/app/src/app/(app)/[orgId]/controls/components/controls-table.tsx b/apps/app/src/app/(app)/[orgId]/controls/components/controls-table.tsx index 53024728e..3ff531ce3 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/components/controls-table.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/components/controls-table.tsx @@ -6,9 +6,10 @@ import { DataTable } from '@/components/data-table/data-table'; import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'; import { CreatePolicySheet } from '@/components/sheets/create-policy-sheet'; import { useDataTable } from '@/hooks/use-data-table'; +import { useGT } from 'gt-next'; import { useParams } from 'next/navigation'; import { ControlWithRelations } from '../data/queries'; -import { getControlColumns } from './controls-table-columns'; +import { getGetControlColumns } from './controls-table-columns'; interface ControlsTableProps { promises: Promise<[{ data: ControlWithRelations[]; pageCount: number }]>; @@ -17,7 +18,8 @@ interface ControlsTableProps { export function ControlsTable({ promises }: ControlsTableProps) { const [{ data, pageCount }] = React.use(promises); const { orgId } = useParams(); - const columns = React.useMemo(() => getControlColumns(), []); + const t = useGT(); + const columns = React.useMemo(() => getGetControlColumns(t), [t]); const [filteredData, setFilteredData] = React.useState(data); // For client-side filtering, we don't need to apply server-side filtering diff --git a/apps/app/src/app/(app)/[orgId]/controls/page.tsx b/apps/app/src/app/(app)/[orgId]/controls/page.tsx index 8a5e11fdb..bdec24081 100644 --- a/apps/app/src/app/(app)/[orgId]/controls/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/controls/page.tsx @@ -1,5 +1,6 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { getValidFilters } from '@/lib/data-table'; +import { getGT } from 'gt-next/server'; import { Metadata } from 'next'; import { SearchParams } from 'nuqs'; import { ControlsTable } from './components/controls-table'; @@ -11,8 +12,9 @@ interface ControlTableProps { } export async function generateMetadata(): Promise { + const t = await getGT(); return { - title: 'Controls', + title: t('Controls'), }; } @@ -20,6 +22,7 @@ export default async function ControlsPage({ ...props }: ControlTableProps) { const searchParams = await props.searchParams; const search = searchParamsCache.parse(searchParams); const validFilters = getValidFilters(search.filters); + const t = await getGT(); const promises = Promise.all([ getControls({ @@ -29,7 +32,7 @@ export default async function ControlsPage({ ...props }: ControlTableProps) { ]); return ( - + ); diff --git a/apps/app/src/app/(app)/[orgId]/error.tsx b/apps/app/src/app/(app)/[orgId]/error.tsx index 67b4ceae3..2c869d823 100644 --- a/apps/app/src/app/(app)/[orgId]/error.tsx +++ b/apps/app/src/app/(app)/[orgId]/error.tsx @@ -1,6 +1,7 @@ 'use client'; import { Button } from '@comp/ui/button'; +import { T, useGT } from 'gt-next'; import Link from 'next/link'; import { useEffect } from 'react'; @@ -11,6 +12,8 @@ export default function ErrorPage({ reset: () => void; error: Error & { digest?: string }; }) { + const t = useGT(); + useEffect(() => { console.error('app/(app)/(dashboard)/[orgId]/error.tsx', error); }, [error]); @@ -19,20 +22,22 @@ export default function ErrorPage({
-

Something went wrong

-

- An unexpected error has occurred. Please try again -
or contact support if the issue persists. -

+ +

Something went wrong

+

+ An unexpected error has occurred. Please try again +
or contact support if the issue persists. +

+
- +
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/actions/delete-framework.ts b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/actions/delete-framework.ts index fb12ad4cc..9dad61f85 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/actions/delete-framework.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/actions/delete-framework.ts @@ -2,6 +2,7 @@ import { authActionClient } from '@/actions/safe-action'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath, revalidateTag } from 'next/cache'; import { z } from 'zod'; @@ -23,11 +24,12 @@ export const deleteFrameworkAction = authActionClient .action(async ({ parsedInput, ctx }) => { const { id } = parsedInput; const { activeOrganizationId } = ctx.session; + const t = await getGT(); if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: t('Not authorized'), }; } @@ -42,7 +44,7 @@ export const deleteFrameworkAction = authActionClient if (!frameworkInstance) { return { success: false, - error: 'Framework instance not found', + error: t('Framework instance not found'), }; } @@ -62,7 +64,7 @@ export const deleteFrameworkAction = authActionClient console.error(error); return { success: false, - error: 'Failed to delete framework instance', + error: t('Failed to delete framework instance'), }; } }); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx index 362f79e55..190ef8832 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkDeleteDialog.tsx @@ -11,6 +11,7 @@ import { } from '@comp/ui/dialog'; import { Form } from '@comp/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; +import { T, useGT } from 'gt-next'; import { Trash2 } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useRouter } from 'next/navigation'; @@ -40,6 +41,7 @@ export function FrameworkDeleteDialog({ }: FrameworkDeleteDialogProps) { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + const t = useGT(); const form = useForm({ resolver: zodResolver(formSchema), @@ -50,12 +52,12 @@ export function FrameworkDeleteDialog({ const deleteFramework = useAction(deleteFrameworkAction, { onSuccess: () => { - toast.info('Framework deleted! Redirecting to frameworks list...'); + toast.info(t('Framework deleted! Redirecting to frameworks list...')); onClose(); router.push(`/${frameworkInstance.organizationId}/frameworks`); }, onError: () => { - toast.error('Failed to delete framework.'); + toast.error(t('Failed to delete framework.')); setIsSubmitting(false); }, }); @@ -72,28 +74,34 @@ export function FrameworkDeleteDialog({ !open && onClose()}> - Delete Framework + + Delete Framework + - Are you sure you want to delete this framework? This action cannot be undone. + Are you sure you want to delete this framework? This action cannot be undone. diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx index 975c5384c..029307efb 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkOverview.tsx @@ -12,6 +12,7 @@ import { } from '@comp/ui/dropdown-menu'; import { Progress } from '@comp/ui/progress'; import { Control, Task } from '@db'; +import { T, useGT } from 'gt-next'; import { BarChart3, MoreVertical, Target, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { getControlStatus } from '../../lib/utils'; @@ -29,6 +30,7 @@ export function FrameworkOverview({ }: FrameworkOverviewProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); + const t = useGT(); // Get all controls from all requirements const allControls = frameworkInstanceWithControls.controls; @@ -87,7 +89,7 @@ export function FrameworkOverview({ className="text-destructive focus:text-destructive" > - Delete Framework + Delete Framework @@ -100,7 +102,7 @@ export function FrameworkOverview({ - Compliance Progress + Compliance Progress @@ -115,15 +117,23 @@ export function FrameworkOverview({ > {compliancePercentage} - % complete + + % complete +
- {compliantControls} completed - {inProgressControls} remaining - {totalControls} total + + {compliantControls} completed + + + {inProgressControls} remaining + + + {totalControls} total +
@@ -133,26 +143,32 @@ export function FrameworkOverview({ - Control Status + Control Status
- Complete + + Complete +
{compliantControls}
- In Progress + + In Progress +
{inProgressControls}
- Total + + Total + {totalControls}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index af2626a21..7969e1c9a 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -6,6 +6,7 @@ import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'; import { useDataTable } from '@/hooks/use-data-table'; import type { FrameworkEditorRequirement } from '@db'; import { ColumnDef } from '@tanstack/react-table'; +import { T, useGT } from 'gt-next'; import { useParams } from 'next/navigation'; import { useMemo } from 'react'; import type { FrameworkInstanceWithControls } from '../../types'; @@ -25,6 +26,7 @@ export function FrameworkRequirements({ orgId: string; frameworkInstanceId: string; }>(); + const t = useGT(); const items = useMemo(() => { return requirementDefinitions.map((def) => { @@ -44,7 +46,7 @@ export function FrameworkRequirements({ () => [ { accessorKey: 'name', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( {row.original.name} ), @@ -53,15 +55,15 @@ export function FrameworkRequirements({ minSize: 150, maxSize: 250, meta: { - label: 'Requirement Name', - placeholder: 'Search...', + label: t('Requirement Name'), + placeholder: t('Search...'), variant: 'text', }, enableColumnFilter: true, }, { accessorKey: 'description', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( {row.original.description} ), @@ -73,7 +75,7 @@ export function FrameworkRequirements({ }, { accessorKey: 'mappedControlsCount', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( {row.original.mappedControlsCount} ), @@ -84,7 +86,7 @@ export function FrameworkRequirements({ enableResizing: true, }, ], - [], + [t], ); const table = useDataTable({ @@ -105,7 +107,7 @@ export function FrameworkRequirements({ return (

- Requirements ({table.table.getFilteredRowModel().rows.length}) + Requirements ({table.table.getFilteredRowModel().rows.length})

- No controls found. + No controls found. )} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableColumns.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableColumns.tsx index 4f9f7acac..abe6224a4 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableColumns.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableColumns.tsx @@ -4,6 +4,7 @@ import { StatusIndicator, StatusType } from '@/components/status-indicator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@comp/ui/tooltip'; import type { Policy } from '@db'; import type { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import Link from 'next/link'; import { useParams } from 'next/navigation'; @@ -35,13 +36,14 @@ function isPolicyCompleted(policy: Policy): boolean { } export function FrameworkControlsTableColumns(): ColumnDef[] { + const t = useGT(); const { orgId } = useParams<{ orgId: string }>(); return [ { id: 'name', accessorKey: 'name', - header: 'Control', + header: t('Control'), cell: ({ row }) => { return (
@@ -55,7 +57,7 @@ export function FrameworkControlsTableColumns(): ColumnDef (
{row.original.name} @@ -65,7 +67,7 @@ export function FrameworkControlsTableColumns(): ColumnDef { const policies = row.original.policies || []; const status = getControlStatusForPolicies(policies); @@ -83,9 +85,16 @@ export function FrameworkControlsTableColumns(): ColumnDef
-

Progress: {Math.round((completedPolicies / totalPolicies) * 100) || 0}%

- Completed: {completedPolicies}/{totalPolicies} policies + {t('Progress: {progress}%', { + progress: Math.round((completedPolicies / totalPolicies) * 100) || 0, + })} +

+

+ {t('Completed: {completed}/{total} policies', { + completed: completedPolicies, + total: totalPolicies, + })}

diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableHeader.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableHeader.tsx index da89f8381..0e12a514e 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableHeader.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/table/FrameworkControlsTableHeader.tsx @@ -2,6 +2,7 @@ import { Button } from '@comp/ui/button'; import { TableHead, TableHeader, TableRow } from '@comp/ui/table'; +import { useGT } from 'gt-next'; import { ArrowDown, ArrowUp } from 'lucide-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useCallback } from 'react'; @@ -21,6 +22,7 @@ type Props = { }; export function FrameworkControlsTableHeader({ table, loading }: Props) { + const t = useGT(); const searchParams = useSearchParams(); const pathname = usePathname(); const router = useRouter(); @@ -62,7 +64,7 @@ export function FrameworkControlsTableHeader({ table, loading }: Props) { variant="ghost" onClick={() => createSortQuery('name')} > - {'Control'} + {t('Control')} {'name' === column && value === 'asc' && } {'name' === column && value === 'desc' && } @@ -76,7 +78,7 @@ export function FrameworkControlsTableHeader({ table, loading }: Props) { variant="ghost" onClick={() => createSortQuery('category')} > - {'Category'} + {t('Category')} {'category' === column && value === 'asc' && } {'category' === column && value === 'desc' && } @@ -90,7 +92,7 @@ export function FrameworkControlsTableHeader({ table, loading }: Props) { variant="ghost" onClick={() => createSortQuery('status')} > - {'Status'} + {t('Status')} {'status' === column && value === 'asc' && } {'status' === column && value === 'desc' && } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx index 1bde771dd..59259e3e3 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx @@ -1,6 +1,7 @@ 'use client'; import type { Control, FrameworkEditorRequirement, RequirementMap, Task } from '@db'; +import { T } from 'gt-next'; import { RequirementControlsTable } from './table/RequirementControlsTable'; interface RequirementControlsProps { @@ -28,7 +29,9 @@ export function RequirementControls({
-

Controls

+ +

Controls

+
{relatedControls.length} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx index 5307600d9..637dae485 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx @@ -6,6 +6,7 @@ import { useDataTable } from '@/hooks/use-data-table'; import { Input } from '@comp/ui/input'; import type { Control, Task } from '@db'; import { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useMemo, useState } from 'react'; @@ -16,6 +17,7 @@ interface RequirementControlsTableProps { } export function RequirementControlsTable({ controls, tasks }: RequirementControlsTableProps) { + const t = useGT(); const { orgId } = useParams<{ orgId: string }>(); const [searchTerm, setSearchTerm] = useState(''); @@ -25,7 +27,7 @@ export function RequirementControlsTable({ controls, tasks }: RequirementControl { id: 'name', accessorKey: 'name', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => (
@@ -40,7 +42,7 @@ export function RequirementControlsTable({ controls, tasks }: RequirementControl enableResizing: true, }, ], - [orgId], + [orgId, t], ); // Filter controls data based on search term @@ -68,7 +70,7 @@ export function RequirementControlsTable({ controls, tasks }: RequirementControl
setSearchTerm(e.target.value)} className="max-w-sm" diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx index 95eba77d2..4857d9b96 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTableColumns.tsx @@ -5,6 +5,7 @@ import { isPolicyCompleted } from '@/lib/control-compliance'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@comp/ui/tooltip'; import type { Control, Policy, Task } from '@db'; import type { ColumnDef } from '@tanstack/react-table'; +import { useGT } from 'gt-next'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { getControlStatus } from '../../../../../lib/utils'; @@ -19,12 +20,13 @@ export function RequirementControlsTableColumns({ tasks: (Task & { controls: Control[] })[]; }): ColumnDef[] { const { orgId } = useParams<{ orgId: string }>(); + const t = useGT(); return [ { id: 'name', accessorKey: 'name', - header: 'Control', + header: t('Control'), cell: ({ row }) => { return (
@@ -38,7 +40,7 @@ export function RequirementControlsTableColumns({ { id: 'status', accessorKey: 'policies', - header: 'Status', + header: t('Status'), cell: ({ row }) => { const controlData = row.original; const policies = controlData.policies || []; @@ -58,9 +60,16 @@ export function RequirementControlsTableColumns({
-

Progress: {Math.round((completedPolicies / totalPolicies) * 100) || 0}%

- Completed: {completedPolicies}/{totalPolicies} policies + {t('Progress: {progress}%', { + progress: Math.round((completedPolicies / totalPolicies) * 100) || 0, + })} +

+

+ {t('Completed: {completed}/{total} policies', { + completed: completedPolicies, + total: totalPolicies, + })}

diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx index a4259e09b..93f8183b5 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/page.tsx @@ -2,6 +2,7 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { auth } from '@/utils/auth'; import type { FrameworkEditorRequirement } from '@db'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { getSingleFrameworkInstanceWithControls } from '../../../data/getSingleFrameworkInstanceWithControls'; @@ -15,6 +16,7 @@ interface PageProps { } export default async function RequirementPage({ params }: PageProps) { + const t = await getGT(); const { frameworkInstanceId, requirementKey } = await params; const session = await auth.api.getSession({ @@ -95,7 +97,7 @@ export default async function RequirementPage({ params }: PageProps) { return ( getAddFrameworksSchema(t), [t]); const form = useForm>({ resolver: zodResolver(addFrameworksSchema), defaultValues: { @@ -45,10 +49,12 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat const { execute, isExecuting } = useAction(addFrameworksToOrganizationAction, { onSuccess: (data) => { + const count = data.data?.frameworksAdded ?? 0; toast.success( - `Successfully added ${data.data?.frameworksAdded ?? 0} framework${ - data.data?.frameworksAdded && data.data?.frameworksAdded > 1 ? 's' : '' - }`, + t('Successfully added {count} framework{plural}', { + count, + plural: count > 1 ? 's' : '', + }), ); onOpenChange(false); router.refresh(); @@ -58,14 +64,14 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat toast.error(error.error.serverError); } else if (error.error.validationErrors) { const errorMessages = Object.values(error.error.validationErrors).flat().join(', '); - toast.error(errorMessages || 'Validation error occurred'); + toast.error(errorMessages || t('Validation error occurred')); } else { - toast.error('Failed to add frameworks'); + toast.error(t('Failed to add frameworks')); } }, }); - const onSubmit = async (data: z.infer) => { + const onSubmit = async (data: z.infer>) => { execute(data); }; @@ -77,12 +83,18 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat return ( - Add Frameworks - - {availableFrameworks.length > 0 - ? 'Select the compliance frameworks to add to your organization.' - : 'No new frameworks are available to add at this time.'} - + + Add Frameworks + + + + 0).toString()} + true="Select the compliance frameworks to add to your organization." + false="No new frameworks are available to add at this time." + /> + + {!isExecuting && availableFrameworks.length > 0 && ( @@ -93,7 +105,9 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat name="frameworkIds" render={({ field }) => ( - Available Frameworks + + Available Frameworks +
{availableFrameworks @@ -103,7 +117,7 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat key={framework.id} framework={framework} isSelected={field.value.includes(framework.id)} - onSelectionChange={(checked) => { + onSelectionChange={(checked: boolean) => { const newValue = checked ? [...field.value, framework.id] : field.value.filter((id) => id !== framework.id); @@ -119,23 +133,27 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat /> - - + + + + + + @@ -143,27 +161,33 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat {!isExecuting && availableFrameworks.length === 0 && (
-
- All available frameworks are already enabled in your organization. -
+ +
+ All available frameworks are already enabled in your organization. +
+
- + + +
)} {isExecuting && ( -
- - Adding frameworks... -
+ +
+ + Adding frameworks... +
+
)} ); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx index 86de5fd77..f23bcdcc2 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; import { Progress } from '@comp/ui/progress'; import type { Control, Task } from '@db'; +import { Num, T, useGT, Var } from 'gt-next'; import { BarChart3, Clock } from 'lucide-react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; @@ -16,34 +17,35 @@ interface FrameworkCardProps { tasks: (Task & { controls: Control[] })[]; } +const getStatusBadge = (score: number, t: (content: string) => string) => { + if (score >= 95) + return { + label: t('Compliant'), + variant: 'default' as const, + }; + if (score >= 80) + return { + label: t('Nearly Compliant'), + variant: 'secondary' as const, + }; + if (score >= 50) + return { + label: t('In Progress'), + variant: 'outline' as const, + }; + return { + label: t('Needs Attention'), + variant: 'destructive' as const, + }; +}; + export function FrameworkCard({ frameworkInstance, complianceScore = 0, tasks, }: FrameworkCardProps) { const { orgId } = useParams<{ orgId: string }>(); - - const getStatusBadge = (score: number) => { - if (score >= 95) - return { - label: 'Compliant', - variant: 'default' as const, - }; - if (score >= 80) - return { - label: 'Nearly Compliant', - variant: 'secondary' as const, - }; - if (score >= 50) - return { - label: 'In Progress', - variant: 'outline' as const, - }; - return { - label: 'Needs Attention', - variant: 'destructive' as const, - }; - }; + const t = useGT(); const getComplianceColor = (score: number) => { if (score >= 80) return 'text-green-600 dark:text-green-400'; @@ -86,7 +88,7 @@ export function FrameworkCard({ // Use direct framework data: const frameworkDetails = frameworkInstance.framework; - const statusBadge = getStatusBadge(complianceScore); + const statusBadge = getStatusBadge(complianceScore, t); // Calculate last activity date - use current date as fallback const lastActivityDate = new Date().toLocaleDateString('en-US', { @@ -118,10 +120,12 @@ export function FrameworkCard({ {/* Progress Section */}
-
- - Progress -
+ +
+ + Progress +
+
{complianceScore}% @@ -130,17 +134,29 @@ export function FrameworkCard({
{/* Stats */} -
- {compliantControlsCount} complete - {inProgressCount} active - {controlsCount} total -
+ +
+ + {compliantControlsCount} complete + + + {inProgressCount} active + + + {controlsCount} total + +
+
{/* Footer */} -
- - Updated {lastActivityDate} -
+ +
+ + + Updated {lastActivityDate} + +
+
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx index 74b3822b1..41c3425ac 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx @@ -4,6 +4,7 @@ import { Button } from '@comp/ui/button'; import { Dialog } from '@comp/ui/dialog'; import type { FrameworkEditorFramework } from '@db'; import { Control, Task } from '@db'; +import { useGT } from 'gt-next'; import { PlusIcon } from 'lucide-react'; import { useParams } from 'next/navigation'; import { useState } from 'react'; @@ -22,6 +23,7 @@ export function FrameworksOverview({ tasks, allFrameworks, }: FrameworksOverviewProps) { + const t = useGT(); const params = useParams<{ orgId: string }>(); const organizationId = params.orgId; const [isAddFrameworkModalOpen, setIsAddFrameworkModalOpen] = useState(false); @@ -37,7 +39,7 @@ export function FrameworksOverview({
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/error.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/error.tsx index 80cb95834..fc74fcab1 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/error.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/error.tsx @@ -1,5 +1,6 @@ 'use client'; +import { T } from 'gt-next'; import { useEffect } from 'react'; export default function ErrorPage({ @@ -14,11 +15,13 @@ export default function ErrorPage({ }, [error]); return ( -
-

Something went wrong!

- -
+ +
+

Something went wrong!

+ +
+
); } diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx index 546836e31..2c95f8f25 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx @@ -2,6 +2,7 @@ import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb'; import { CheckoutCompleteTracking } from '@/components/tracking/CheckoutCompleteTracking'; import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; import { cache } from 'react'; @@ -9,12 +10,14 @@ import { FrameworksOverview } from './components/FrameworksOverview'; import { getAllFrameworkInstancesWithControls } from './data/getAllFrameworkInstancesWithControls'; export async function generateMetadata() { + const t = await getGT(); return { - title: 'Frameworks', + title: t('Frameworks'), }; } export default async function DashboardPage() { + const t = await getGT(); const session = await auth.api.getSession({ headers: await headers(), }); @@ -37,7 +40,7 @@ export default async function DashboardPage() { }); return ( - + { + const t = await getGT(); return { - title: 'Integrations', + title: t('Integrations'), }; } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts index aa95073b8..66bbde46b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/get-employee-details.ts @@ -3,8 +3,9 @@ import { authActionClient } from '@/actions/safe-action'; import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; -import { type AppError, appErrors, employeeDetailsInputSchema } from '../types'; +import { type AppError, employeeDetailsInputSchema, getAppErrors } from '../types'; // Type-safe action response export type ActionResponse = Promise< @@ -28,9 +29,11 @@ export const getEmployeeDetails = authActionClient }); const organizationId = session?.session.activeOrganizationId; + const t = await getGT(); + const appErrors = getAppErrors(t); if (!organizationId) { - throw new Error('Organization ID not found'); + throw new Error(t('Organization ID not found')); } try { diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts index 13f166395..06fd5d8c1 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-department.ts @@ -4,9 +4,10 @@ import { authActionClient } from '@/actions/safe-action'; import { auth } from '@/utils/auth'; import type { Departments } from '@db'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; -import { type AppError, appErrors, updateEmployeeDepartmentSchema } from '../types'; +import { type AppError, getAppErrors, updateEmployeeDepartmentSchema } from '../types'; export type ActionResponse = Promise< { success: true; data: T } | { success: false; error: AppError } @@ -29,6 +30,8 @@ export const updateEmployeeDepartment = authActionClient }); const organizationId = session?.session.activeOrganizationId; + const t = await getGT(); + const appErrors = getAppErrors(t); if (!organizationId) { return { diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts index 39e12a432..bf9f54407 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-details.ts @@ -3,19 +3,20 @@ import { authActionClient } from '@/actions/safe-action'; import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; -import { appErrors } from '../types'; +import { getAppErrors } from '../types'; -const schema = z.object({ - employeeId: z.string(), - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email address'), -}); +const getSchema = (t: Awaited>) => + z.object({ + employeeId: z.string(), + name: z.string().min(1, t('Name is required')), + email: z.string().email(t('Invalid email address')), + }); export const updateEmployeeDetails = authActionClient - .inputSchema(schema) .metadata({ name: 'update-employee-details', track: { @@ -26,8 +27,21 @@ export const updateEmployeeDetails = authActionClient .action( async ({ parsedInput, + ctx, }): Promise<{ success: true; data: any } | { success: false; error: any }> => { - const { employeeId, name, email } = parsedInput; + const t = await getGT(); + const schema = getSchema(t); + const appErrors = getAppErrors(t); + + const parseResult = schema.safeParse(parsedInput); + if (!parseResult.success) { + return { + success: false, + error: parseResult.error.errors[0]?.message || t('Invalid input'), + }; + } + + const { employeeId, name, email } = parseResult.data; const session = await auth.api.getSession({ headers: await headers(), diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts index bde8ccf74..ea5538206 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee-status.ts @@ -3,10 +3,11 @@ import { authActionClient } from '@/actions/safe-action'; import { auth } from '@/utils/auth'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import { z } from 'zod'; -import { appErrors } from '../types'; +import { getAppErrors } from '../types'; const schema = z.object({ employeeId: z.string(), @@ -33,6 +34,8 @@ export const updateEmployeeStatus = authActionClient }); const organizationId = session?.session.activeOrganizationId; + const t = await getGT(); + const appErrors = getAppErrors(t); if (!organizationId) { return { diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts index cee6a511a..c020b14c3 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/update-employee.ts @@ -3,9 +3,10 @@ import { authActionClient } from '@/actions/safe-action'; import type { Departments } from '@db'; import { db, Prisma } from '@db'; +import { getGT } from 'gt-next/server'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; -import { appErrors } from '../types'; +import { getAppErrors } from '../types'; const schema = z.object({ employeeId: z.string(), @@ -27,6 +28,8 @@ export const updateEmployee = authActionClient }) .action(async ({ parsedInput, ctx }) => { const { employeeId, name, email, department, isActive, createdAt } = parsedInput; + const t = await getGT(); + const appErrors = getAppErrors(t); const organizationId = ctx.session.activeOrganizationId; if (!organizationId) throw new Error(appErrors.UNAUTHORIZED.message); @@ -110,7 +113,7 @@ export const updateEmployee = authActionClient if (error.code === 'P2002') { const targetFields = error.meta?.target as string[] | undefined; if (targetFields?.includes('email')) { - throw new Error('Email address is already in use.'); + throw new Error(t('Email address is already in use.')); } } } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx index 2f503b158..18bd65517 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EditableDepartment.tsx @@ -2,19 +2,20 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import type { Departments } from '@db'; +import { useGT } from 'gt-next'; import { useAction } from 'next-safe-action/hooks'; import { useState } from 'react'; import { toast } from 'sonner'; import { updateEmployeeDepartment } from '../actions/update-department'; -const DEPARTMENTS = [ - { value: 'admin', label: 'Admin' }, - { value: 'gov', label: 'Governance' }, - { value: 'hr', label: 'HR' }, - { value: 'it', label: 'IT' }, - { value: 'itsm', label: 'IT Service Management' }, - { value: 'qms', label: 'Quality Management' }, - { value: 'none', label: 'None' }, +const getDepartments = (t: ReturnType) => [ + { value: 'admin', label: t('Admin') }, + { value: 'gov', label: t('Governance') }, + { value: 'hr', label: t('HR') }, + { value: 'it', label: t('IT') }, + { value: 'itsm', label: t('IT Service Management') }, + { value: 'qms', label: t('Quality Management') }, + { value: 'none', label: t('None') }, ]; interface EditableDepartmentProps { @@ -29,14 +30,16 @@ export function EditableDepartment({ onSuccess, }: EditableDepartmentProps) { const [department, setDepartment] = useState(currentDepartment); + const t = useGT(); + const DEPARTMENTS = getDepartments(t); const { execute, status } = useAction(updateEmployeeDepartment, { onSuccess: () => { - toast.success('Department updated successfully'); + toast.success(t('Department updated successfully')); onSuccess?.(); }, onError: (error) => { - toast.error(error?.error?.serverError || 'Failed to update department'); + toast.error(error?.error?.serverError || t('Failed to update department')); }, }); @@ -48,7 +51,7 @@ export function EditableDepartment({
setStatus(value as EmployeeStatusType)}> - + {STATUS_OPTIONS.map((option) => ( diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx index df5cc75f0..596bea34e 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeDetails.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@comp/ui/c import { Form } from '@comp/ui/form'; import type { Departments, Member, User } from '@db'; import { zodResolver } from '@hookform/resolvers/zod'; +import { T, useGT } from 'gt-next'; import { Save } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useForm } from 'react-hook-form'; @@ -17,7 +18,7 @@ import { JoinDate } from './Fields/JoinDate'; import { Name } from './Fields/Name'; import { Status } from './Fields/Status'; -// Define form schema with Zod +// Define form schema with Zod - validation messages will be translated in the component const employeeFormSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email address'), @@ -35,6 +36,7 @@ export const EmployeeDetails = ({ user: User; }; }) => { + const t = useGT(); const form = useForm({ resolver: zodResolver(employeeFormSchema), defaultValues: { @@ -49,10 +51,10 @@ export const EmployeeDetails = ({ const { execute, status: actionStatus } = useAction(updateEmployee, { onSuccess: () => { - toast.success('Employee details updated successfully'); + toast.success(t('Employee details updated successfully')); }, onError: (error) => { - toast.error(error?.error?.serverError || 'Failed to update employee details'); + toast.error(error?.error?.serverError || t('Failed to update employee details')); }, }); @@ -91,17 +93,21 @@ export const EmployeeDetails = ({ await execute(updateData); } else { // No changes were made - toast.info('No changes to save'); + toast.info(t('No changes to save')); } }; return ( - Employee Details -

- Manage employee information and department assignment -

+ + Employee Details + + +

+ Manage employee information and department assignment +

+
@@ -126,7 +132,9 @@ export const EmployeeDetails = ({ {!(form.formState.isSubmitting || actionStatus === 'executing') && ( )} - {form.formState.isSubmitting || actionStatus === 'executing' ? 'Saving...' : 'Save'} + {form.formState.isSubmitting || actionStatus === 'executing' + ? t('Saving...') + : t('Save')} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx index 35a938e3f..95ad696d9 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx @@ -4,6 +4,7 @@ import type { EmployeeTrainingVideoCompletion, Member, Policy, User } from '@db' import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@comp/ui/tabs'; +import { DateTime, T, useGT } from 'gt-next'; import { AlertCircle, CheckCircle2, XCircle } from 'lucide-react'; import type { FleetPolicy, Host } from '../../devices/types'; @@ -26,31 +27,46 @@ export const EmployeeTasks = ({ fleetPolicies: FleetPolicy[]; isFleetEnabled: boolean; }) => { + const t = useGT(); return (
-

Employee Tasks

-

- View and manage employee tasks and their status -

+ +

Employee Tasks

+
+ +

+ View and manage employee tasks and their status +

+
- Policies - Training Videos - {isFleetEnabled && Device} + + Policies + + + Training Videos + + {isFleetEnabled && ( + + Device + + )}
{policies.length === 0 ? (
-

No policies required to sign.

+ +

No policies required to sign.

+
) : ( policies.map((policy) => { @@ -80,7 +96,9 @@ export const EmployeeTasks = ({
{trainingVideos.length === 0 ? (
-

No training videos required to watch.

+ +

No training videos required to watch.

+
) : ( trainingVideos.map((video) => { @@ -102,11 +120,12 @@ export const EmployeeTasks = ({ )} {video.metadata.title}
- {isCompleted && ( - - Completed -{' '} - {video.completedAt && new Date(video.completedAt).toLocaleDateString()} - + {isCompleted && video.completedAt && ( + + + Completed - {new Date(video.completedAt)} + + )}
@@ -121,7 +140,10 @@ export const EmployeeTasks = ({ {host ? ( - {host.computer_name}'s Policies + + {host.computer_name} + {t("'s Policies")} + {fleetPolicies.map((policy) => ( @@ -136,12 +158,16 @@ export const EmployeeTasks = ({ {policy.response === 'pass' ? (
- Pass + + Pass +
) : (
- Fail + + Fail +
)}
@@ -150,7 +176,9 @@ export const EmployeeTasks = ({ ) : (
-

No device found.

+ +

No device found.

+
)} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Department.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Department.tsx index 6bcc57493..e39baad85 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Department.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Department.tsx @@ -1,20 +1,26 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import type { Departments } from '@db'; +import { T, useGT } from 'gt-next'; import type { Control } from 'react-hook-form'; import type { EmployeeFormValues } from '../EmployeeDetails'; -const DEPARTMENTS: { value: Departments; label: string }[] = [ - { value: 'admin', label: 'Admin' }, - { value: 'gov', label: 'Governance' }, - { value: 'hr', label: 'HR' }, - { value: 'it', label: 'IT' }, - { value: 'itsm', label: 'IT Service Management' }, - { value: 'qms', label: 'Quality Management' }, - { value: 'none', label: 'None' }, +const getDepartments = ( + t: (content: string) => string, +): { value: Departments; label: string }[] => [ + { value: 'admin', label: t('Admin') }, + { value: 'gov', label: t('Governance') }, + { value: 'hr', label: t('HR') }, + { value: 'it', label: t('IT') }, + { value: 'itsm', label: t('IT Service Management') }, + { value: 'qms', label: t('Quality Management') }, + { value: 'none', label: t('None') }, ]; export const Department = ({ control }: { control: Control }) => { + const t = useGT(); + const departments = getDepartments(t); + return ( } render={({ field }) => ( - Department + Department + diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx index 0c96a087d..34fc805c1 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx @@ -4,11 +4,13 @@ import { cn } from '@comp/ui/cn'; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; import { Popover, PopoverContent, PopoverTrigger } from '@comp/ui/popover'; import { format } from 'date-fns'; +import { T, useGT } from 'gt-next'; import { CalendarIcon } from 'lucide-react'; import type { Control } from 'react-hook-form'; import type { EmployeeFormValues } from '../EmployeeDetails'; export const JoinDate = ({ control }: { control: Control }) => { + const t = useGT(); return ( }) render={({ field }) => ( - Join Date + Join Date @@ -28,7 +30,13 @@ export const JoinDate = ({ control }: { control: Control }) !field.value && 'text-muted-foreground', )} > - {field.value ? format(field.value, 'PPP') : Pick a date} + {field.value ? ( + format(field.value, 'PPP') + ) : ( + + Pick a date + + )} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Name.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Name.tsx index 8ddb5101a..0ea6b74a7 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Name.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Name.tsx @@ -1,9 +1,11 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; import { Input } from '@comp/ui/input'; +import { T, useGT } from 'gt-next'; import type { Control } from 'react-hook-form'; import type { EmployeeFormValues } from '../EmployeeDetails'; export const Name = ({ control }: { control: Control }) => { + const t = useGT(); return ( }) => { render={({ field }) => ( - NAME + NAME - + diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Status.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Status.tsx index a46d9ccbc..14fa0f2c4 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Status.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/Status.tsx @@ -2,12 +2,15 @@ import type { EmployeeStatusType } from '@/components/tables/people/employee-sta import { cn } from '@comp/ui/cn'; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; +import { T, useGT } from 'gt-next'; import type { Control } from 'react-hook-form'; import type { EmployeeFormValues } from '../EmployeeDetails'; -const STATUS_OPTIONS: { value: EmployeeStatusType; label: string }[] = [ - { value: 'active', label: 'Active' }, - { value: 'inactive', label: 'Inactive' }, +const getStatusOptions = ( + t: (key: string) => string, +): { value: EmployeeStatusType; label: string }[] => [ + { value: 'active', label: t('Active') }, + { value: 'inactive', label: t('Inactive') }, ]; // Status color hex values for charts @@ -17,6 +20,9 @@ export const EMPLOYEE_STATUS_HEX_COLORS: Record = { }; export const Status = ({ control }: { control: Control }) => { + const t = useGT(); + const statusOptions = getStatusOptions(t); + return ( }) => render={({ field }) => ( - Status + Status @@ -447,11 +472,15 @@ mike@company.com,admin`; name={`manualInvites.${index}.roles`} render={({ field: { onChange, value }, fieldState: { error } }) => ( - {index === 0 && {'Role'}} + {index === 0 && ( + + Role + + )} {error?.message} @@ -464,7 +493,7 @@ mike@company.com,admin`; onClick={() => fields.length > 1 && remove(index)} disabled={fields.length <= 1} className={`mt-${index === 0 ? '6' : '0'} self-center ${fields.length <= 1 ? 'cursor-not-allowed opacity-50' : ''}`} - aria-label="Remove invite" + aria-label={t('Remove invite')} > @@ -483,9 +512,11 @@ mike@company.com,admin`; } > - Add Another + Add Another - {'Add an employee to your organization.'} + + Add an employee to your organization. + @@ -494,17 +525,19 @@ mike@company.com,admin`; name="csvFile" render={({ field: { onChange, value, ...fieldProps } }) => ( - {'CSV File'} + + CSV File +
- {csvFileName || 'No file chosen'} + {csvFileName || t('No file chosen')}
@@ -522,16 +555,17 @@ mike@company.com,admin`; /> - { - "Upload a CSV file with 'email' and 'role' columns. Use pipe (|) to separate multiple roles (e.g., employee|admin)." - } + + Upload a CSV file with 'email' and 'role' columns. Use pipe (|) to + separate multiple roles (e.g., employee|admin). + - {'Download CSV template'} + Download CSV template
@@ -548,11 +582,11 @@ mike@company.com,admin`; disabled={isLoading} className="w-full sm:w-auto" > - {'Cancel'} + Cancel diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 18114c662..b391db86a 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -36,6 +36,7 @@ import { import { Label } from '@comp/ui/label'; import type { Role } from '@db'; +import { T, useGT, Var } from 'gt-next'; import { MultiRoleCombobox } from './MultiRoleCombobox'; import type { MemberWithUser } from './TeamMembers'; @@ -46,7 +47,11 @@ interface MemberRowProps { } // Helper to get initials -function getInitials(name?: string | null, email?: string | null): string { +function getInitials( + name: string | null | undefined, + email: string | null | undefined, + t: (content: string) => string, +): string { if (name) { return name .split(' ') @@ -57,12 +62,13 @@ function getInitials(name?: string | null, email?: string | null): string { if (email) { return email.substring(0, 2).toUpperCase(); } - return '??'; + return t('??'); } export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { const params = useParams<{ orgId: string }>(); const { orgId } = params; + const t = useGT(); const [isRemoveAlertOpen, setIsRemoveAlertOpen] = useState(false); const [isUpdateRolesOpen, setIsUpdateRolesOpen] = useState(false); @@ -137,7 +143,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
- {getInitials(member.user.name, member.user.email)} + {getInitials(member.user.name, member.user.email, t)}
@@ -147,7 +153,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { href={`/${orgId}/people/${memberId}`} className="text-xs text-blue-600 hover:underline" > - ({'View Profile'}) + ({t('View Profile')}) )}
@@ -161,13 +167,13 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { {(() => { switch (role) { case 'owner': - return 'Owner'; + return t('Owner'); case 'admin': - return 'Admin'; + return t('Admin'); case 'auditor': - return 'Auditor'; + return t('Auditor'); case 'employee': - return 'Employee'; + return t('Employee'); default: return '???'; } @@ -209,44 +215,50 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { }} > - {'Edit Roles'} + {t('Edit Roles')} - {'Edit Member Roles'} + {t('Edit Member Roles')} - {'Change roles for'} {memberName} + + Change roles for {memberName} +
- + {isOwner && ( + +

+ The owner role cannot be removed. +

+
+ )} +

- {'The owner role cannot be removed.'} + Members must have at least one role.

- )} -

- {'Members must have at least one role.'} -

+
@@ -258,7 +270,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { onSelect={() => setIsRemoveAlertOpen(true)} > - {'Remove Member'} + {t('Remove Member')} )} @@ -269,16 +281,18 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) { - {'Remove Team Member'} + {t('Remove Team Member')} - {'Are you sure you want to remove'} {memberName}?{' '} - {'They will no longer have access to this organization.'} + + Are you sure you want to remove {memberName}? They will no longer have + access to this organization. + - {'Cancel'} + {t('Cancel')} - {'Remove'} + {t('Remove')} diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx index 79e9a86bc..309176b36 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleCombobox.tsx @@ -4,6 +4,7 @@ import type { Role } from '@db'; import * as React from 'react'; import { Dialog, DialogContent } from '@comp/ui/dialog'; +import { useGT } from 'gt-next'; import { MultiRoleComboboxContent } from './MultiRoleComboboxContent'; import { MultiRoleComboboxTrigger } from './MultiRoleComboboxTrigger'; @@ -52,6 +53,7 @@ export function MultiRoleCombobox({ }: MultiRoleComboboxProps) { const [open, setOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); + const t = useGT(); // Process selected roles to handle comma-separated values const selectedRoles = React.useMemo(() => { @@ -88,32 +90,34 @@ export function MultiRoleCombobox({ const getRoleLabel = (roleValue: Role) => { switch (roleValue) { case 'owner': - return 'Owner'; + return t('Owner'); case 'admin': - return 'Admin'; + return t('Admin'); case 'auditor': - return 'Auditor'; + return t('Auditor'); case 'employee': - return 'Employee'; + return t('Employee'); default: return roleValue; } }; const triggerText = - selectedRoles.length > 0 ? `${selectedRoles.length} selected` : placeholder || 'Select role(s)'; + selectedRoles.length > 0 + ? t('{count} selected', { count: selectedRoles.length }) + : placeholder || t('Select role(s)'); const filteredRoles = availableRoles.filter((role) => { const label = (() => { switch (role.value) { case 'admin': - return 'Admin'; + return t('Admin'); case 'auditor': - return 'Auditor'; + return t('Auditor'); case 'employee': - return 'Employee'; + return t('Employee'); case 'owner': - return 'Owner'; + return t('Owner'); default: return role.value; } diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxContent.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxContent.tsx index 56c320e16..bd03aad03 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxContent.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxContent.tsx @@ -12,6 +12,7 @@ import type { Role } from '@db'; // Assuming Role is from prisma import { Check } from 'lucide-react'; import { cn } from '@comp/ui/cn'; +import { useGT } from 'gt-next'; interface MultiRoleComboboxContentProps { searchTerm: string; @@ -32,16 +33,18 @@ export function MultiRoleComboboxContent({ selectedRoles, onCloseDialog, }: MultiRoleComboboxContentProps) { + const t = useGT(); + const getRoleDisplayLabel = (roleValue: Role) => { switch (roleValue) { case 'owner': - return 'Owner'; + return t('Owner'); case 'admin': - return 'Admin'; + return t('Admin'); case 'auditor': - return 'Auditor'; + return t('Auditor'); case 'employee': - return 'Employee'; + return t('Employee'); default: return roleValue; } @@ -50,13 +53,13 @@ export function MultiRoleComboboxContent({ const getRoleDescription = (roleValue: Role) => { switch (roleValue) { case 'owner': - return 'Can manage users, policies, tasks, and settings, and delete organization.'; + return t('Can manage users, policies, tasks, and settings, and delete organization.'); case 'admin': - return 'Can manage users, policies, tasks, and settings.'; + return t('Can manage users, policies, tasks, and settings.'); case 'auditor': - return 'Read-only access for compliance checks.'; + return t('Read-only access for compliance checks.'); case 'employee': - return 'Can sign policies and complete training.'; + return t('Can sign policies and complete training.'); default: return ''; } @@ -64,9 +67,9 @@ export function MultiRoleComboboxContent({ return ( - + - {'No results found'} + {t('No results found')} {filteredRoles.map((role) => ( {getRoleDisplayLabel(role.value)} {lockedRoles.includes(role.value) && selectedRoles.includes(role.value) && ( - (Locked) + ({t('Locked')}) )}
diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxTrigger.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxTrigger.tsx index ce7eb4fd5..db670d178 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxTrigger.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MultiRoleComboboxTrigger.tsx @@ -5,6 +5,7 @@ import { Button } from '@comp/ui/button'; import { cn } from '@comp/ui/cn'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@comp/ui/tooltip'; import type { Role } from '@db'; // Assuming Role is from prisma +import { T, useGT } from 'gt-next'; import { ChevronsUpDown, Lock, X } from 'lucide-react'; interface MultiRoleComboboxTriggerProps { @@ -28,6 +29,7 @@ export function MultiRoleComboboxTrigger({ onClick, ariaExpanded, }: MultiRoleComboboxTriggerProps) { + const t = useGT(); return (
{/* No secondary email line for invitations */} @@ -108,7 +110,9 @@ export function PendingInvitationRow({ invitation, onCancel }: PendingInvitation - Cancel Invitation + + Cancel Invitation + - Cancel Invitation + + Cancel Invitation + - Are you sure you want to cancel the invitation for {invitation.email}? + + Are you sure you want to cancel the invitation for{' '} + {invitation.email}? +

- This action cannot be undone. + This action cannot be undone.

diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 65e842714..4fc3e2286 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -13,6 +13,7 @@ import { Input } from '@comp/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import { Separator } from '@comp/ui/separator'; import type { Invitation, Role } from '@db'; +import { T, useGT } from 'gt-next'; import { MemberRow } from './MemberRow'; import { PendingInvitationRow } from './PendingInvitationRow'; @@ -49,6 +50,7 @@ export function TeamMembersClient({ removeMemberAction, revokeInvitationAction, }: TeamMembersClientProps) { + const t = useGT(); const router = useRouter(); const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault('')); const [roleFilter, setRoleFilter] = useQueryState('role', parseAsString.withDefault('all')); @@ -126,7 +128,7 @@ export function TeamMembersClient({ router.refresh(); // Add client-side refresh as well } else { // Error case - const errorMessage = result?.serverError || 'Failed to add user'; + const errorMessage = result?.serverError || t('Failed to add user'); console.error('Cancel Invitation Error:', errorMessage); } }; @@ -135,11 +137,11 @@ export function TeamMembersClient({ const result = await removeMemberAction({ memberId }); if (result?.data) { // Success case - toast.success('has been removed from the organization'); + toast.success(t('has been removed from the organization')); router.refresh(); // Add client-side refresh as well } else { // Error case - const errorMessage = result?.serverError || 'Failed to remove member'; + const errorMessage = result?.serverError || t('Failed to remove member'); console.error('Remove Member Error:', errorMessage); toast.error(errorMessage); } @@ -153,13 +155,13 @@ export function TeamMembersClient({ // Client-side check (optional, robust check should be server-side in authClient) if (member && member.role === 'owner' && !rolesArray.includes('owner')) { // Show toast error directly, no need to return an error object - toast.error('The Owner role cannot be removed.'); + toast.error(t('The Owner role cannot be removed.')); return; } // Ensure at least one role is selected if (rolesArray.length === 0) { - toast.warning('Please select at least one role.'); + toast.warning(t('Please select at least one role.')); return; } @@ -169,7 +171,7 @@ export function TeamMembersClient({ memberId: memberId, role: rolesArray, // Pass the array of roles }); - toast.success('Member roles updated successfully.'); + toast.success(t('Member roles updated successfully.')); router.refresh(); // Revalidate data } catch (error) { console.error('Update Role Error:', error); @@ -179,7 +181,7 @@ export function TeamMembersClient({ toast.error(error.message); return; } - toast.error('Failed to update member roles'); + toast.error(t('Failed to update member roles')); } }; @@ -195,7 +197,7 @@ export function TeamMembersClient({
setSearchQuery(e.target.value || null)} leftIcon={} @@ -217,19 +219,29 @@ export function TeamMembersClient({ onValueChange={(value) => setRoleFilter(value === 'all' ? null : value)} > - + - {'All Roles'} - {'Owner'} - {'Admin'} - {'Auditor'} - {'Employee'} + + All Roles + + + Owner + + + Admin + + + Auditor + + + Employee +
@@ -263,13 +275,15 @@ export function TeamMembersClient({ {activeMembers.length === 0 && pendingInvites.length === 0 && (
-

{'No employees yet'}

+

+ No employees yet +

- {'Get started by inviting your first team member.'} + Get started by inviting your first team member.

)} diff --git a/apps/app/src/app/(app)/[orgId]/people/all/page.tsx b/apps/app/src/app/(app)/[orgId]/people/all/page.tsx index a1687716c..b430f4d63 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/page.tsx @@ -1,4 +1,5 @@ import PageCore from '@/components/pages/PageCore.tsx'; +import { getGT } from 'gt-next/server'; import type { Metadata } from 'next'; import { TeamMembers } from './components/TeamMembers'; @@ -11,7 +12,8 @@ export default async function Members() { } export async function generateMetadata(): Promise { + const t = await getGT(); return { - title: 'People', + title: t('People'), }; } diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx index 18270cb3d..6bde1c158 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx @@ -1,6 +1,7 @@ 'use client'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; +import { T, useGT } from 'gt-next'; import type { CSSProperties } from 'react'; import * as React from 'react'; @@ -43,6 +44,7 @@ export function EmployeeCompletionChart({ policies, trainingVideos, }: EmployeeCompletionChartProps) { + const t = useGT(); // Calculate completion data for each employee const employeeStats: EmployeeTaskStats[] = React.useMemo(() => { return employees.map((employee) => { @@ -102,11 +104,13 @@ export function EmployeeCompletionChart({ return ( - {'Employee Task Completion'} + + Employee Task Completion +

- {'No employee data available'} + No employee data available

@@ -118,11 +122,13 @@ export function EmployeeCompletionChart({ return ( - {'Employee Task Completion'} + + Employee Task Completion +

- {'No tasks available to complete'} + No tasks available to complete

@@ -137,7 +143,9 @@ export function EmployeeCompletionChart({ return ( - {'Employee Task Completion'} + + Employee Task Completion +
@@ -146,7 +154,8 @@ export function EmployeeCompletionChart({

{stat.name}

- {stat.policiesCompleted + stat.trainingsCompleted} / {stat.totalTasks} {'tasks'} + {stat.policiesCompleted + stat.trainingsCompleted} / {stat.totalTasks}{' '} + tasks
@@ -155,11 +164,15 @@ export function EmployeeCompletionChart({
- {'Completed'} + + Completed +
- {'Not Completed'} + + Not Completed +
@@ -171,6 +184,7 @@ export function EmployeeCompletionChart({ } function TaskBarChart({ stat }: { stat: EmployeeTaskStats }) { + const t = useGT(); const totalCompleted = stat.policiesCompleted + stat.trainingsCompleted; const totalIncomplete = stat.totalTasks - totalCompleted; const barHeight = 12; @@ -202,7 +216,7 @@ function TaskBarChart({ stat }: { stat: EmployeeTaskStats }) { width: '100%', height: '100%', }} - title={`Completed: ${totalCompleted}`} + title={t('Completed: {count}', { count: totalCompleted })} />
)} @@ -223,7 +237,7 @@ function TaskBarChart({ stat }: { stat: EmployeeTaskStats }) { width: '100%', height: '100%', }} - title={`Incomplete: ${totalIncomplete}`} + title={t('Incomplete: {count}', { count: totalIncomplete })} />
)} diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx index b5faa8fb7..19912dbda 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx @@ -1,4 +1,4 @@ -import { trainingVideos as trainingVideosData } from '@/lib/data/training-videos'; +import { getTrainingVideos } from '@/lib/data/training-videos'; import { auth } from '@/utils/auth'; import type { Member, Policy, User } from '@db'; import { db } from '@db'; @@ -26,6 +26,10 @@ interface ProcessedTrainingVideo { } export async function EmployeesOverview() { + const { getGT } = await import('gt-next/server'); + const t = await getGT(); + const trainingVideosData = getTrainingVideos(t); + const session = await auth.api.getSession({ headers: await headers(), }); @@ -74,7 +78,7 @@ export async function EmployeesOverview() { for (const dbVideo of employeeTrainingVideos) { const videoMetadata = trainingVideosData.find( - (metadataVideo) => metadataVideo.id === dbVideo.videoId, + (metadataVideo: any) => metadataVideo.id === dbVideo.videoId, ); if (videoMetadata) { diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/page.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/page.tsx index 421ad19d7..032c3f3e5 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/page.tsx @@ -1,3 +1,4 @@ +import { getGT } from 'gt-next/server'; import type { Metadata } from 'next'; import { EmployeesOverview } from './components/EmployeesOverview'; @@ -6,7 +7,9 @@ export default async function PeopleOverviewPage() { } export async function generateMetadata(): Promise { + const t = await getGT(); + return { - title: 'People', + title: t('People'), }; } diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx index 13d2cec06..cdfdda3c5 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx @@ -7,6 +7,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@comp/ui/chart'; +import { T, useGT } from 'gt-next'; import * as React from 'react'; import { Cell, Label, Pie, PieChart } from 'recharts'; import type { Host } from '../types'; @@ -21,6 +22,8 @@ const CHART_COLORS = { }; export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { + const t = useGT(); + const { pieDisplayData, legendDisplayData } = React.useMemo(() => { if (!devices || devices.length === 0) { return { pieDisplayData: [], legendDisplayData: [] }; @@ -38,12 +41,12 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { } const allItems = [ { - name: 'Compliant', + name: t('Compliant'), value: compliantCount, fill: CHART_COLORS.compliant, }, { - name: 'Non-Compliant', + name: t('Non-Compliant'), value: nonCompliantCount, fill: CHART_COLORS.nonCompliant, }, @@ -52,7 +55,7 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { pieDisplayData: allItems.filter((item) => item.value > 0), legendDisplayData: allItems, }; - }, [devices]); + }, [devices, t]); const totalDevices = React.useMemo(() => { return devices?.length || 0; @@ -60,14 +63,14 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { const chartConfig = { devices: { - label: 'Devices', + label: t('Devices'), }, compliant: { - label: 'Compliant', + label: t('Compliant'), color: CHART_COLORS.compliant, }, nonCompliant: { - label: 'Non-Compliant', + label: t('Non-Compliant'), color: CHART_COLORS.nonCompliant, }, } satisfies ChartConfig; @@ -76,14 +79,18 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { return ( - Device Compliance + + Device Compliance +
-

- No device data available. Please make sure your employees access the portal and - install the device agent. -

+ +

+ No device data available. Please make sure your employees access the portal and + install the device agent. +

+
@@ -96,7 +103,9 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { return ( - Device Compliance + + Device Compliance + {/* Optional: Add a subtitle or small description here if needed */} @@ -141,7 +150,7 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { y={(viewBox.cy || 0) + 20} className="text-muted-foreground text-sm" > - Devices + {t('Devices')} ); diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesColumns.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesColumns.tsx index 17d681de1..70cb9395f 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesColumns.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesColumns.tsx @@ -1,17 +1,18 @@ 'use client'; import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'; -import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle2, XCircle } from 'lucide-react'; -import type { FleetPolicy, Host } from '../types'; +import type { FleetPolicy } from '../types'; -export function getEmployeeDevicesColumns(): ColumnDef[] { +export const getGetEmployeeDevicesColumns = (t: (content: string) => string) => { return [ { id: 'computer_name', accessorKey: 'computer_name', - header: ({ column }) => , - cell: ({ row }) => { + header: ({ column }: { column: any }) => ( + + ), + cell: ({ row }: { row: any }) => { return (
@@ -26,8 +27,10 @@ export function getEmployeeDevicesColumns(): ColumnDef[] { accessorKey: 'policies', enableColumnFilter: false, enableSorting: false, - header: ({ column }) => , - cell: ({ row }) => { + header: ({ column }: { column: any }) => ( + + ), + cell: ({ row }: { row: any }) => { const policies = row.getValue('policies') as FleetPolicy[]; const isCompliant = policies.every((policy) => policy.response === 'pass'); return isCompliant ? ( @@ -38,4 +41,4 @@ export function getEmployeeDevicesColumns(): ColumnDef[] { }, }, ]; -} +}; diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesList.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesList.tsx index ad806fe08..06bcb44e3 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesList.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesList.tsx @@ -3,14 +3,16 @@ import { DataTable } from '@/components/data-table/data-table'; import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'; import { useDataTable } from '@/hooks/use-data-table'; +import { useGT } from 'gt-next'; import { useMemo, useState } from 'react'; import type { Host } from '../types/index'; -import { getEmployeeDevicesColumns } from './EmployeeDevicesColumns'; +import { getGetEmployeeDevicesColumns } from './EmployeeDevicesColumns'; // This requires a t function to be passed into it import { HostDetails } from './HostDetails'; export const EmployeeDevicesList = ({ devices }: { devices: Host[] }) => { + const t = useGT(); const [selectedRow, setSelectedRow] = useState(null); - const columns = useMemo(() => getEmployeeDevicesColumns(), []); + const columns = useMemo(() => getGetEmployeeDevicesColumns(t), [t]); const { table } = useDataTable({ data: devices, diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/HostDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/HostDetails.tsx index 65bca1673..5fec3be0b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/HostDetails.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/HostDetails.tsx @@ -1,19 +1,24 @@ import { Button } from '@comp/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; +import { T, useGT } from 'gt-next'; import { ArrowLeft, CheckCircle2, XCircle } from 'lucide-react'; import type { Host } from '../types'; export const HostDetails = ({ host, onClose }: { host: Host; onClose: () => void }) => { + const t = useGT(); + return (
- + + + - {host.computer_name}'s Policies + {t('{name}s Policies', { name: host.computer_name })} {host.policies.length > 0 ? ( @@ -29,18 +34,24 @@ export const HostDetails = ({ host, onClose }: { host: Host; onClose: () => void {policy.response === 'pass' ? (
- Pass + + Pass +
) : (
- Fail + + Fail +
)}
)) ) : ( -

No policies found for this device.

+ +

No policies found for this device.

+
)} diff --git a/apps/app/src/app/(app)/[orgId]/people/layout.tsx b/apps/app/src/app/(app)/[orgId]/people/layout.tsx index f0aa85263..45a185372 100644 --- a/apps/app/src/app/(app)/[orgId]/people/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/layout.tsx @@ -2,10 +2,12 @@ import { getPostHogClient } from '@/app/posthog'; import { auth } from '@/utils/auth'; import { SecondaryMenu } from '@comp/ui/secondary-menu'; import { db } from '@db'; +import { getGT } from 'gt-next/server'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; export default async function Layout({ children }: { children: React.ReactNode }) { + const t = await getGT(); const session = await auth.api.getSession({ headers: await headers(), }); @@ -38,13 +40,13 @@ export default async function Layout({ children }: { children: React.ReactNode } items={[ { path: `/${orgId}/people/all`, - label: 'People', + label: t('People'), }, ...(employees.length > 0 ? [ { path: `/${orgId}/people/dashboard`, - label: 'Employee Tasks', + label: t('Employee Tasks'), }, ] : []), @@ -52,7 +54,7 @@ export default async function Layout({ children }: { children: React.ReactNode } ? [ { path: `/${orgId}/people/devices`, - label: 'Employee Devices', + label: t('Employee Devices'), }, ] : []), diff --git a/apps/app/src/app/(app)/[orgId]/people/types.ts b/apps/app/src/app/(app)/[orgId]/people/types.ts index 5fe8a3b30..dece7d0b0 100644 --- a/apps/app/src/app/(app)/[orgId]/people/types.ts +++ b/apps/app/src/app/(app)/[orgId]/people/types.ts @@ -5,16 +5,16 @@ export interface AppError { message: string; } -export const appErrors = { +export const getAppErrors = (t: (content: string) => string) => ({ UNAUTHORIZED: { code: 'UNAUTHORIZED', - message: 'You are not authorized to access this resource', + message: t('You are not authorized to access this resource'), }, UNEXPECTED_ERROR: { code: 'UNEXPECTED_ERROR', - message: 'An unexpected error occurred', + message: t('An unexpected error occurred'), }, -}; +}); export interface EmployeesInput { search?: string; diff --git a/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-assignee-chart.tsx b/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-assignee-chart.tsx index 5cf5afbbf..1b726e09b 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-assignee-chart.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-assignee-chart.tsx @@ -10,6 +10,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@comp/ui/chart'; +import { T, useGT, Var } from 'gt-next'; import { Users } from 'lucide-react'; import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'; @@ -35,6 +36,8 @@ const CHART_COLORS = { }; export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) { + const t = useGT(); + // Sort assignees by total policies (descending) const sortedData = React.useMemo(() => { if (!data || data.length === 0) return []; @@ -60,11 +63,14 @@ export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) {
- {'Policies by Assignee'} - - - Distribution - + + Policies by Assignee + + + + Distribution + +
@@ -72,9 +78,11 @@ export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) {
-

- No policies assigned to users -

+ +

+ No policies assigned to users +

+
@@ -94,19 +102,19 @@ export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) { const chartConfig = { published: { - label: 'Published', + label: t('Published'), color: CHART_COLORS.published, }, draft: { - label: 'Draft', + label: t('Draft'), color: CHART_COLORS.draft, }, archived: { - label: 'Archived', + label: t('Archived'), color: CHART_COLORS.archived, }, needs_review: { - label: 'Needs Review', + label: t('Needs Review'), color: CHART_COLORS.needs_review, }, } satisfies ChartConfig; @@ -115,11 +123,15 @@ export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) {
- {'Policies by Assignee'} + + Policies by Assignee + {topAssignee && ( - - Top: {topAssignee.name} - + + + Top: {topAssignee.name} + + )}
@@ -136,8 +148,12 @@ export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) {
- Assignee - Policy Count + + Assignee + + + Policy Count +
diff --git a/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-status-chart.tsx b/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-status-chart.tsx index f35a200ec..22ec0db99 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-status-chart.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/(overview)/components/policy-status-chart.tsx @@ -11,6 +11,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@comp/ui/chart'; +import { T, useGT, Var } from 'gt-next'; import { Info } from 'lucide-react'; interface PolicyOverviewData { @@ -39,9 +40,14 @@ const StatusTooltip = ({ active, payload }: any) => { return (

{data.name}

-

- Count: {data.value} -

+ +

+ Count:{' '} + + {data.value} + +

+
); } @@ -49,32 +55,34 @@ const StatusTooltip = ({ active, payload }: any) => { }; export function PolicyStatusChart({ data }: PolicyStatusChartProps) { + const t = useGT(); + const chartData = React.useMemo(() => { if (!data) return []; const items = [ { - name: 'Published', + name: t('Published'), value: data.publishedPolicies, fill: CHART_COLORS.published, }, { - name: 'Draft', + name: t('Draft'), value: data.draftPolicies, fill: CHART_COLORS.draft, }, { - name: 'Needs Review', + name: t('Needs Review'), value: data.needsReviewPolicies, fill: CHART_COLORS.needs_review, }, { - name: 'Archived', + name: t('Archived'), value: data.archivedPolicies, fill: CHART_COLORS.archived, }, ]; return items.filter((item) => item.value > 0); - }, [data]); + }, [data, t]); // Calculate most common status const mostCommonStatus = React.useMemo(() => { @@ -87,10 +95,14 @@ export function PolicyStatusChart({ data }: PolicyStatusChartProps) {
- {'Policy by Status'} - - Overview - + + Policy by Status + + + + Overview + +
@@ -98,7 +110,9 @@ export function PolicyStatusChart({ data }: PolicyStatusChartProps) {
-

No policy data available

+ +

No policy data available

+
@@ -110,7 +124,7 @@ export function PolicyStatusChart({ data }: PolicyStatusChartProps) { const chartConfig = { value: { - label: 'Count', + label: t('Count'), }, } satisfies ChartConfig; @@ -118,18 +132,21 @@ export function PolicyStatusChart({ data }: PolicyStatusChartProps) {
- {'Policy by Status'} - + + Policy by Status + {data.totalPolicies > 0 && mostCommonStatus && ( - - Most: {mostCommonStatus.name} - + + + Most: {mostCommonStatus.name} + + )}
@@ -191,7 +208,7 @@ export function PolicyStatusChart({ data }: PolicyStatusChartProps) { y={(viewBox.cy || 0) + 26} className="fill-muted-foreground text-xs" > - Policies + {t('Policies')} { }; export async function generateMetadata(): Promise { + const t = await getGT(); return { - title: 'Policies', + title: t('Policies'), }; } diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyActionDialog.tsx b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyActionDialog.tsx index 82e89eefa..a97a9c8bc 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyActionDialog.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/PolicyActionDialog.tsx @@ -13,6 +13,7 @@ import { import { Form, FormControl, FormField, FormItem } from '@comp/ui/form'; import { Textarea } from '@comp/ui/textarea'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useGT } from 'gt-next'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -44,6 +45,7 @@ export function PolicyActionDialog({ confirmIcon, confirmVariant = 'default', }: PolicyActionDialogProps) { + const t = useGT(); const [isSubmitting, setIsSubmitting] = useState(false); const form = useForm({ @@ -80,7 +82,7 @@ export function PolicyActionDialog({