{"_id":"@becaskurtces/gaia-ng-front-core","_rev":"2-b3b5b176670d13110fb414f3345210cb","name":"@becaskurtces/gaia-ng-front-core","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@becaskurtces/gaia-ng-front-core","version":"1.0.0","keywords":["angular","components","ui","design-system","gaia"],"license":"MIT","_id":"@becaskurtces/gaia-ng-front-core@1.0.0","maintainers":[{"name":"becaskurtces","email":"becaskurtces@gmail.com"}],"homepage":"https://github.com/kurtces/gaia-ng-front-core#readme","bugs":{"url":"https://github.com/kurtces/gaia-ng-front-core/issues"},"bin":{"ng-generate-versions":"bin/ng-generate-versions.js"},"dist":{"shasum":"60e79cd9a3a5d9a8fbfa08718ee1bfac924dd05e","tarball":"https://registry.npmjs.org/@becaskurtces/gaia-ng-front-core/-/gaia-ng-front-core-1.0.0.tgz","fileCount":933,"integrity":"sha512-CkYhmrIapaiqHndgzsPnfqPopCEBZMi8cUXY5KwjU7F69Ta2IGEqJQYhsIzKA4u1DO/xvhndGnZ2fxL2YOYdGw==","signatures":[{"sig":"MEUCIA509qWusHnyQSc78iaWVyQfqZwpye8jP/Xm4ICvXPhXAiEA4S/yvbTeJltxTcfsauBc/GAQCFJ0Ku6qAzsKWep1t0A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":7004041},"module":"fesm2022/becaskurtces-gaia-ng-front-core.mjs","exports":{".":{"esm":"./esm2022/becaskurtces-gaia-ng-front-core.mjs","types":"./index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core.mjs","esm2022":"./esm2022/becaskurtces-gaia-ng-front-core.mjs"},"./ui":{"esm":"./esm2022/ui/becaskurtces-gaia-ng-front-core-ui.mjs","types":"./ui/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-ui.mjs","esm2022":"./esm2022/ui/becaskurtces-gaia-ng-front-core-ui.mjs"},"./appmenu":{"esm":"./esm2022/appmenu/becaskurtces-gaia-ng-front-core-appmenu.mjs","types":"./appmenu/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-appmenu.mjs","esm2022":"./esm2022/appmenu/becaskurtces-gaia-ng-front-core-appmenu.mjs"},"./maskito":{"esm":"./esm2022/maskito/becaskurtces-gaia-ng-front-core-maskito.mjs","types":"./maskito/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito.mjs","esm2022":"./esm2022/maskito/becaskurtces-gaia-ng-front-core-maskito.mjs"},"./incognito":{"esm":"./esm2022/incognito/becaskurtces-gaia-ng-front-core-incognito.mjs","types":"./incognito/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-incognito.mjs","esm2022":"./esm2022/incognito/becaskurtces-gaia-ng-front-core-incognito.mjs"},"./package.json":{"default":"./package.json"},"./maskito/bigjs":{"esm":"./esm2022/maskito/bigjs/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs","types":"./maskito/bigjs/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs","esm2022":"./esm2022/maskito/bigjs/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs"},"./maskito/bignumberjs":{"esm":"./esm2022/maskito/bignumberjs/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs","types":"./maskito/bignumberjs/index.d.ts","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs","esm2022":"./esm2022/maskito/bignumberjs/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs"}},"gitHead":"b6163f3673d9784c10bc78292b9b2e6e0373417e","private":false,"typings":"index.d.ts","_npmUser":{"name":"becaskurtces","email":"becaskurtces@gmail.com"},"repository":{"url":"git+https://github.com/kurtces/gaia-ng-front-core.git","type":"git"},"_npmVersion":"11.5.2","description":"Librería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección \"[Comunicación entre marco global e iframes](","directories":{},"sideEffects":false,"_nodeVersion":"22.17.1","dependencies":{"tslib":"^2.3.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"peerDependencies":{"big.js":">=6.0.0","@maskito/kit":">=3.0.0","bignumber.js":">=8.0.0","@angular/core":">=17.0.0","@maskito/core":">=3.0.0","@types/big.js":">=6.0.0","@angular/common":">=17.0.0","@maskito/angular":">=3.0.0","@angular/material":">=17.0.0","@jsverse/transloco":">=7.0.0 || >=8.0.0","angular-auth-oidc-client":">=17.0.0","@becaskurtces/gaia-fe-entity-fonts":">=1.0.0"},"peerDependenciesMeta":{"big.js":{"optional":true},"@maskito/kit":{"optional":true},"bignumber.js":{"optional":true},"@angular/core":{"optional":true},"@maskito/core":{"optional":true},"@types/big.js":{"optional":true},"@angular/common":{"optional":true},"@maskito/angular":{"optional":true},"@angular/material":{"optional":true},"@jsverse/transloco":{"optional":true},"angular-auth-oidc-client":{"optional":true},"@becaskurtces/gaia-fe-entity-fonts":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/gaia-ng-front-core_1.0.0_1778569259526_0.40516347568087796","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@becaskurtces/gaia-ng-front-core","version":"1.0.1","private":false,"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"bin":{"ng-generate-versions":"bin/ng-generate-versions.js"},"keywords":["angular","components","ui","design-system","gaia"],"repository":{"type":"git","url":"git+https://github.com/kurtces/gaia-ng-front-core.git"},"bugs":{"url":"https://github.com/kurtces/gaia-ng-front-core/issues"},"homepage":"https://github.com/kurtces/gaia-ng-front-core#readme","license":"MIT","peerDependencies":{"@angular/common":">=17.0.0","@angular/core":">=17.0.0","@angular/material":">=17.0.0","@jsverse/transloco":">=7.0.0 || >=8.0.0","@becaskurtces/gaia-fe-entity-fonts":">=1.0.0","angular-auth-oidc-client":">=17.0.0","@types/big.js":">=6.0.0","big.js":">=6.0.0","bignumber.js":">=8.0.0","@maskito/angular":">=3.0.0","@maskito/core":">=3.0.0","@maskito/kit":">=3.0.0"},"peerDependenciesMeta":{"@angular/common":{"optional":true},"@angular/core":{"optional":true},"@angular/material":{"optional":true},"@jsverse/transloco":{"optional":true},"angular-auth-oidc-client":{"optional":true},"@becaskurtces/gaia-fe-entity-fonts":{"optional":false},"@types/big.js":{"optional":true},"big.js":{"optional":true},"bignumber.js":{"optional":true},"@maskito/angular":{"optional":true},"@maskito/core":{"optional":true},"@maskito/kit":{"optional":true}},"dependencies":{"tslib":"^2.3.0"},"module":"fesm2022/becaskurtces-gaia-ng-front-core.mjs","typings":"index.d.ts","exports":{"./package.json":{"default":"./package.json"},".":{"types":"./index.d.ts","esm2022":"./esm2022/becaskurtces-gaia-ng-front-core.mjs","esm":"./esm2022/becaskurtces-gaia-ng-front-core.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core.mjs"},"./appmenu":{"types":"./appmenu/index.d.ts","esm2022":"./esm2022/appmenu/becaskurtces-gaia-ng-front-core-appmenu.mjs","esm":"./esm2022/appmenu/becaskurtces-gaia-ng-front-core-appmenu.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-appmenu.mjs"},"./incognito":{"types":"./incognito/index.d.ts","esm2022":"./esm2022/incognito/becaskurtces-gaia-ng-front-core-incognito.mjs","esm":"./esm2022/incognito/becaskurtces-gaia-ng-front-core-incognito.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-incognito.mjs"},"./maskito":{"types":"./maskito/index.d.ts","esm2022":"./esm2022/maskito/becaskurtces-gaia-ng-front-core-maskito.mjs","esm":"./esm2022/maskito/becaskurtces-gaia-ng-front-core-maskito.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito.mjs"},"./ui":{"types":"./ui/index.d.ts","esm2022":"./esm2022/ui/becaskurtces-gaia-ng-front-core-ui.mjs","esm":"./esm2022/ui/becaskurtces-gaia-ng-front-core-ui.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-ui.mjs"},"./maskito/bignumberjs":{"types":"./maskito/bignumberjs/index.d.ts","esm2022":"./esm2022/maskito/bignumberjs/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs","esm":"./esm2022/maskito/bignumberjs/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito-bignumberjs.mjs"},"./maskito/bigjs":{"types":"./maskito/bigjs/index.d.ts","esm2022":"./esm2022/maskito/bigjs/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs","esm":"./esm2022/maskito/bigjs/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs","default":"./fesm2022/becaskurtces-gaia-ng-front-core-maskito-bigjs.mjs"}},"sideEffects":false,"_id":"@becaskurtces/gaia-ng-front-core@1.0.1","gitHead":"2879cd1a8ac826588ca2e769e69f07d4babe7fe5","description":"Librería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección \"[Comunicación entre marco global e iframes](","_nodeVersion":"22.17.1","_npmVersion":"11.5.2","dist":{"integrity":"sha512-6HW5IdSt/cN1w1cFaAWo/+vLT2GkUcAnb+qdOvv8Zzg0U27r+zouINIceJxDszr/BTt8UTOnmpiHWzmtM+Yu+w==","shasum":"1f899a0374f7cb41e43a3f29210cd9080dc563c9","tarball":"https://registry.npmjs.org/@becaskurtces/gaia-ng-front-core/-/gaia-ng-front-core-1.0.1.tgz","fileCount":933,"unpackedSize":7113286,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDIVdJ8WJKU9RAfQqGPwndT5gwCGBUci+P6Mf8cyO9MzwIhAKH6FhjY24sZSn2ALUMEOU2DcEXZ0TB0Ax7XaVz/58XF"}]},"_npmUser":{"name":"becaskurtces","email":"becaskurtces@gmail.com"},"directories":{},"maintainers":[{"name":"becaskurtces","email":"becaskurtces@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gaia-ng-front-core_1.0.1_1779101540683_0.10145678204941722"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-12T07:00:59.338Z","modified":"2026-05-18T10:52:21.111Z","1.0.0":"2026-05-12T07:00:59.825Z","1.0.1":"2026-05-18T10:52:20.996Z"},"bugs":{"url":"https://github.com/kurtces/gaia-ng-front-core/issues"},"license":"MIT","homepage":"https://github.com/kurtces/gaia-ng-front-core#readme","keywords":["angular","components","ui","design-system","gaia"],"repository":{"type":"git","url":"git+https://github.com/kurtces/gaia-ng-front-core.git"},"description":"Librería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección \"[Comunicación entre marco global e iframes](","maintainers":[{"name":"becaskurtces","email":"becaskurtces@gmail.com"}],"readme":"# @becaskurtces/gaia-ng-front-core\r\n\r\nLibrería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección \"[Comunicación entre marco global e iframes](./docs/apps-communication.md)\". Si no usas la librería, puedes copiar y modificar todo el código que te pueda ser útil. \r\n\r\n**La versión mayor de esta librería sigue la versión mayor de Angular.** Es decir, si la librería tiene la versión 18.x.x, entonces está desarrollada sobre Angular 18.x.x.\r\n \r\n**Es compatible con Angular 17+.** Si tu proyecto usa una versión más antigua, la única opción es copiar el código que necesites o actualizar la app a Angular 17+. El código es compatible con las versiones anteriores excepto porque se está usando la nueva forma de organizar el código en Angular, con componentes standalone y sin NgModules. La librería ha sido probada en aplicaciones con Angular 17, 18, 19 y 20.\r\n \r\nLos cambios que se vayan haciendo en el código de la librería se irán anotando en el fichero [CHANGELOG](./CHANGELOG.md)\r\n\r\nEn el caso de encontrar errores o querer hacer alguna sugerencia, contactar con la Oficina Técnica o crear un merge/pull request.\r\n\r\n> Puedes usar el proyecto [gaia-fe-angular-base-project](https://bitbucket.org/business/gaia-fe-angular-base-project) como referencia de uso de esta librería.\r\n\r\n\r\n## Índice\r\n\r\n- [Sobre la librería](#markdown-header-sobre-la-libreria)\r\n- [Recomendaciones al empezar un proyecto](#markdown-header-recomendaciones-al-empezar-un-proyecto)\r\n- [Instalar en tu proyecto](#markdown-header-instalar-en-tu-proyecto)\r\n- [Componentes UI](#markdown-header-componentes-ui)\r\n- [El marco global](#markdown-header-el-marco-global)\r\n- [Notificaciones](#markdown-header-notificaciones)\r\n- [Comunicación entre marco global e iframes](#markdown-header-comunicacion-entre-marco-global-e-iframes)\r\n    - [Para la configuración común del usuario](#markdown-header-para-la-configuracion-comun-del-usuario)\r\n    - [Para la sincronización de rutas](#markdown-header-para-la-sincronizacion-de-rutas)\r\n    - [Para la sincronización del usuario](#markdown-header-para-la-sincronizacion-del-usuario)\r\n- [Obtener la configuración de usuario actual](#markdown-header-obtener-la-configuracion-de-usuario-actual)\r\n- [Cómo saber si mi app está dentro de un iframe](#markdown-header-como-saber-si-mi-app-esta-dentro-de-un-iframe)\r\n- [Recargar la aplicación con los cambios en la configuración](#markdown-header-recargar-la-aplicacion-con-los-cambios-en-la-configuracion)\r\n- [Inicializar y sincronizar el idioma (i18n)](#markdown-header-inicializar-y-sincronizar-el-idioma-i18n)\r\n    - [Loader alternativo](#markdown-header-loader-alternativo)\r\n    - [I18n en Angular Material](#markdown-header-i18n-en-angular-material)\r\n    - [Directivas y Pipes de ayuda](#markdown-header-directivas-y-pipes-de-ayuda)\r\n        - [I18nRerenderDirective](#markdown-header-i18nrerenderdirective)\r\n        - [I18nRerenderLocaleDirective](#markdown-header-i18nrerenderlocaledirective)\r\n        - [I18nRerenderDateDirective](#markdown-header-i18nrerenderdatedirective)\r\n        - [I18nRerenderTimeDirective](#markdown-header-i18nrerendertimedirective)\r\n        - [I18nRerenderNumberDirective](#markdown-header-i18nrerendernumberdirective)\r\n        - [I18nDateDirective](#markdown-header-i18ndatedirective)\r\n        - [I18nDatePipe](#markdown-header-i18ndatepipe)\r\n        - [I18nNumberDirective](#markdown-header-i18nnumberdirective)\r\n        - [I18nNumberPipe](#markdown-header-i18nnumberpipe)\r\n        - [I18nCurrencyPipe](#markdown-header-i18ncurrencypipe)\r\n        - [I18nConcatDateFormatKeyPipe](#markdown-header-i18nconcatdateformatkeypipe)\r\n    - [Funciones de ayuda](#markdown-header-funciones-de-ayuda)\r\n        - [shortDate](#markdown-header-shortdate)\r\n- [Sincronización de rutas](#markdown-header-sincronizacion-de-rutas)\r\n- [Sincronización de las opciones del menú](#markdown-header-sincronizacion-de-las-opciones-del-menu)\r\n- [Solicitar al marco global abrir una URL (en la misma u otra pestaña)](#markdown-header-solicitar-al-marco-global-abrir-una-url-en-la-misma-u-otra-pestana)\r\n- [Solicitar al marco global cerrar la ventana actual](#markdown-header-solicitar-al-marco-global-cerrar-la-ventana-actual)\r\n- [Solicitar al marco global que muestre una configuración propia de mi app](#markdown-header-solicitar-al-marco-global-que-muestre-una-configuracion-propia-de-mi-app)\r\n- [Autenticación](#markdown-header-autenticacion)\r\n    - [Cargar configuración de forma asíncrona](#markdown-header-cargar-configuracion-de-forma-asincrona)\r\n    - [Mostrar errores de autenticación](#markdown-header-mostrar-errores-de-autenticacion)\r\n    - [Flujo propio de autenticación (sin provideAuth)](#markdown-header-flujo-propio-de-autenticacion-sin-provideauth)\r\n    - [Sincronización de usuario (sin provideAuth)](#markdown-header-sincronizacion-de-usuario-sin-provideauth)\r\n    - [Notas sobre la autenticación en los iframes](#markdown-header-notas-sobre-la-autenticacion-en-los-iframes)\r\n- [Entornos](#markdown-header-entornos)\r\n    - [Interfaces](#markdown-header-interfaces)\r\n    - [Ejemplo de uso](#markdown-header-ejemplo-de-uso)\r\n    - [Compilar la configuración por entornos escrita en TypeScript](#markdown-header-compilar-la-configuracion-por-entornos-escrita-en-typescript)\r\n- [Otras utilidades](#markdown-header-otras-utilidades)\r\n    - [deepEquals](#markdown-header-deepequals)\r\n    - [dateEquals](#markdown-header-dateequals)\r\n    - [findFirstUUID](#markdown-header-findfirstuuid)\r\n    - [detectIncognito](#markdown-header-detectincognito)\r\n- [Cargar el módulo de notificaciones en tu app](#markdown-header-cargar-el-modulo-de-notificaciones-en-tu-app)\r\n- [Mostrar spinner mientras carga la app](#markdown-header-mostrar-spinner-mientras-carga-la-app)\r\n- [Generar el fichero versions.json](#markdown-header-generar-el-fichero-versionsjson)\r\n- [Desarrollo de la librería](#markdown-header-desarrollo-de-la-libreria)\r\n\r\n\r\n## Sobre la librería\r\n\r\nEsta librería trata de solucionar los siguientes aspectos:\r\n\r\n- **Comunicación entre el marco global e iframes** usando postMessage.\r\n- **Configuración común del usuario** (idioma, formato de fecha, formato numérico) y sincronización con el marco global.\r\n- **Sincronización de rutas** entre marco global e iframes.\r\n- **Sincronización de las opciones del menú** entre marco global e iframes.\r\n- Interfaces sugeridas para definir la **configuración por entornos**.\r\n- Configuración sugerida para **autenticación** y sincronización del usuario logueado en el marco global con la app cargada en el iframe.\r\n- Configuración sugerida para **i18n** con sincronización con el marco global y utilidades.\r\n- **Cargar módulos remotos** (por ejemplo, el Web Component de notificaciones).\r\n- Script para generar un fichero `versions.json` dentro del compilado de Angular con las versiones de la aplicación, información del sistema operativo, versiones de Node.js y npm, la rama de Git y el hash del commit, y la fecha de generación del fichero.\r\n- **Componentes básicos de UI** con el diseño definido por el equipo de UX.\r\n\r\n\r\n## Recomendaciones al empezar un proyecto\r\n\r\nAl empezar un nuevo proyecto de Angular, se recomienda usar la última versión estable, y como mínimo la versión 17. Se recomienda también crear todos los componentes como 'standalone', pues es la forma en la que el equipo de Angular ha decidido que usemos el framework desde las últimas versiones. [A partir de la versión 19 de Angular, todos los componentes pasarán a ser standalone por defecto](https://blog.angular.dev/the-future-is-standalone-475d7edbc706).\r\n\r\n\r\n## Instalar en tu proyecto\r\n\r\nLa librería se publica como paquete **público** en npmjs.com. No necesitas token para instalarla.\r\n\r\nLa librería tiene las siguientes dependencias:\r\n\r\n- `@jsverse/transloco@7+`\r\n- `angular-auth-oidc-client@17+`\r\n- `@angular/material@17+`\r\n\r\nInstala la librería:\r\n\r\n```sh\r\nnpm install @becaskurtces/gaia-ng-front-core\r\n```\r\n\r\nSi te aparece algún error al usar la librería, es posible que necesites modificar el fichero `tsconfig.json` de tu proyecto y cambiar el valor de `moduleResolution` por `Bundler`. En proyectos nuevos generados con el CLI de Angular, ya viene configurado así. \r\n\r\n```json\r\n{\r\n  \"compilerOptions\": {\r\n    \"moduleResolution\": \"Bundler\"\r\n  }\r\n}\r\n```\r\n\r\n## Componentes UI\r\n\r\nLos componentes de UI se han definido dentro de un subpaquete: `@becaskurtces/gaia-ng-front-core/ui`.\r\n\r\nPuedes ver todos los componentes creados hasta el momento en la documentación dedicada solo a estos componentes de UI:\r\n\r\n[Ver documentación de @becaskurtces/gaia-ng-front-core/ui.](./README-UI.md)\r\n\r\n\r\n## El marco global\r\n\r\nTodas las aplicaciones se han pensando para que sean lo más independientes posibles unas de otras. Cada aplicación se encarga de un contexto diferente (siniestros, contabilidad, etc.) y cada una tiene su frontend y su backend.\r\n\r\nEl marco global es la aplicación que carga todas estás aplicaciones dentro de una misma app. Cuenta solo con una barra superior y un menú lateral de navegación. En el centro carga el resto de apps dentro de un iframe. En la barra superior puede haber iconos que abren otros menús, como algunos ajustes del usuario o las notificaciones.\r\n\r\nTodas las aplicaciones también deben poder abrirse sin necesidad del marco global, funcionando de forma independiente, con su autenticación y todo.\r\n\r\nEn la siguiente imagen se muestra una captura de pantalla del marco global actual. En el centro se está cargando la aplicación de bandeja de tareas (una versión no actualizada usada para hacer pruebas):\r\n\r\n![Marco global](./docs/images/gaia-fe-home/gaia-fe-home-1.png)\r\n\r\nEn principio, el marco global será la única app que mostrará las notificaciones. Se encargará de abrir los links de las notificaciones y ajustar la URL según a que app pertenezcan.\r\n\r\n![Marco global](./docs/images/gaia-fe-home/gaia-fe-home-2-notifications.png)\r\n\r\nEl marco global será el encargado de permitir al usuario cambiar el idioma, formato de fecha y formato numérico. Estos datos se comunicarán al resto de apps cargadas dentro del iframe ([Comunicación entre marco global e iframes](./docs/app-communication.md)).\r\n\r\n![Marco global](./docs/images/gaia-fe-home/gaia-fe-home-3-settings.png)\r\n\r\nEn la barra superior también aparecerá el usuario logueado:\r\n\r\n![Marco global](./docs/images/gaia-fe-home/gaia-fe-home-5-user.png)\r\n\r\nEl menú izquierdo por ahora es fijo y sus opciones se configuran en el fichero \"{env}.properties.js\" del marco global:\r\n\r\n![Marco global](./docs/images/gaia-fe-home/gaia-fe-home-4-menu.png)\r\n\r\n\r\n## Notificaciones\r\n\r\nLas notificaciones pueden contener links. Los links deben ser propios del front de la app que las generó, no deben ser links del marco global. El marco global parsea las URLs para cargar la app correspodiente e iniciarla con la URL de la notificación. Por ahora, las aplicaciones de los iframes deben configurarse en la configuración por entorno del marco global, y en caso de que alguna aplicación **no** esté definida y llegué una notificación con un link de la misma, se mostrará una página 404.\r\n\r\n\r\n## Comunicación entre marco global e iframes\r\n\r\nDebido a limitaciones de seguridad por parte de los navegadores, la mejor opción para comunicar la aplicación del marco global y el resto de aplicaciones cargadas en iframes, es usando la API [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).\r\n\r\nSe necesita compartir la siguiente información entre el marco global y la app cargada en el iframe:\r\n\r\n- Configuración común del usuario.\r\n    * Idioma\r\n    * Formato de fecha\r\n    * Formato numérico\r\n- Sincronizar la ruta del iframe con la del marco global.\r\n- Sincronizar el usuario logueado en el marco global con la del iframe.\r\n\r\nLos mensajes que se envían a través de `postMessage` deben tener el siguiente formato para que los entienda el marco global:\r\n\r\n```ts\r\nexport interface IframeMessage {\r\n  type: string;\r\n  data: any;\r\n}\r\n```\r\n\r\nSe puede usar el código de `src/iframe-messaging/services/iframe-messaging.service.ts` como referencia para ver como enviar y recibir mensajes con `postMessage`.\r\n\r\n\r\n### Para la configuración común del usuario\r\n\r\n`type` será `\"ENT_DOMAIN_COMMON_USER_CONFIG\"`\r\n\r\ny `data` sigue la interfaz `CommonUserConfig`:\r\n\r\n```ts\r\nexport interface CommonUserConfigMessage extends IframeMessage {\r\n  type: \"ENT_DOMAIN_COMMON_USER_CONFIG\";\r\n  data: CommonUserConfig;\r\n}\r\n```\r\n\r\n```ts\r\nexport interface CommonUserConfig {\r\n  locale: Locale;\r\n  dateFormat: DateFormat;\r\n  numberFormat: NumberFormat;\r\n}\r\n```\r\n\r\nEl marco global envía este mensaje al iframe al terminarse de cargar la app en el iframe y cada vez que cambia un valor de la configuración.\r\n\r\nLos valores de los enumerados `Locale`, `DateFormat` y `NumberFormat` son los siguientes:\r\n\r\n```ts\r\n/**\r\n * Todos los códigos de idioma que deberían soportar todas las aplicaciones.  \r\n * De momento solo español e inglés. Esto no restringe a que cada app pueda\r\n * añadir otros idiomas aunque este enumerado no se modifique.\r\n */\r\nexport enum Locale {\r\n  ES = \"es\",\r\n  EN = \"en\",\r\n}\r\n```\r\n\r\n```ts\r\n/**\r\n * Los valores se han definido siguiendo la documentación\r\n * de DatePipe de Angular\r\n * https://angular.dev/api/common/DatePipe?tab=usage-notes\r\n */\r\nexport enum DateFormat {\r\n  /**\r\n   * MM/dd/yyyy, hh:mm a  \r\n   * (09/25/2024, 09:15 PM)\r\n   */\r\n  MM_DD_YYYY_hhmma = \"MM/dd/yyyy, hh:mm a\",\r\n  /**\r\n   * dd/MM/yyyy HH:mm  \r\n   * (25/09/2024 21:15)\r\n   */\r\n  DD_MM_YYYY_HHmm = \"dd/MM/yyyy HH:mm\",\r\n  /**\r\n   * yyyy/MM/dd HH:mm  \r\n   * (2024/09/25 21:15)\r\n   */\r\n  YYYY_MM_DD_HHmm = \"yyyy/MM/dd HH:mm\"\r\n}\r\n```\r\n\r\n```ts\r\nexport enum NumberFormat {\r\n  /**\r\n   * Usar puntos para los enteros y coma para separar los decimales.\r\n   * Ejemplo: 100.000.000,00\r\n   */\r\n  DOTS_COMMA = \"dots_comma\",\r\n  /**\r\n   * Usar commas para los enteros y punto para separar los decimales.\r\n   * Ejemplo: 100,000,000.00\r\n   */\r\n  COMMAS_DOT = \"commas_dot\"\r\n}\r\n```\r\n\r\n\r\n### Para la sincronización de rutas\r\n\r\n`type` será `\"ENT_DOMAIN_ROUTE_CHANGE\"`\r\n\r\ny `data` sigue la interfaz `RouteChange`:\r\n\r\n```ts\r\nexport interface RouteChangeMessage extends IframeMessage {\r\n  type: \"ENT_DOMAIN_ROUTE_CHANGE\";\r\n  data: RouteChange;\r\n}\r\n```\r\n\r\n```ts\r\nexport interface RouteChange {\r\n  path: string;\r\n}\r\n```\r\n\r\nEl iframe indicará al marco global cuando cambia su ruta y el marco global indicará a los iframe si quiere que carguen una ruta en específico, por ejemplo, al pinchar en las opciones del menú de navegación.\r\n\r\nSi la app del iframe cambia de ruta, debe enviar un mensaje `\"ENT_DOMAIN_ROUTE_CHANGE\"` al marco global con su nuevo path. Si, estando la app cargada en el iframe, el marco global quiere mostrar otra ruta, por ejemplo, al pulsar una sub-opción del menú, enviará un mensaje `\"ENT_DOMAIN_ROUTE_CHANGE\"` al iframe con el path deseado.\r\n\r\n\r\n### Para la sincronización del usuario\r\n\r\n`type` será `\"ENT_DOMAIN_CURRENT_AUTH\"`\r\n\r\ny `data` sigue la interfaz `CurrentAuth`:\r\n\r\n```ts\r\nexport interface CurrentAuthMessage extends IframeMessage {\r\n  type: \"ENT_DOMAIN_CURRENT_AUTH\";\r\n  data?: CurrentAuth;\r\n}\r\n```\r\n\r\n```ts\r\nexport interface CurrentAuth {\r\n  email: string;\r\n}\r\n```\r\n\r\nPara que la app del iframe obtenga el usuario logueado del marco global, debe enviar un mensaje \r\n`\"ENT_DOMAIN_CURRENT_AUTH\"` al marco global, **sin necesidad de especificar la propiedad data**. Cuando el marco global recibe este mensaje, responde al iframe con otro mensaje con el mismo tipo, `\"ENT_DOMAIN_CURRENT_AUTH\"`, con la propiedad data con el email del usuario logueado. Este email se puede usar con la librería `angular-auth-oidc-client` para pasarle al servicio de Microsoft la propiedad `login_hint` con el email del usuario y la propiedad `prompt` con el valor `none`. De esta forma, se autentica directamente con ese usuario sin mostrar la pantalla de login. Se puede ver un ejemplo de esto en el fichero `src/auth/provide-auth-app-initializer.ts`.\r\n\r\nEs importante que en la configuración de `angular-auth-oidc-cliente` **NO** se use la siguiente configuración:\r\n\r\n```js\r\n// No poner esto\r\ncustomParamsAuthRequest: {\r\n  prompt: \"select_account\"\r\n}\r\n```\r\n\r\nEsta opción provoca que siempre se muestre la página de login de Microsoft y esta página no funciona dentro de un iframe, por lo que la app no se cargaría correctamente.\r\n\r\n\r\n## Obtener la configuración de usuario actual\r\n\r\n```ts\r\nimport { ReadonlyUserConfigService } from \"@becaskurtces/gaia-ng-front-core\";\r\n\r\n@Component()\r\nexport class MyComponent implements OnInit {\r\n\r\n  private readonlyUserConfigService = inject(ReadonlyUserConfigService);\r\n\r\n  ngOnInit() {\r\n    // Suscribirse a toda la configuración\r\n    this.readonlyUserConfigService.config$.subscribe(config => ...);\r\n\r\n    // Suscribirse a partes de la configuración\r\n    this.readonlyUserConfigService.locale$.subscribe(locale => ...);\r\n    this.readonlyUserConfigService.dateFormat$.subscribe(dateFormat => ...);\r\n    this.readonlyUserConfigService.numberFormat$.subscribe(numberFormat => ...);\r\n\r\n    // Obtener la configuración sin necesidad de suscribirse\r\n    const config = this.readonlyUserConfigService.getCurrentConfig();\r\n  }\r\n}\r\n```\r\n\r\n## Cómo saber si mi app está dentro de un iframe\r\n\r\n```js\r\nwindow !== window.parent // dentro de un iframe\r\n\r\nwindow === window.parent // ejecución normal sin iframe\r\n```\r\n\r\nPuedes usar la utilidad `isInsideAnIframe()` de esta librería si no te acuerdas de la condición de arriba.\r\n\r\n\r\n## Recargar la aplicación con los cambios en la configuración\r\n\r\nSi quieres que tu app se recargue automáticamente cada vez que cambia alguna de las configuraciones del usuario en el marco global, importa la siguiente configuración:\r\n\r\n```ts\r\nimport { provideCommonUserConfig } from \"@becaskurtces/gaia-ng-front-core\";\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideCommonUserConfig({\r\n      // Recargar con cualquier cambio\r\n      reloadWindowOnAnyChange: true\r\n    })\r\n  ]\r\n};\r\n```\r\n\r\n```ts\r\nimport { provideCommonUserConfig, CommonUserConfigProperty } from \"@becaskurtces/gaia-ng-front-core\";\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideCommonUserConfig({\r\n      reloadWindowWithChangesIn: [\r\n        // Elegir con que cambios recargar\r\n        CommonUserConfigProperty.LOCALE,\r\n        CommonUserConfigProperty.DATE_FORMAT,\r\n        CommonUserConfigProperty.NUMBER_FORMAT\r\n      ]\r\n    })\r\n  ]\r\n};\r\n```\r\n\r\n\r\n## Inicializar y sincronizar el idioma (i18n)\r\n\r\nPara escuchar los eventos del marco global y actualizar automáticamente el idioma en el servicio de Transloco, solo es necesario importar e inicializar los servicios usando `provideI18n(config?: I18nConfig)`. Esta función crea un servicio que llama a `translocoService.setActiveLang(lang)` cuando recibe un evento de configuración del marco global, además de inicializar Transloco con los datos proporcionados con el parámetro `I18nConfig`.\r\n\r\nSi nuestra app tiene el fichero `app.config.ts`:\r\n\r\n```ts\r\nimport { provideI18n } from '@becaskurtces/gaia-ng-front-core';\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideI18n()\r\n  ]\r\n};\r\n```\r\n\r\nO importándolo en un `NgModule`:\r\n\r\n```ts\r\nimport { provideI18n } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@NgModule({\r\n  providers: [\r\n    provideI18n()\r\n  ]\r\n})\r\nexport class MyModule {}\r\n```\r\n\r\nLa función `provideI18n()` usa la siguiente configuración por defecto:\r\n\r\n```ts\r\nconst defaultConfig: I18nConfig = {\r\n  assetsPath: `${window.location.origin}/assets/i18n`,\r\n  availableLocales: availableLocales,\r\n  defaultLocale: DEFAULT_LOCALE,\r\n  fallbackLocale: DEFAULT_LOCALE,\r\n  reRenderOnLangChange: false,\r\n  logMissingKeys: true,\r\n  loader: undefined // usa uno por defecto si no se define\r\n}\r\n\r\n// availableLocales son los valores del enumerado Locale\r\nconst availableLocales = Object.values(Locale);\r\n\r\n// DEFAULT_LOCALE se elige según el idioma del navegador y\r\n// los idiomas de availableLocales\r\nconst DEFAULT_LOCALE = availableLocales.includes(browserLang) ? browserLang : Locale.ES;\r\n```\r\n\r\nLos ficheros de traducciones tendrán el formato `{{código-de-idioma}}.json` dentro de la ruta `assetsPath`.\r\n\r\n\r\n### Loader alternativo\r\n\r\nSi el \"loader\" por defecto no cumple con todas nuestras necesidades, podemos usar uno personalizado:\r\n\r\n```ts\r\nimport { inject, Injectable } from \"@angular/core\";\r\nimport { HttpClient } from \"@angular/common/http\";\r\nimport { Translation, TranslocoLoader } from \"@jsverse/transloco\";\r\nimport { Observable } from \"rxjs\";\r\nimport { I18N_CONFIG_TOKEN } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class MyCustomTranslocoLoaderService implements TranslocoLoader {\r\n\r\n  private http = inject(HttpClient);\r\n  private config = inject(I18N_CONFIG_TOKEN);\r\n\r\n  /**\r\n   * Descarga el JSON con las traducciones usando la ruta\r\n   * especificada en la configuración\r\n   */\r\n  getTranslation(lang: string): Observable<Translation> {\r\n    return this.http.get<Translation>(`${this.config.assetsPath}/${lang}.json`);\r\n  }\r\n\r\n}\r\n```\r\n\r\n```ts\r\nimport { provideI18n } from '@becaskurtces/gaia-ng-front-core';\r\nimport { MyCustomTranslocoLoaderService } from './my-custom-transloco-loader.service';\r\n\r\n@NgModule({\r\n  providers: [\r\n    provideI18n({ loader: MyCustomTranslocoLoaderService })\r\n  ]\r\n})\r\nexport class MyModule {}\r\n```\r\n\r\n\r\n### I18n en Angular Material\r\n\r\nTanto en Angular como en Angular Material, la configuración de idioma y fechas, se suele tratar como estática en muchos componentes y servicios. No se puede actualizar dinámicamente mientras se usa la aplicación. La librería NgxMask tampoco permite cambiar el formato a posteriori. Por tanto, para asegurar que todo se muestra con el mismo idioma y formato, se recomienda recargar la página como se indica en un uno de los apartados anteriores: \"[Recargar la aplicación con los cambios en la configuración](#markdown-header-recargar-la-aplicacion-con-los-cambios-en-la-configuracion)\". Si tu aplicación no usa Datepickers de Material, NgxMask u otra librería que no permita actualizarse dinámicamente su configuración, no es necesario hacer esta recarga.\r\n\r\nPara obtener la configuración del usuario al cargarse la app, se han creado las siguientes funciones:\r\n\r\n- `getInitUserConfig`. Devuelve toda la configuración del usuario (locale, dateFormat y numberFormat).\r\n- `getInitNumberConfig`. Devuelve la configuración numberFormat preparada para usar con NgxMask. (También se puede usar Maskito con un componente ya preconfigurado. Ver la documentación de README-UI.md, busca en el índice: \"Maskito\")\r\n- `getInitMatDateFormats`. Devuelve la configuración dateFormat preparada para importarse como MatDateFormats.\r\n\r\nA continuación se deja un ejemplo de como usar estas funciones:\r\n\r\n```ts\r\nimport { provideDateFnsAdapter } from \"@angular/material-date-fns-adapter\";\r\nimport { provideMomentDateAdapter } from \"@angular/material-moment-adapter\";\r\nimport { es } from \"date-fns/locale\";\r\nimport { enUS } from \"date-fns/locale\";\r\nimport { provideNgxMask } from \"ngx-mask\";\r\nimport {\r\n  getInitUserConfig,\r\n  getInitNumberConfig,\r\n  getInitMatDateFormats,\r\n  Locale\r\n} from \"@becaskurtces/gaia-ng-front-core\";\r\n\r\nconst initLang = getInitUserConfig().locale;\r\n\r\nconst initNumberConfig = getInitNumberConfig();\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    // Inicializar el idioma en Material\r\n    {\r\n      provide: MAT_DATE_LOCALE,\r\n      useValue: initLang == Locale.ES ? es : enUS\r\n    },\r\n\r\n    // Importa solo un DateAdapter: provideDateFnsAdapter o provideMomentDateAdapter\r\n\r\n    // Inicializar el DateAdapter de DateFns con los\r\n    // formatos de fecha según la configuración de usuario\r\n    provideDateFnsAdapter(getInitMatDateFormats()),\r\n\r\n    // Inicializar el DateAdapter de Momentjs  con los\r\n    // formatos de fecha según la configuración de usuario\r\n    provideMomentDateAdapter(getInitMatDateFormats(\"moment\"), { useUtc: true }),\r\n\r\n    // Inicializar NgxMask con la configuración de usuario\r\n    provideNgxMask({\r\n      thousandSeparator: initNumberConfig.thousandSeparator,\r\n      decimalMarker: initNumberConfig.decimalMarker\r\n    })\r\n  ]\r\n}\r\n```\r\n\r\n> **Si no aparecen las fechas en el formato esperado, comprueba que no estés importando más de una vez un DateAdapter o un módulo que inicialice un DateAdapter.**\r\n\r\n\r\n### Directivas y Pipes de ayuda\r\n\r\nEl módulo de `i18n` incluye varias directivas y pipes de ayuda:\r\n\r\n```ts\r\n@Component({\r\n  selector: 'my-component',\r\n  standalone: true,\r\n  imports: [\r\n    I18nRerenderDirective,\r\n    I18nRerenderLocaleDirective,\r\n    I18nRerenderDateDirective,\r\n    I18nRerenderTimeDirective,\r\n    I18nRerenderNumberDirective,\r\n    I18nDatePipe,\r\n    I18nDateDirective,\r\n    I18nNumberPipe,\r\n    I18nNumberDirective,\r\n    I18nCurrencyPipe,\r\n    I18nConcatDateFormatKeyPipe\r\n  ],\r\n  template: `...`\r\n})\r\nexport class MyComponent {}\r\n```\r\n\r\n> Las directivas `I18nRerender*` solo pueden ser útiles en el caso de que **no** se recargue la aplicación automáticamente con los cambios en la configuración del usuario.\r\n\r\n#### I18nRerenderDirective\r\n\r\n Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el locale, dateFormat o numberFormat.\r\n \r\n Acepta un string con los valores separados por comas. Los valores se convierten a minúsculas internamente, por lo que es case insensitive. Los posibles valores son: `locale`, `dateFormat` y `numberFormat`.\r\n\r\n Solo re-renderiza los componentes o elementos si cambia alguno de los valores especificados o en el caso de no especificar ninguno, re-renderiza con cualquier cambio en los 3 valores.\r\n\r\n```html\r\n<ng-container *i18nRerender></ng-container>\r\n\r\n<div *i18nRerender></div>\r\n\r\n<div *i18nRerender=\"'locale,dateFormat,numberFormat'\"></div>\r\n\r\n<div *i18nRerender=\"'dateFormat'\"></div>\r\n```\r\n\r\n#### I18nRerenderLocaleDirective\r\n\r\nDirectiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo)  al cambiar el `locale`.\r\n\r\n```html\r\n<div *i18nRerenderLocale></div>\r\n```\r\n\r\n#### I18nRerenderDateDirective\r\n\r\nDirectiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo)  al cambiar el `dateFormat`. Está pensada para componentes o elementos que muestran una fecha sin incluir la hora.\r\n\r\n```html\r\n<div *i18nRerenderDate></div>\r\n```\r\n\r\n#### I18nRerenderTimeDirective\r\n\r\nDirectiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo)  al cambiar el `locale` o `dateFormat`. El locale puede afectar en algún caso a la hora mostrada (AM/PM, a. m./p. m.). Está pensada para componentes o elementos que muestren una fecha con la hora.\r\n\r\n```html\r\n<div *i18nRerenderTime></div>\r\n```\r\n\r\n#### I18nRerenderNumberDirective\r\n\r\nDirectiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el `numberFormat`.\r\n\r\n```html\r\n<div *i18nRerenderNumber></div>\r\n```\r\n\r\n\r\n#### I18nDateDirective\r\n\r\nDirectiva para renderizar una fecha según el idioma de la configuración de usuario `dateFormat`. Si cambia la configuración vuelve a renderizar la fecha.\r\n\r\nEl valor por defecto del input `format` es `\"shortDate\"`, el de `tooltipFormat` es `undefined` por defecto y en ese caso no se renderiza el tooltip. El tooltip se crea usando la propiedad `name` de HTML.\r\n\r\n```html\r\n<span [i18nDate]=\"notification.createdOn\" format=\"shortDate\" tooltipFormat=\"short\"></span>\r\n\r\n<span [i18nDate]=\"notification.createdOn\"></span>  \r\n\r\n<span i18nDate=\"{{ notification.createdOn }}\"></span>\r\n```\r\n\r\n#### I18nDatePipe\r\n\r\nPipe para renderizar una fecha según la configuración de usuario `dateFormat`.\r\n \r\nSi cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva `*i18nRerenderDate` o `*i18nRerenderTime` para añadir la funcionalidad de refresco cuando cambia la configuración.\r\n \r\nLa propiedad `format` es por defecto `\"shortDate\"`. Acepta los mimos valores que la [DatePipe](https://angular.dev/api/common/DatePipe?tab=usage-notes) de Angular.\r\n\r\n\r\n```html\r\n<span>{{ date | i18nDate }}</span>\r\n\r\n<span>{{ date | i18nDate:\"{format}\" }}</span>\r\n\r\n<span *i18nRerenderDate>{{ date | i18nDate }}</span>\r\n\r\n<span *i18nRerenderTime>{{ date | i18nDate:\"short\" }}</span>\r\n```\r\n\r\n#### I18nNumberDirective\r\n\r\nDirectiva para renderizar un número según la configuración de usuario `numberFormat`. Si cambia la configuración vuelve a renderizar el número. La propiedad `digitsInfo` acepta el mismo formato que la [DecimalPipe](https://angular.dev/api/common/DecimalPipe?tab=usage-notes) de Angular.\r\n\r\n```html\r\n<span [i18nNumber]=\"cost\"></span>\r\n\r\n<span [i18nNumber]=\"cost\" digitsInfo=\"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}\"></span>\r\n\r\n<span i18nNumber=\"{{ cost }}\"></span> \r\n```\r\n\r\n#### I18nNumberPipe\r\n\r\nPipe para renderizar un número según la configuración de usuario `numberFormat`.\r\n \r\nSi cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva `*i18nRerenderNumber` para añadir la funcionalidad de refresco cuando cambia la configuración.\r\n\r\nAcepta el parámetro opcional `digitsInfo` con el mismo formato que la [DecimalPipe](https://angular.dev/api/common/DecimalPipe?tab=usage-notes) de Angular.\r\n\r\n```html\r\n<span>{{ cost | i18nNumber }}\\</span>\r\n\r\n<span>{{ cost | i18nNumber:\"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}\" }}</span>\r\n\r\n<span *i18nRerenderNumber>{{ cost | i18nNumber }}\\</span>\r\n```\r\n\r\n#### I18nCurrencyPipe\r\n\r\nPipe para renderizar un número según la configuración de usuario `numberFormat`.\r\n \r\nSi cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva `*i18nRerenderNumber` para añadir la funcionalidad de refresco cuando cambia la configuración.\r\n\r\nLos parámetros son los siguientes:\r\n\r\n`i18nCurrency:\"currencyCode_ISO4217\":\"narrow|wide\":\"digitsInfo\"`\r\n\r\n```html\r\n<span>{{ cost | i18nCurrency }}</span>\r\n\r\n<span>{{ cost | i18nCurrency:\"EUR\" }}</span>\r\n\r\n<span>{{ cost | i18nCurrency:\"EUR\":\"wide\" }}</span>\r\n\r\n<span>{{ cost | i18nCurrency:\"EUR\":\"narrow\":\"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}\" }}</span>\r\n \r\n<span *i18nRerenderNumber>{{ cost | i18nCurrency }}</span>\r\n```\r\n\r\n#### I18nConcatDateFormatKeyPipe\r\n\r\nPipe que concatena la clave en formato string del `DateFormat` seleccionado por el usuario.\r\n\r\n```ts\r\n// Recordatorio de como es el enumerado DateFormat\r\nexport enum DateFormat {\r\n  MM_DD_YYYY_hhmma = \"MM/dd/yyyy, hh:mm a\",\r\n  DD_MM_YYYY_HHmm = \"dd/MM/yyyy HH:mm\",\r\n  YYYY_MM_DD_HHmm = \"yyyy/MM/dd HH:mm\"\r\n}\r\n```\r\n\r\nSi cambia la configuración NO vuelve a procesar el string. Se puede combinar con la directiva *i18nRerenderDate o *i18nRerenderTime para añadir la funcionalidad de refresco cuando cambia la configuración.\r\n\r\n```html\r\n<span>\r\n  {{ \"date-format.shortDate.\" | i18nConcatDateFormatKey | transloco }}\r\n</span>\r\n\r\n<span *i18nRerenderDate>\r\n  {{ \"date-format.shortDate.\" | i18nConcatDateFormatKey | transloco }}\r\n</span>\r\n\r\n<span *i18nRerenderTime>\r\n  {{ \"date-format.short.\" | i18nConcatDateFormatKey | transloco }}\r\n</span>\r\n```\r\n\r\nEl fichero `en.json` tendría el siguiente aspecto:\r\n\r\n```json\r\n{\r\n  \"date-format.shortDate.MM_DD_YYYY_hhmma\": \"MM/DD/YYYY\",\r\n  \"date-format.shortDate.DD_MM_YYYY_HHmm\": \"DD/MM/YYYY\",\r\n  \"date-format.shortDate.YYYY_MM_DD_HHmm\": \"YYYY/MM/DD\",\r\n  \"date-format.short.MM_DD_YYYY_hhmma\": \"MM/DD/YYYY, hh:mm AM/PM\",\r\n  \"date-format.short.DD_MM_YYYY_HHmm\": \"DD/MM/YYYY hh:mm\",\r\n  \"date-format.short.YYYY_MM_DD_HHmm\": \"YYYY/MM/DD hh:mm\"\r\n}\r\n```\r\n\r\nY el fichero `es.json`:\r\n\r\n```json\r\n{\r\n  \"date-format.shortDate.MM_DD_YYYY_hhmma\": \"MM/DD/AAAA\",\r\n  \"date-format.shortDate.DD_MM_YYYY_HHmm\": \"DD/MM/AAAA\",\r\n  \"date-format.shortDate.YYYY_MM_DD_HHmm\": \"AAAA/MM/DD\",\r\n  \"date-format.short.MM_DD_YYYY_hhmma\": \"MM/DD/AAAA, hh:mm a.m./p.m.\",\r\n  \"date-format.short.DD_MM_YYYY_HHmm\": \"DD/MM/AAAA hh:mm\",\r\n  \"date-format.short.YYYY_MM_DD_HHmm\": \"AAAA/MM/DD hh:mm\"\r\n}\r\n```\r\n\r\n### Funciones de ayuda\r\n\r\n#### shortDate\r\n\r\nDevuelve el formato de fecha definido en el enumerado `DateFormat` pero solo con el año, mes y día, quitando las horas y minutos.\r\n\r\n`shortDate(dateFormat: DateFormat): string`\r\n\r\n```ts\r\nimport { DateFormat, shortDate } from \"@becaskurtces/gaia-ng-front-core\";\r\n\r\nshortDate(DateFormat.MM_DD_YYYY_hhmma); // \"MM/dd/yyyy\"\r\nshortDate(DateFormat.DD_MM_YYYY_HHmm);  // \"dd/MM/yyyy\"\r\nshortDate(DateFormat.YYYY_MM_DD_HHmm);  // \"yyyy/MM/dd\"\r\n```\r\n\r\n\r\n## Sincronización de rutas\r\n\r\nLas apps cargadas en los iframes enviarán al marco global su ruta actual cada vez que cambie, sin incluir el dominio. También pueden recibir del marco global el mismo tipo de mensaje para que la app en el iframe cargue otra ruta.\r\n\r\nSi nuestra app tiene el fichero `app.config.ts`:\r\n\r\n```ts\r\nimport { provideIframeSyncRoute } from '@becaskurtces/gaia-ng-front-core';\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideIframeSyncRoute()\r\n  ]\r\n};\r\n```\r\n\r\nO importándolo en un `NgModule`:\r\n\r\n```ts\r\nimport { provideIframeSyncRoute } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@NgModule({\r\n  providers: [\r\n    provideIframeSyncRoute()\r\n  ]\r\n})\r\nexport class MyModule {}\r\n```\r\n\r\nNo es necesario hacer nada más.\r\n\r\n\r\n## Sincronización de las opciones del menú\r\n\r\nPara poder sincronizar el menú de nuestra aplicación con el marco global, debemos hacer los siguientes cambios:\r\n\r\n- Crear la configuración del menú (`AppMenuSyncConfig`).\r\n- Añadir JSONs con las traducciones del menú.\r\n- Cambiar la función \"boostrap\" del fichero `main.ts`.\r\n- Actualizar la configuración del marco global (`gaia-fe-home`) para habilitar la sincronización.\r\n- Asegurarnos que el servicio desplegado con la aplicación permite los `CORS` a la aplicación `gaia-fe-home`.\r\n\r\nPara que el menú se sincronice, no es necesario abrir la aplicación dentro del marco global. La sincronización del menú se realiza internamente usando un iframe temporal (y no visible para el usuario) y postMessage. Se inicia una app con la menor lógica posible (solo con la autenticación en iframe y lógica de sincronización del menú) para que la sincronización sea lo más rápida posible. Cuando termina la sincronización o salta un timeout definido en el marco global, el iframe se destruye.\r\n\r\n### Crear la configuración del menú\r\n\r\nLa interfaz `AppMenuSyncConfig` consta de la siguientes propiedades:\r\n\r\n```ts\r\n/**\r\n * Configuración para la sincronización del menú de la aplicación\r\n */\r\nexport interface AppMenuSyncConfig {\r\n  /**\r\n   * Configuración de autenticación.  \r\n   * Puede ser un objeto o una función que devuelva una promesa u observable con la configuración.\r\n   * Usa la misma interfaz que la función \"provideAuth\".\r\n   */\r\n  auth: ProvideAuthConfig;\r\n  /**\r\n   * Función para construir el menú de la aplicación\r\n   */\r\n  buildMenuFn: BuildMenuFn;\r\n}\r\n```\r\n\r\n`ProvideAuthConfig` es un tipo con la siguiente definición:\r\n\r\n```ts\r\n// AuthConfig o función que devuelve Promise<AuthConfig> o Observable<AuthConfig>\r\nexport type ProvideAuthConfig = AuthConfig | (() => Promise<AuthConfig>) | (() => Observable<AuthConfig>);\r\n```\r\n\r\nLa función con la interfaz `BuildMenuFn` es la siguiente:\r\n\r\n```ts\r\n/**\r\n * Interfaz para la función que construye el menú.\r\n */\r\nexport type BuildMenuFn = (ctx: BuildMenuFnContext) => AppMenu;\r\n\r\n/**\r\n * Contexto para la función que construye el menú.\r\n */\r\nexport interface BuildMenuFnContext {\r\n  /**\r\n   * Devuelve true si el usuario tiene TODOS los roles indicados.\r\n   */\r\n  userHasRoles: (roles: string[]) => boolean;\r\n  /**\r\n   * Devuelve true si el usuario NO TIENE TODOS los roles indicados.\r\n   */\r\n  userDoesntHaveRoles: (roles: string[]) => boolean;\r\n  /**\r\n   * Devuelve true si el usuario tiene ALGUNO de los roles indicados.\r\n   */\r\n  userHasAnyRole: (roles: string[]) => boolean;\r\n}\r\n```\r\n\r\nInterfaces para definir el menú (`AppMenu`, `AppMenuTranslations` y `AppMenuItem`):\r\n\r\n```ts\r\n/**\r\n * Menú de la aplicación que se mostrará dentro\r\n * del marco global.\r\n */\r\nexport interface AppMenu {\r\n  /**\r\n   * Idioma a usar por defecto si en \"translations\"\r\n   * no se encuentra el idioma elegido por el usuario.\r\n   */\r\n  defaultLocale: Locale | string;\r\n  /**\r\n   * Paths para obtener las traducciones.\r\n   * \r\n   * Ejemplo:\r\n   * ```json\r\n   * {\r\n   *   \"en\": \"/assets/i18n/en.json\",\r\n   *   \"es\": \"/assets/i18n/es.json\"\r\n   * }\r\n   * ```\r\n   */\r\n  translations: AppMenuTranslations;\r\n  /**\r\n   * Item principal del menú.\r\n   */\r\n  rootItem: AppMenuItem;\r\n}\r\n```\r\n\r\n```ts\r\n/**\r\n * Clave: Código de idioma.  \r\n * Valor: Path al JSON con las traducciones.\r\n * \r\n * Ejemplo:\r\n * {\r\n *   \"en\": \"/assets/i18n/en.json\",\r\n *   \"es\": \"/assets/i18n/es.json\"\r\n * }\r\n */\r\nexport interface AppMenuTranslations {\r\n  [key: string]: string;\r\n}\r\n```\r\n\r\n```ts\r\n/**\r\n * Elemento del menú.\r\n */\r\nexport interface AppMenuItem {\r\n  /**\r\n   * Clave de las traducciones para usar como título/label del elemento\r\n   */\r\n  titleI18n: string;\r\n  /**\r\n   * Path al recurso asociado al elemento.  \r\n   * La ruta es relativa al AppMenuItem padre.\r\n   */\r\n  path: string;\r\n  /**\r\n   * Indica si el elemento está deshabilitado.\r\n   */\r\n  disabled?: boolean;\r\n  /**\r\n   * Elementos hijo. Por ahora solo se muestran hasta\r\n   * 2 niveles de menú sin contar con el nivel raíz.\r\n   */\r\n  children?: AppMenuItem[];\r\n}\r\n```\r\n\r\n#### Ejemplo de configuración de menú\r\n\r\n`app-menu.config.ts`\r\n\r\n```ts\r\nimport { AppMenuSyncConfig } from \"@becaskurtces/gaia-ng-front-core/appmenu\";\r\nimport { environment } from '@env';\r\n\r\nexport const appMenuConfig: AppMenuSyncConfig = {\r\n  // En este ejemplo pasamos los valores para la\r\n  // autenticación usando la configuración de entorno.\r\n  // La propiedad usa la misma interfaz de configuración que \"provideAuth\".\r\n  // Más abajo hay un ejemplo para devolver la configuración\r\n  // dentro de una promesa.\r\n  auth: environment.auth,\r\n  // Función para construir el menú\r\n  buildMenuFn: (ctx) => {\r\n    return {\r\n      // Idioma por defecto a usar si el usuario ha elegido un idioma\r\n      // que no se encuentra en \"translations\", por ejemplo \"fr\".\r\n      defaultLocale: 'en',\r\n      // Rutas de los ficheros con las traducciones del menú\r\n      translations: {\r\n        en: '/assets/i18n/appmenu/en.json',\r\n        es: '/assets/i18n/appmenu/es.json'\r\n      },\r\n      rootItem: {\r\n        titleI18n: 'menu.root-item',\r\n        // Path raíz de la aplicación\r\n        path: '',\r\n        // Ocultar elemento raíz (y por tanto todo el menú)\r\n        // si el usuario no tiene los roles especificados:\r\n        // disabled: ctx.userDoesntHaveRoles(['my-app-role'])\r\n        children: [\r\n          {\r\n            titleI18n: 'menu.child.admin',\r\n            path: '/admin',\r\n            // Ocultar item del menú si no tiene los roles \"admin\" y \"manager\"\r\n            disabled: ctx.userDoesntHaveRoles(['admin', 'manager'])\r\n          },\r\n          {\r\n            titleI18n: \"menu.child.option-a\",\r\n              // Los paths son relativos al path padre\r\n            path: \"/option-a\",\r\n            children: [\r\n              {\r\n                titleI18n: \"menu.child.option-a.option-1\",\r\n                // Los paths son relativos al path padre.\r\n                // Para esta opción el path resultante sería:\r\n                // \"/option-a/option-1\"\r\n                path: \"/option-1\"\r\n              },\r\n              {\r\n                titleI18n: \"menu.child.option-a.option-2\",\r\n                path: \"/option-2\",\r\n                // Ocultar item del menú si no tiene el rol \"second-level-option-a2\"\r\n                disabled: ctx.userDoesntHaveRoles(['second-level-option-a2'])\r\n              }\r\n            ]\r\n          },\r\n          {\r\n            titleI18n: \"menu.child.option-b\",\r\n            path: \"/option-b\"\r\n          },\r\n          {\r\n            titleI18n: \"menu.child.option-c\",\r\n            path: \"/option-c\"\r\n          }\r\n        ]\r\n      }\r\n    };\r\n  }\r\n};\r\n```\r\n\r\nEn el ejemplo se devuelve un objecto directamente y se usan las funciones del contexto para ocultar opciones del menú. Al ser una función, se pueden usar ifs, fors, etc para contruir el menú como se quiera.\r\n\r\n#### Más ejemplos de la propiedad \"auth\"\r\n\r\nEjemplo cargando la configuración de un fichero JSON usando el `fetch` nativo y devolviendo un `Promise<AuthConfig>`:\r\n\r\n```ts\r\nimport { AppMenuSyncConfig } from \"@becaskurtces/gaia-ng-front-core/appmenu\";\r\nimport { findFirstUUID } from \"@becaskurtces/gaia-ng-front-core\";\r\nimport { environment } from \"@/environment\";\r\nimport { MyAppConfig } from \"./my-app-config\";\r\n\r\nexport const appMenuConfig: AppMenuSyncConfig = {\r\n  auth: async () => {\r\n    const response = await fetch(environment.configFile);\r\n    const config = response.ok ? await response.json() as MyAppConfig : null;\r\n    return {\r\n      clientId: config.security.clientId,\r\n      scope: config.security.scopes,\r\n      // Podemos usar la función \"findFirstUUID\" para extraer el\r\n      // \"tenantId\" de la URL usada como \"authority\"\r\n      tenantId: findFirstUUID(config.security.authority)\r\n    };\r\n  },\r\n  buildMenuFn: (ctx) => { ... }\r\n};\r\n```\r\n\r\nEjemplo usando `HttpClient` de Angular y devolviendo un `Observable<AuthConfig>`:\r\n\r\n```ts\r\nimport { inject } from \"@angular/core\";\r\nimport { HttpClient } from \"@angular/common/http\";\r\nimport { AppMenuSyncConfig } from \"@becaskurtces/gaia-ng-front-core/appmenu\";\r\nimport { environment } from \"@/environment\";\r\nimport { MyAppConfig } from \"./my-app-config\";\r\n\r\nexport const appMenuConfig: AppMenuSyncConfig = {\r\n  auth: () => {\r\n    const httpClient = inject(HttpClient);\r\n\r\n    return httpClient.get('/assets/config.json').pipe(\r\n      map((config: MyAppConfig) => {\r\n        return {\r\n          clientId: config.clientId,\r\n          scope: config.scope,\r\n          tenantId: config.tenantId,\r\n          // etc...\r\n        } as AuthConfig;\r\n      })\r\n    );\r\n  },\r\n  buildMenuFn: (ctx) => { ... }\r\n};\r\n```\r\n\r\nEjemplo cargando la configuración desde otra variable:\r\n\r\n```ts\r\nimport { AppMenuSyncConfig } from \"@becaskurtces/gaia-ng-front-core/appmenu\";\r\nimport { config } from \"./config\";\r\n\r\nexport const appMenuConfig: AppMenuSyncConfig = {\r\n  auth: {\r\n    clientId: config.auth.clientId,\r\n    scope: config.auth.scopes,\r\n    tenantId: config.auth.tenantId\r\n  },\r\n  buildMenuFn: (ctx) => { ... }\r\n};\r\n```\r\n\r\n\r\n### Añadir JSONs con las traducciones del menú\r\n\r\nCrea todos los ficheros `json` que hayas definido en la propiedad `translations`. **No es necesario añadir un prefijo único a las traducciones, el marco global se encarga de añadir uno para evitar conflictos con otras aplicaciones.**\r\n\r\nEjemplo de fichero `/assets/i18n/appmenu/en.json`:\r\n\r\n```json\r\n{\r\n  \"menu.root-item\": \"My App\",\r\n  \"menu.child.admin\": \"Admin\",\r\n  \"menu.child.option-a\": \"Option A\",\r\n  ...\r\n}\r\n```\r\n\r\n### Cambiar la función \"boostrap\" del fichero `main.ts`\r\n\r\nPara iniciar la app con la menor lógica posible, como se ha comentado más arriba, se ha creado una nueva función bootstrap. Esta función inicia la app en modo sincronización del menú si se cumple una condición y si esta condición no se cumple, se inicia la app de forma normal, al igual que el código original.\r\n\r\n#### Si usamos `ApplicationConfig` (app.config.ts)\r\n\r\nCambiar `bootstrapApplication` por `bootstrapApplicationWithMenuSync`:\r\n\r\nOriginal:\r\n\r\n```ts\r\nimport { bootstrapApplication } from '@angular/platform-browser';\r\nimport { appConfig } from './app/app.config';\r\nimport { AppComponent } from './app/app.component';\r\n\r\nbootstrapApplication(AppComponent, appConfig)\r\n  .catch((err) => console.error(err));\r\n```\r\n\r\nNuevo:\r\n\r\n```ts\r\nimport { bootstrapApplicationWithMenuSync } from '@becaskurtces/gaia-ng-front-core/appmenu';\r\nimport { appConfig } from './app/app.config';\r\nimport { AppComponent } from './app/app.component';\r\nimport { appMenuConfig } from './app/app-menu.config';\r\n\r\nbootstrapApplicationWithMenuSync(AppComponent, appConfig, appMenuConfig)\r\n  .catch((err) => console.error(err));\r\n```\r\n\r\n#### Si usamos `NgModule`\r\n\r\nCambiar `platformBrowserDynamic().bootstrapModule(...)` por `bootstrapModuleWithMenuSync`:\r\n\r\nOriginal:\r\n\r\n```ts\r\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\r\nimport { AppModule } from './app/app.module';\r\n\r\nplatformBrowserDynamic()\r\n  .bootstrapModule(AppModule)\r\n  .catch(err => console.error(err));\r\n```\r\n\r\nNuevo:\r\n\r\n```ts\r\nimport { bootstrapModuleWithMenuSync } from '@becaskurtces/gaia-ng-front-core/appmenu';\r\nimport { AppModule } from './app/app.module';\r\nimport { appMenuConfig } from './app/app-menu.config';\r\n\r\nbootstrapModuleWithMenuSync(AppModule, appMenuConfig)\r\n  .catch(err => console.error(err));\r\n```\r\n\r\n### Actualizar la configuración del marco global (`gaia-fe-home`) para habilitar la sincronización\r\n\r\nEl marco global debe habilitar para cada app la sincronización del menú. Para ello, las apps que tengan implementada esta funcionalidad, deben añadir la propiedad `hasAppMenuSync` con el valor `true` en el item raíz de cada app en la configuración del marco global.\r\n\r\n**En el caso de que ocurra un error de sincronización, el marco global usará el menú definido en su configuración de entorno (como si no existiera la sincronización).**\r\n\r\n### Permitir CORS al Marco Global (gaia-fe-home)\r\n\r\nEl marco global debe tener acceso a los JSON con las traducciones del menú. Para ello debemos asegurarnos que no se bloquean los CORS al Marco Global.\r\n\r\nLa forma fácil usando Nginx es añadir la cabecera `Access-Control-Allow-Origin` con el valor `*`. Para más complejas, buscar en Internet como sería la configuración correcta.\r\n\r\n```nginx\r\nserver {\r\n  ...\r\n\r\n\r\n  location / {\r\n    ... \r\n\r\n    add_header Access-Control-Allow-Origin \"*\" always;\r\n  }\r\n}\r\n```\r\n\r\n\r\n## Solicitar al marco global abrir una URL (en la misma u otra pestaña)\r\n\r\nSolicita al marco global abrir una URL de una de las aplicaciones pero dentro del marco global y no de forma independiente. La URL de la aplicación que se quiere abrir, debe estar configurada en el menú del marco global.\r\n\r\nPara abrir otra aplicación diferente a la actual, se debe usar el dominio de esa app que se quiere abrir y NO la URL del marco global. El marco global transforma la URL de cada dominio en el URL correcta del marco global. De esta forma esta funcionalidad funciona tanto estando dentro como fuera del marco global. Si la aplicación no se está ejecutando dentro del marco global, la URL se abre sin modificar.\r\n\r\nSi se solicita abrir una URL que solo es un path (`no` empieza por `http://` o `https://`), la URL se transform en `location.origin + URL`.\r\n\r\nA partir de la versión `1.20.0` de la librería, este servicio funciona también sin abrir la aplicación dentro del marco global.\r\n\r\n```ts\r\nimport { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Component({ ... })\r\nexport class MyComponent {\r\n\r\n  private openUrlRequestService = inject(OpenUrlRequestService);\r\n\r\n  onUrlClick(url: string): void {\r\n\r\n    // En la misma pestaña\r\n    this.openUrlRequestService.openUrl(url);\r\n\r\n    // En una pestaña nueva\r\n    this.openUrlRequestService.openUrlInNewTab(url);\r\n  }\r\n\r\n}\r\n```\r\n\r\nA partir de la versión `1.20.0` también, se puede añadir un parámetro a la URL para que la aplicación destino pueda conocer que aplicación la ha abierto.\r\n\r\n```ts\r\nimport { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Component({ ... })\r\nexport class MyComponent {\r\n\r\n  private openUrlRequestService = inject(OpenUrlRequestService);\r\n\r\n  onUrlClick(url: string): void {\r\n\r\n    // En la misma pestaña\r\n    this.openUrlRequestService.openUrl(url, { informOriginUrl: true });\r\n\r\n    // En una pestaña nueva\r\n    this.openUrlRequestService.openUrlInNewTab(url, { informOriginUrl: true });\r\n  }\r\n\r\n}\r\n```\r\n\r\nEl parámetro se llama `entOpenUrlOrigin` y al iniciar la app se puede obtener fácilmente con este método:\r\n\r\n```ts\r\nimport { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Component({ ... })\r\nexport class MyComponent implements OnInit {\r\n\r\n  private openUrlRequestService = inject(OpenUrlRequestService);\r\n\r\n  ngOnInit(): void {\r\n\r\n    const originUrl: string | null = this.openUrlRequestService.captureOpenUrlOriginParam();\r\n\r\n  }\r\n\r\n}\r\n```\r\n\r\n\r\n## Solicitar al marco global cerrar la ventana actual\r\n\r\n> **Importante**: Solo funciona si la ventana ha sido abierta antes por la propia app. No puedes cerrar una ventana que ha sido creada manualmente por el usuario.\r\n\r\nA partir de la versión `1.20.0` de la librería, este servicio funciona también sin abrir la aplicación dentro del marco global.\r\n\r\n```ts\r\nimport { CloseTabRequestService } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Component({ ... })\r\nexport class MyComponent {\r\n\r\n  private closeTabRequestService = inject(CloseTabRequestService);\r\n\r\n  closeCurrentTab(): void {\r\n    this.closeTabRequestService.close();\r\n  }\r\n\r\n}\r\n```\r\n\r\n\r\n## Solicitar al marco global que muestre una configuración propia de mi app\r\n\r\n> **Importante**: Los valores de los \"labels\", deben enviarse traducidos al idioma activo. El marco global no conoce las traducciones de otras aplicaciones y por tanto no puede realizar la traducción.\r\n\r\n```ts\r\nimport { CustomConfigRequestService, CustomConfigRequest } from '@becaskurtces/gaia-ng-front-core';\r\n\r\n@Component({ ... })\r\nexport class MyComponent implements OnInit {\r\n\r\n  private customConfigRequestService = inject(CustomConfigRequestService);\r\n\r\n  private ngOnDestroy$ = new Subject();\r\n\r\n  ngOnInit() {\r\n    this.listentoCustomConfigChanges();\r\n    this.sendCustomConfig();\r\n  }\r\n\r\n  ngOnDestroy() {\r\n    this.ngOnDestroy$.next(null);\r\n    this.ngOnDestroy$.complete();\r\n  }\r\n\r\n  // Envía la configuración con los campos deseados.\r\n  private sendCustomConfig(): void {\r\n    const config: CustomConfigRequest = {\r\n      items: [\r\n        {\r\n          id: \"id1\",\r\n          label: \"NombreCampo1\",\r\n          value: \"valor1\",\r\n          options: [\r\n            {\r\n              label: \"NombreValor1\",\r\n              value: \"valor1\"\r\n            },\r\n            {\r\n              label: \"NombreValor2\",\r\n              value: \"valor2\"\r\n            },\r\n            {\r\n              label: \"NombreValor3\",\r\n              value: \"valor3\"\r\n            }\r\n          ]\r\n        },\r\n        {\r\n          id: \"id2\",\r\n          label: \"NombreCampo2\",\r\n          placeholder: \"Elige un valor\",\r\n          enableFilter: true,\r\n          filterPlaceholder: \"Filtra los valores\",\r\n          options: [\r\n            {\r\n              label: \"NombreValor1\",\r\n              value: \"valor1\"\r\n            },\r\n            {\r\n              label: \"NombreValor2\",\r\n              value: \"valor2\"\r\n            }\r\n          ]\r\n        }\r\n      ]\r\n    };\r\n\r\n    this.customConfigRequestService.sendConfig(config);\r\n  }\r\n\r\n  // Escucha los eventos de cambio de valor.\r\n  // Se devuelve el \"id\" y el \"value\" del campo.\r\n  private listentoCustomConfigChanges() {\r\n    this.customConfigRequestService.onValueChange()\r\n      .pipe(takeUntil(ngOnDestroy$))\r\n      .subscribe(valueChange => {\r\n        console.log(valueChange.id, \"-\", valueChange.value);\r\n      });\r\n  }\r\n\r\n}\r\n```\r\n\r\nLa interfaz de `CustomConfigRequest` es la siguiente:\r\n\r\n```ts\r\nexport interface CustomConfigRequest {\r\n  items: CustomConfigRequestItem[];\r\n}\r\n\r\nexport interface CustomConfigRequestItem {\r\n  /** Identificador de la configuración */\r\n  id: string;\r\n  /** Etiqueta del selector */\r\n  label: string;\r\n  /** Valor actual si tiene */\r\n  value?: any;\r\n  /** Opciones a elegir */\r\n  options: CustomConfigRequestItemOption[];\r\n  /** Placeholder */\r\n  placeholder?: string;\r\n  /** Habilitar un filtro de texto para filtrar las opciones */\r\n  enableFilter?: boolean;\r\n  /** Placeholder del filtro */\r\n  filterPlaceholder?: string;\r\n}\r\n\r\nexport interface CustomConfigRequestItemOption {\r\n  /** Lo que se va a mostrar en la UI como valor */\r\n  label: string;\r\n  /** El valor real del elemento */\r\n  value: any;\r\n}\r\n```\r\n\r\n\r\n## Autenticación\r\n\r\nEn este módulo se ha reunido la configuración mínima de autenticación. Se usa la dependencia `angular-auth-oidc-client`. En principio la configuración de este módulo debería ser suficiente para todas las aplicaciones.\r\n\r\nSolo necesitamos importar `provideAuth(config: ProvideAuthConfig)` y `authInterceptor` que añade la cabecera \"Authentication\" a todas las peticiones HTTP realizadas con el cliente HTTP de Angular. Este modulo se encarga de sincronizar el usuario del marco global con el iframe.\r\n\r\nLa configuración acepta las siguientes propiedades:\r\n\r\n```ts\r\n// AuthConfig o función que devuelve Promise<AuthConfig> o Observable<AuthConfig>\r\nexport type ProvideAuthConfig = AuthConfig | (() => Promise<AuthConfig>) | (() => Observable<AuthConfig>);\r\n```\r\n\r\n```ts\r\nexport interface AuthConfig {\r\n  clientId: string;\r\n  tenantId: string;\r\n  scope: string;\r\n  /**\r\n   * URL a la que redirigir al hacer un login correcto.  \r\n   * Si no se informa el valor, se usa por defecto: `window.location.origin`.  \r\n   * Si el valor NO empieza por `http(s)://` se usa `${window.location.origin}/?${redirectUrl}`.  \r\n   * En cualquier otro caso, se usa el valor indicado.\r\n   */\r\n  redirectUrl?: string;\r\n  /**\r\n   * Dominio de correo permitido. Si no se especifica, la\r\n   * ventana de login permite usar cualquier correo. En el\r\n   * caso de tener varios usuarios con la sesión iniciada\r\n   * en Microsoft, se filtran las cuentas usando este dominio.\r\n   */\r\n  domainHint?: string;\r\n  /**\r\n   * Rutas a las que añadir la cabecera Authorization. Recuerda importar el\r\n   * interceptor en los providers. Puedes usar el interceptor de \"@becaskurtces/gaia-ng-front-core\"\r\n   * o el de \"angular-auth-oidc-client\". El interceptor de \"@becaskurtces/gaia-ng-front-core\"\r\n   * añade la cabecera a todas las peticiones si no se especifica ninguna ruta.  \r\n   * Las rutas que no empiecen por `http(s)://`, se les añadirá el valor de\r\n   * `location.origin` al principio de la ruta.\r\n   */\r\n  secureRoutes?: string[];\r\n  /**\r\n   * Claves para acceder a las propiedas del token JWT y obtener el nombre,\r\n   * nombre de usuario e email del usuario logueado. Por defecto:  \r\n   * \r\n   * { nameKey: \"name\", emailKey: \"email\", usernameKey: \"preferred_username\" }\r\n   * \r\n   * Se usa en el servicio AuthHelperService.\r\n   */\r\n  jwtUserDataKeys?: JwtUserDataKeys;\r\n  /**\r\n   * Lista de roles mínimos requeridos por el usuario para usar la app.\r\n   * Si la lista es vacía o nula, no se requiere ningún rol.\r\n   * \r\n   * También puede ser una función que recibe un contexto\r\n   * con métodos para comprobar los roles del usuario.  \r\n   * Si la función devuelve `true`, se considera que el usuario\r\n   * tiene los roles correctos, `false` en caso contrario.  \r\n   * `CheckUserRolesFn = (ctx: CheckUserRolesFnContext) => boolean`\r\n   */\r\n  userMinimuentquiredRoles?: string[] | CheckUserRolesFn;\r\n  /**\r\n   * Lanzar un error si el proceso de login no se completa con éxito.  \r\n   * El error provoca que la app no termine de inicializarse.  \r\n   * \r\n   * Por defecto: true.\r\n   */\r\n  throwErrorWhenUnauthenticated?: boolean;\r\n  /**\r\n   * Lanzar un error si el usuario se autentica con éxito pero\r\n   * no tiene los roles mínimos requeridos.  \r\n   * El error provoca que la app no termine de inicializarse.  \r\n   * \r\n   * Por defecto: true.\r\n   */\r\n  throwErrorWhenUserDoesNotHaveRequiredRoles?: boolean;\r\n    /**\r\n   * Indica si se debe deshabilitar el login automático al iniciar la aplicación.\r\n   * Si se establece a true, la aplicación no intentará iniciar sesión automáticamente al arrancar.\r\n   * Esto puede ser útil en escenarios donde se desea iniciar el proceso de autenticación desde un componente específico.\r\n   *\r\n   * Por defecto: false.\r\n   */\r\n  disableAutoLoginOnStartup?: boolean;\r\n  /**\r\n   * Indica si se debe forzar el uso de un popup para el login.\r\n   * \r\n   * Por defecto: false.\r\n   */\r\n  forceLoginWithPopup?: boolean;\r\n  /**\r\n   * Indica si se debe deshabilitar la comprobación para saber si el usuario\r\n   * permite abrir popups. Si se establece a true, la aplicación no verificará\r\n   * si el usuario ha permitido abrir popups antes de intentar iniciar sesión\r\n   * con un popup.\r\n   * \r\n   * Si se deshabilita esta comprobación, se asume que el usuario permite abrir popups,\r\n   * lo que puede provocar errores en el proceso de autenticación si el usuario tiene\r\n   * bloqueados los popups en su navegador.\r\n   * \r\n   * La verificación de permisos implica abrir un popup de prueba.\r\n   * \r\n   * Por defecto: false.\r\n   */\r\n  disableCheckPopupPermission?: boolean;\r\n}\r\n```\r\n\r\n```ts\r\n/**\r\n * Contexto para la función que comprueba los roles del usuario.\r\n */\r\nexport interface CheckUserRolesFnContext {\r\n  /**\r\n   * Devuelve true si el usuario tiene TODOS los roles indicados.\r\n   */\r\n  userHasRoles: (roles: string[]) => boolean;\r\n  /**\r\n   * Devuelve true si el usuario NO TIENE TODOS los roles indicados.\r\n   */\r\n  userDoesntHaveRoles: (roles: string[]) => boolean;\r\n  /**\r\n   * Devuelve true si el usuario tiene ALGUNO de los roles indicados.\r\n   */\r\n  userHasAnyRole: (roles: string[]) => boolean;\r\n}\r\n\r\n/**\r\n * Interfaz para la función que comprueba los roles del usuario.\r\n */\r\nexport type CheckUserRolesFn = (ctx: CheckUserRolesFnContext) => boolean;\r\n```\r\n\r\n> Con `domainHint` podemos hacer que Azure AD solo nos muestre cuentas con un dominio especifico, por ejemplo, si `domainHint: \"entitynopro.onmicrosoft.com\"`, entonces solo las cuentas con un email con el dominio \"entitynopro.onmicrosoft.com\", podrán hacer login.\r\n> \r\n> Por ejemplo, si dejamos `domainHint` vacío y hemos iniciado sesión en Microsoft con más de una cuenta, aparecerá una pantalla como la siguiente, en la que hay 3 usuarios:\r\n>\r\n> ![Selección de usuario en el login de Microsoft](./docs/images/azure-ad/login-screen-users.jpg)\r\n> \r\n> Si especificamos `domainHint: \"entitynopro.onmicrosoft.com\"`, entonces solo se mostraría la primera cuenta.\r\n\r\n\r\nSi nuestra app tiene el fichero `app.config.ts`:\r\n\r\n```ts\r\nimport { ApplicationConfig } from '@angular/core';\r\nimport { provideHttpClient, withInterceptors } from '@angular/common/http';\r\nimport { authInterceptor, provideAuth, provideI18n } from '@becaskurtces/gaia-ng-front-core';\r\nimport { environment } from '@env';\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideHttpClient(\r\n      withInterceptors([\r\n        authInterceptor\r\n      ])\r\n    ),\r\n    provideAuth(environment.auth)\r\n  ]\r\n};\r\n```\r\n\r\nO importándolo en un `NgModule`:\r\n\r\n```ts\r\nimport { NgModule } from '@angular/core';\r\nimport { provideHttpClient, withInterceptors } from '@angular/common/http';\r\nimport { authInterceptor, provideAuth, provideI18n } from '@becaskurtces/gaia-ng-front-core';\r\nimport { environment } from '@env';\r\n\r\n@NgModule({\r\n  providers: [\r\n    provideHttpClient(\r\n      withInterceptors([\r\n        authInterceptor\r\n      ])\r\n    ),\r\n    provideAuth(environment.auth)\r\n  ]\r\n})\r\nexport class MyModule {}\r\n```\r\n\r\nPara securizar solo los endpoints de la API, se pueden especificar las rutas en la configuración por entornos o añadir directamente la URL de la API en \"secureRoutes\". Las rutas que no empiecen por \"http(s)://\", se les añadirá el valor de `location.origin` al principio de la ruta.\r\n\r\n```ts\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideHttpClient(\r\n      withInterceptors([\r\n        authInterceptor\r\n      ])\r\n    ),\r\n    provideAuth({\r\n      ...environment.auth,\r\n      secureRoutes: [environment.apiUrl]\r\n    })\r\n  ]\r\n};\r\n```\r\n\r\n> IMPORTANTE: Si no configuras las \"secureRoutes\", puedes tener problemas al obtener los ficheros de traducciones de los \"/assets\" una vez esté la app desplegada en AWS, porque no se permite usar la cabecera Authorization para obtener ficheros estáticos (sólo si se sirve la app desde S3).\r\n\r\n### Cargar configuración de forma asíncrona\r\n\r\nComo `provideAuth` acepta funciones que devuelven la configuración en una promesa u observable, podemos cargar la configuración de forma asíncrona, por ejemplo:\r\n\r\n```ts\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideHttpClient(\r\n      withInterceptors([\r\n        authInterceptor\r\n      ])\r\n    ),\r\n    provideAuth(authConfigLoader)\r\n  ]\r\n};\r\n\r\n// Función que carga la configuración\r\n// y la mapea a la interfaz AuthConfig\r\nconst authConfigLoader = () => {\r\n  const configService = inject(MyConfigService);\r\n\r\n  return configService.initialized().pipe(\r\n    map(config => {\r\n      return {\r\n        clientId: config.clientId,\r\n        scope: config.scope,\r\n        tenantId: config.tenantId,\r\n        // etc...\r\n      } as AuthConfig;\r\n    }),\r\n    take(1)\r\n  );\r\n}\r\n```\r\n\r\nOtro ejemplo de función que carga la configuración:\r\n\r\n```ts\r\nconst authConfigJsonLoader = () => {\r\n  const httpClient = inject(HttpClient);\r\n\r\n  return httpClient.get<MyAppConfig>('/assets/config.json').pipe(\r\n    map(config => {\r\n      return {\r\n        clientId: config.clientId,\r\n        scope: config.scope,\r\n        tenantId: config.tenantId,\r\n        // etc...\r\n      } as AuthConfig;\r\n    })\r\n  );\r\n}\r\n```\r\n\r\n\r\n### Mostrar errores de autenticación\r\n\r\nEn el caso de que ocurra un error en la autenticación del usuario, lo ideal es informar al usuario de ello.\r\n\r\n#### Método rápido\r\n\r\nAñade `provideAuthErrorDialog` a tu módulo principal o `app.config.ts` para mostrar un diálogo informativo cuando ocurran errores de autenticación o por no cumplir con los roles mínimos:\r\n\r\n```ts\r\nimport { provideAuthErrorDialog } from \"@becaskurtces/gaia-ng-front-core/ui\";\r\n\r\nexport const appConfig: ApplicationConfig = {\r\n  providers: [\r\n    provideAuthErrorDialog()\r\n  ]\r\n};\r\n```\r\n\r\nAñade las traducciones de los diálogos a tu app copiando los `assets` de `@becaskurtces/gaia-ng-front-core` a los assets de tu app.\r\n\r\nPara ello copia lo siguiente en el fichero `angular.json`, en el listado de `assets`:\r\n\r\n```json\r\n{\r\n  \"glob\": \"**/*\",\r\n  \"input\": \"node_modules/@becaskurtces/gaia-ng-front-core/assets/i18n\",\r\n  \"output\": \"assets/i18n\"\r\n}\r\n```\r\n\r\nQuedaría así:\r\n\r\n`angular.json`\r\n\r\n```json\r\n{\r\n  \"$schema\": \"./node_modules/@angular/cli/lib/config/schema.json\",\r\n  \"version\": 1,\r\n  \"newProjectRoot\": \"projects\",\r\n  \"projects\": {\r\n    \"my-app\": {\r\n      \"architect\": {\r\n        \"build\": {\r\n          \"options\": {\r\n            \"assets\": [\r\n              {\r\n                \"glob\": \"**/*\",\r\n                \"input\": \"node_modules/@becaskurtces/gaia-ng-front-core/assets/i18n\",\r\n                \"output\": \"assets/i18n\"\r\n              }\r\n            ]\r\n          }\r\n        }\r\n      }\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n\r\n#### Método personalizado\r\n\r\nPara poder saber si ha ocurrido algún error, podemos suscribirnos al Observable `AuthService.checkAuth$`, que guarda el último resultado del método `AuthService.checkAuth()`.\r\n\r\nComo la autenticación se hace en un \"initializer\", debemos suscribirnos al observable en otro \"initializer\" y usar dialogs o snackbars para mostrar el error. Si falla algún initializer (como cuando falla la autenticación, debido a que se devuelve una excepción), el resto del código de la app no se ejecuta (por ejemplo, el app.component.ts, no se carga), por tanto no podemos poner la lógica en un componente.\r\n\r\nEl primer valor que puede devolver `AuthService.checkAuth$` es `undefined`. Esto significa que `AuthService.checkAuth()` todavía no se ha llamado. Una vez se llama a `AuthService.checkAuth()`, se obtiene el resultado de la autenticación.\r\n\r\n`provide-auth-error-dialog.ts`\r\n\r\n```ts\r\nimport { APP_INITIALIZER, EnvironmentProviders, makeEnvironmentProviders } from \"@angular/core\";\r\nimport { TranslocoService } from \"@jsverse/transloco\";\r\nimport { AuthService, CheckAuthResult, getInitUserConfig, getPostLoginRoute } from \"@becaskurtces/gaia-ng-front-core\";\r\nimport { EntDialogService, EntDialogType } from \"@becaskurtces/gaia-ng-front-core/ui\";\r\nimport { Observable, filter, map, take } from \"rxjs\";\r\n\r\n\r\n/**\r\n * Muestra un diálogo de error en caso de que ocurra algún\r\n * error en el proceso de autenticación.\r\n */\r\nexport function provideAuthErrorDialog(): EnvironmentProviders {\r\n  return makeEnvironmentProviders([\r\n    {\r\n      provide: APP_INITIALIZER,\r\n      useFactory: authErrorCheck,\r\n      deps: [AuthService, EntDialogService, TranslocoService],\r\n      multi: true\r\n    }\r\n  ]);\r\n}\r\n\r\nfunction authErrorCheck(\r\n  authService: AuthService,\r\n  dialogService: EntDialogService,\r\n  translocoService: TranslocoService\r\n): () => Observable<void> {\r\n  return () => authService.checkAuth$.pipe(\r\n    filter(res => !!res),\r\n    take(1),\r\n    map(result => {\r\n      if (result == CheckAuthResult.ERROR)\r\n        openGenericErrorDialog(dialogService, translocoService);\r\n\r\n      if (result == CheckAuthResult.POPUP_BLOCKED)\r\n        openPopupsBlockedErrorDialog(dialogService, translocoService);\r\n    })\r\n  );\r\n}\r\n\r\nfunction openGenericErrorDialog(\r\n  dialogService: EntDialogService,\r\n  translocoService: TranslocoService\r\n) {\r\n  const locale = getInitUserConfig().locale;\r\n\r\n  translocoService.load(locale).subscribe(() => {\r\n    dialogService.open({\r\n      type: EntDialogType.ERROR,\r\n      title: translocoService.translate(\"auth-error.dialog.common.title\", undefined, locale),\r\n      message: translocoService.translate(\"auth-error.dialog.unknown-error.message\", undefined, locale),\r\n    });\r\n  });\r\n}\r\n\r\nfunction openPopupsBlockedErrorDialog(\r\n  dialogService: EntDialogService,\r\n  translocoService: TranslocoService\r\n) {\r\n  const locale = getInitUserConfig().locale;\r\n\r\n  translocoService.load(locale).subscribe(() => {\r\n    dialogService\r\n      .open({\r\n        type: EntDialogType.ERRO","readmeFilename":"README.md"}