Versión: 1.0.0
Fecha: 22 de Julio, 2026
Estado: NORMATIVO - Convención de Desarrollo
Arquitecto: Carlos Alberto Torres Camargo
Las traducciones (i18n) de la plataforma se sirven como bundle plano (key_code → valor) desde el servicio admin:
GET /simappe-admin/api/v1/translation/bundle/{langCode}
El frontend recibe ese mapa plano y lo convierte a una estructura anidada (flatToNested) para consumo tipo ngx-translate, donde el punto (.) es el separador de jerarquía.
Las claves nacen en el código del backend (constantes de dominio, p. ej. nebula-accounting-core) y se ingestan a las tablas admin.translation_keys / admin.translations, que alimentan el bundle. i18n SIEMPRE en ES + EN (langId EN=1 / ES=2).
Esta convención aplica a todo el que cree claves de traducción: backend de dominio, admin, y front.
Como el punto es jerarquía, la regla de oro es:
Un nodo es HOJA (mensaje) o NAMESPACE (contenedor), NUNCA ambos.
Ninguna clave puede ser prefijo estricto de otra.
dominio.entidad.tipo.campo[.variante]
dominio — módulo funcional (accounting, billing, …).tipo — naturaleza del texto (error, solucion, label, column, tooltip, message, …).nebula-accounting-core)accounting.causacion.error.cc.requerido ✅ hoja pura
accounting.causacion.solucion.cc.requerido ✅ hoja pura (par error/solución alineado)
accounting.causacion.error.formato.inactivo ✅ 'formato' como segmento, no cuelga de una hoja
accounting.causacion.error.import.formato ✅ 'formato' terminal
Para mensajes de negocio, mantener alineados el error y su solución bajo el mismo campo terminal:
accounting.causacion.error.cc.requerido
accounting.causacion.solucion.cc.requerido
Si coexisten una clave hoja y otra que la usa como prefijo, flatToNested intenta crear una propiedad sobre un string y lanza:
TypeError: Cannot create property 'formato' on string 'El centro de costos no está activo'
at TranslationService.flatToNested (translation.service.ts)
Consecuencia (crítica): con fallbackToStatic:false, el front descarta el bundle COMPLETO → toda la UI sale con las claves crudas (COMMON.*, MODULES.*, ACTIONS.*…), aunque el 99 % de los literales sí existan.
COMMON.GOOD_AFTERNOON, MODULES.ADMIN_NAME sin traducir.HTTP error loading bundle es: Cannot create property '…' on string '…'.accounting.causacion.error.cc.inactivo = "El centro de costos no está activo" (HOJA)
accounting.causacion.error.cc.inactivo.formato = "El centro de costos del formato ..." (cuelga como HIJO → CHOCA)
Al agregar la variante .formato debajo de una clave que ya era hoja, ...cc.inactivo quedó siendo hoja y namespace. En ese bundle se detectaron 75 colisiones de este tipo (todas del módulo contable de Centrica; catcsoft tenía 0).
Listar las claves-hoja que son prefijo de otra (sobre el JSON del bundle):
import json
t = json.load(open('bundle_es.json'))['response']['content']['translations']
ks = set(t)
colisiones = [k for k in ks if any(o.startswith(k + '.') for o in ks)]
print(len(colisiones), 'colisiones')
for c in sorted(colisiones):
print(' ', c)
Regla mecánica: para toda clave k, no debe existir ninguna o tal que o empiece por k + ".".
Renombrar la constante para que la variante no cuelgue de la hoja base. Siguiendo el propio estilo del dominio:
❌ accounting.causacion.error.cc.inactivo + accounting.causacion.error.cc.inactivo.formato
✅ accounting.causacion.error.cc.inactivo + accounting.causacion.error.cc.formato.inactivo
o agrupar bajo un namespace puro (la base deja de ser hoja):
✅ accounting.causacion.error.cc.inactivo.base + accounting.causacion.error.cc.inactivo.formato
Tras el rename → re-ingestar i18n. El repo de dominio (p. ej. nebula-accounting-core) es el dueño de estas claves; el cambio se hace en su código bajo su autorización, no borrando datos sueltos.
flatToNested debe ser tolerante: nunca lanzar. Si un nodo intermedio ya es string (hoja), se promueve a objeto conservando el valor original en un centinela, de modo que un dato así jamás rompa el arranque del bundle.
Validación al crear/importar translation_keys (en el admin) que rechace una clave nueva si es prefijo estricto de, o extensión de, una clave existente. Convierte la convención en una barrera mecánica.
dominio.entidad.tipo.campo[.variante].GET /simappe-admin/api/v1/translation/bundle/{langCode}.simappe-shared/…/core/i18n/translation.service.ts (flatToNested).admin.translation_keys, admin.translations, admin.translation_bundles.nebula-accounting-core → AccountingDomainConstants.