Esta guía explica cómo declarar y consumir literales traducibles en Nebula ERP, cómo solicitar su registro y cómo se distribuyen las responsabilidades entre libs/domain y libs/shared/data-access. El ejemplo de referencia es el módulo de contabilidad, en particular accounting-close.
El catálogo de traducciones está centralizado en Simappe Admin. El frontend consume bundles por idioma y utiliza claves estables para resolver el texto visible.
| Ubicación | Responsabilidad | Qué contiene |
|---|---|---|
libs/domain/src/lib/i18n/ |
Catálogos tipados de claves del frontend. | Constantes como ACCOUNTING_CLOSE cuyos valores son claves, no textos traducidos. Es el destino de migración de los catálogos que hoy siguen en shared/data-access. |
libs/shared/data-access/src/lib/i18n/ |
Runtime e infraestructura Angular de i18n. | TranslationService, TranslatePipe, estrategia de título y, durante la migración, catálogos heredados. El servicio HTTP y el pipe no se trasladan a domain, porque dependen de Angular, HTTP, sesión e idioma activo. |
| Simappe Admin | Almacenamiento y entrega de traducciones. | translation_keys registra la clave, módulo, tipo, valor por defecto y descripción; translations guarda el valor por idioma y su estado de verificación. |
| Formulario de registro i18n | Canal oficial para pedir altas o correcciones. | Solicitudes individuales o por archivo Excel/CSV, con validación y vista previa. Arquitectura realiza la carga en DEV y QA. |
La relación en base de datos es translations.translation_key_id → translation_keys.id; las filas de traducción también identifican el idioma mediante language_id. El frontend no inserta ni actualiza estas tablas directamente.
La migración de catálogos desde shared/data-access hacia domain es gradual. Ya existen catálogos en libs/domain/src/lib/i18n/, incluidos accounting-close, bank-closures y budget-line-items; libs/domain/src/lib/i18n/index.ts y libs/domain/src/index.ts publican esas claves. En shared/data-access aún existen archivos de claves heredados junto con el runtime. Antes de crear un catálogo nuevo, revisar ambos lugares para reutilizar claves y evitar duplicados.
libs/accounting/src/lib/feature-accounting/src/lib/accounting-close/ es un ejemplo de la organización objetivo:
accounting-close.ts declara el objeto ACCOUNTING_CLOSE con claves como ACCOUNTING.CLOSE.LABEL.TITLE y ACCOUNTING.CLOSE.HINT.PROCESS.accounting-close/index.ts exporta el catálogo de esa funcionalidad.libs/domain/src/lib/i18n/index.ts agrega el barril de la funcionalidad.libs/domain/src/index.ts publica el barril i18n de dominio.accounting-close.component.ts importa ACCOUNTING_CLOSE desde @nebula/domain, importa TranslatePipe y TranslationService desde @nebula-shared/data-access, y expone readonly i18n = ACCOUNTING_CLOSE.accounting-close.component.html resuelve las claves con expresiones como {{ i18n.HINT.PROCESS | translate }}.El componente usa el catálogo tipado para reducir errores de escritura. El pipe realiza la resolución del texto en la plantilla. En TypeScript, TranslationService.translate(...) sirve cuando el texto traducido se necesita para construir títulos, avisos, diálogos u otros valores dinámicos.
Antes de solicitar una nueva clave:
domain y shared/data-access.Para claves de frontend usa el patrón en mayúsculas NAMESPACE.SEGMENTO[.SEGMENTO...], con al menos dos segmentos. Para Accounting, por ejemplo:
ACCOUNTING.CLOSE.LABEL.TITLE
ACCOUNTING.CLOSE.HINT.READ_ONLY
ACCOUNTING.CLOSE.ERROR.CLOSE_MONTH
Mantén el módulo como primer segmento y organiza el resto por funcionalidad y contexto (por ejemplo, LABEL, FIELD, ACTION, ERROR, CONFIRM). Usa nombres descriptivos y estables. No codifiques el texto español o inglés como valor de la constante: el valor debe ser el literal que se registrará, por ejemplo ACCOUNTING.CLOSE.LABEL.TITLE.
Las claves especiales de menú y breadcrumb de un solo segmento (OPTIONS_*, SUB_MODULES_*, MENU) solo se usan en las posiciones permitidas para etiquetas del menú.
domainPara una funcionalidad nueva, crea un archivo bajo libs/domain/src/lib/i18n/<funcionalidad>/ y exporta sus claves como constante inmutable. Ejemplo simplificado basado en el catálogo de Accounting:
export const ACCOUNTING_CLOSE = {
LABEL: {
TITLE: 'ACCOUNTING.CLOSE.LABEL.TITLE',
},
HINT: {
READ_ONLY: 'ACCOUNTING.CLOSE.HINT.READ_ONLY',
},
} as const;
Añade un index.ts de funcionalidad que reexporte el archivo y agrega esa exportación a libs/domain/src/lib/i18n/index.ts. Confirma que el barril público de libs/domain/src/index.ts exporte ./lib/i18n/index (ya está configurado en el proyecto actual). No mantengas dos catálogos activos para la misma clave en shared/data-access y domain.
Si migras un catálogo existente, actualiza los consumidores para importarlo desde @nebula/domain; retira su exportación heredada de shared/data-access cuando ya no tenga consumidores. El runtime TranslationService y TranslatePipe siguen importándose desde @nebula-shared/data-access.
En un componente standalone, importa el catálogo y el pipe:
import { ACCOUNTING_CLOSE } from '@nebula/domain';
import { TranslatePipe } from '@nebula-shared/data-access';
@Component({
standalone: true,
imports: [TranslatePipe],
})
export class AccountingCloseComponent {
readonly i18n = ACCOUNTING_CLOSE;
}
En el HTML, utiliza el pipe para todo texto visible:
<h1>{{ i18n.LABEL.TITLE | translate }}</h1>
<p>{{ i18n.HINT.READ_ONLY | translate }}</p>
También se puede pasar directamente una clave literal al pipe, pero se recomienda el catálogo tipado cuando la clave pertenece a la funcionalidad. No dejes prosa visible en HTML ni textos de usuario incrustados en constantes TypeScript.
En TypeScript, inyecta el servicio para resolver textos dinámicos:
protected readonly translationService = inject(TranslationService);
const title = this.translationService.translate(ACCOUNTING_CLOSE.LABEL.TITLE);
Para texto con variables, conserva los marcadores del estándar de frontend {{nombre}} tanto en español como en inglés y entrega el parámetro al mecanismo de traducción. No reemplaces marcadores por concatenación de fragmentos de texto.
El registro oficial se solicita mediante el formulario de literales i18n. La hoja asociada es el registro de solicitudes y seguimiento, no un punto de edición manual. No se escriben filas directamente en la hoja ni en las tablas de base de datos.
Para una carga masiva, descarga la plantilla desde el formulario o prepara un Excel/CSV con estos encabezados, en este orden:
Literal;Módulo;Español;Inglés
BUDGET.ITEM_CATALOG.ACTIONS.DELETE;budget;Eliminar;Delete
BUDGET.RUBRO.ERROR.CODIGODUPLICADO;budget;Ya existe un rubro con el código {{codigo}};A budget item with code {{codigo}} already exists
El ejemplo conserva el marcador en ambos idiomas. Para frontend el estándar de marcador es {{nombre}}; los marcadores {0}, {1}, etc. corresponden al formato posicional descrito para mensajes de backend. El formulario rechaza un registro si los marcadores del español e inglés no coinciden.
El formulario acepta Excel .xlsx (primera hoja) o CSV, con separador punto y coma, coma o tabulador; admite hasta 200 filas por archivo. El tipo elegido, Frontend o Backend, aplica a todas las filas del archivo. Antes de registrar, revisa la vista previa, el inglés sugerido y los motivos de filas rechazadas; se puede editar el inglés y desmarcar filas. El inglés sugerido debe revisarse siempre: se consulta primero el glosario del módulo, después textos aprobados y, si no hay coincidencia, el traductor.
En la columna Módulo usa el identificador del módulo ERP correspondiente (por ejemplo, accounting, budget, treasury, invoicing o common). El módulo sugerido por la clave se debe revisar, especialmente para textos compartidos. La convención de clave de frontend es distinta de la de backend; no mezcles ambos tipos en el mismo archivo.
Al registrar, la fila queda en estado Solicitado. Arquitectura valida y carga los literales tanto en DEV como en QA, verifica los bundles y después actualiza el estado. La clave puede añadirse al catálogo frontend mientras se coordina el registro, pero la traducción se verá cuando la carga correspondiente esté disponible en el ambiente y el bundle se haya actualizado.
Se eliminará {{nombre}} / {{nombre}} will be deleted.es.json/en.json para este mecanismo.TranslationService pertenece a shared/data-access: carga los idiomas disponibles y el bundle del idioma activo desde /simappe-admin/api/v1/translation, conserva una copia local para fallback y ofrece translate(...) para consultas síncronas después de inicializarse. La inicialización está diseñada para ejecutarse antes del primer render mediante APP_INITIALIZER. El servicio también coordina el idioma activo y el locale usado por el kit visual.
TranslatePipe es un pipe standalone que delega la traducción al servicio y vuelve a evaluar el texto cuando cambia el idioma. Por ello, en componentes standalone que lo usan en HTML debe incluirse en imports. No se debe duplicar esa lógica creando otro pipe local.
Si una clave aparece tal cual en pantalla, comprueba que: (1) el literal coincide exactamente con el registrado; (2) el bundle del idioma fue cargado; (3) la clave ya se cargó en el ambiente actual; y (4) el catálogo de domain está exportado y el consumidor importa el alias correcto.
domain contiene claves, no textos traducidos.TranslatePipe; TypeScript usa TranslationService solo cuando necesita el texto dinámicamente.libs/domain/src/lib/i18n/accounting-close/accounting-close.ts.libs/accounting/src/lib/feature-accounting/src/lib/accounting-close/accounting-close.component.ts y accounting-close.component.html.libs/shared/data-access/src/lib/i18n/translation.service.ts y translate.pipe.ts.