From 910b574c5c53406e400cc002f18ae950c1e7fa8b Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Mon, 22 Jun 2026 13:42:31 +0200 Subject: [PATCH 1/3] feat: add language selector to header and detect browser default language - Add LocaleService as the single source of truth for language management: reads/writes localStorage, detects browser language on first visit (falls back to 'en' if unsupported) - Add language selector dropdown in the header (desktop + mobile) --- src/app/app.component.ts | 73 ++++++++---------- src/app/app.module.ts | 16 ++-- src/app/services/locale.service.ts | 53 +++++++++++++ .../services/theme-aware-translate.loader.ts | 74 +++++++++---------- src/app/shared/header/header.component.html | 41 ++++++++++ src/app/shared/header/header.component.ts | 24 +++--- 6 files changed, 175 insertions(+), 106 deletions(-) create mode 100644 src/app/services/locale.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 90af9824..bca14cfb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,50 +1,37 @@ -import {Component, OnInit} from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; import { initFlowbite } from 'flowbite'; -import { TranslateService } from '@ngx-translate/core'; -import {LocalStorageService} from "./services/local-storage.service"; -import {Category} from "./models/interfaces"; -import {EventMessageService} from "./services/event-message.service"; -import { ActivatedRoute, NavigationEnd } from '@angular/router'; -import { Router } from '@angular/router'; +import * as moment from 'moment'; +import { filter } from 'rxjs'; import { LoginInfo } from 'src/app/models/interfaces'; -import { ApiServiceService } from 'src/app/services/product-service.service'; import { RefreshLoginServiceService } from "src/app/services/refresh-login-service.service"; -import * as moment from 'moment'; -import {ThemeService} from "./services/theme.service"; -import {environment} from "../environments/environment"; -import { filter } from 'rxjs'; +import { environment } from "../environments/environment"; +import { EventMessageService } from "./services/event-message.service"; +import { LocalStorageService } from "./services/local-storage.service"; +import { LocaleService } from './services/locale.service'; +import { ThemeService } from "./services/theme.service"; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], + styleUrls: ['./app.component.css'], }) export class AppComponent implements OnInit { title = 'BAE Marketplace'; showPanel = false; - providerThemeName=environment.providerThemeName; - isProduction:boolean = environment.isProduction; + providerThemeName = environment.providerThemeName; + isProduction: boolean = environment.isProduction; showHeaderAndFooter = false; - constructor(private translate: TranslateService, - private localStorage: LocalStorageService, - private eventMessage: EventMessageService, - private route: ActivatedRoute, - private router: Router, - private api: ApiServiceService, - private themeService: ThemeService, - private refreshApi: RefreshLoginServiceService) { - this.translate.addLangs(['en', 'es']); - this.translate.setDefaultLang('es'); - let currLang = this.localStorage.getItem('current_language') - if(!currLang || currLang == null) { - this.localStorage.setItem('current_language', ''); - this.translate.use('es'); - } else { - this.translate.use(currLang); - } - if(!this.localStorage.getObject('selected_categories')) + constructor(private localeService: LocaleService, + private localStorage: LocalStorageService, + private eventMessage: EventMessageService, + private router: Router, + private themeService: ThemeService, + private refreshApi: RefreshLoginServiceService) { + this.localeService.init().subscribe(); + if (!this.localStorage.getObject('selected_categories')) this.localStorage.setObject('selected_categories', []); /*this.eventMessage.messages$.subscribe(ev => { @@ -60,34 +47,34 @@ export class AppComponent implements OnInit { this.themeService.initializeProviderTheme(providerThemeName); initFlowbite(); - if(!this.localStorage.getObject('selected_categories')) + if (!this.localStorage.getObject('selected_categories')) this.localStorage.setObject('selected_categories', []); - if(!this.localStorage.getObject('cart_items')) + if (!this.localStorage.getObject('cart_items')) this.localStorage.setObject('cart_items', []); - if(!this.localStorage.getObject('login_items')) + if (!this.localStorage.getObject('login_items')) this.localStorage.setObject('login_items', {}); - if(!this.localStorage.getObject('feedback')) + if (!this.localStorage.getObject('feedback')) this.localStorage.setObject('feedback', {}); //this.checkPanel(); this.eventMessage.messages$.subscribe(ev => { - if(ev.type === 'LoginProcess') { + if (ev.type === 'LoginProcess') { this.refreshApi.stopInterval(); let info = ev.value as LoginInfo; - this.refreshApi.startInterval(((info.expire - moment().unix())-4)*1000, ev); + this.refreshApi.startInterval(((info.expire - moment().unix()) - 4) * 1000, ev); initFlowbite(); //this.refreshApi.startInterval(3000, ev.value); } }) let aux = this.localStorage.getObject('login_items') as LoginInfo; - if(JSON.stringify(aux) === '{}'){ + if (JSON.stringify(aux) === '{}') { //this.siopInfo.getSiopInfo().subscribe((data)=>{ // environment.SIOP_INFO = data //}) } - else if (((aux.expire - moment().unix())-4) > 0) { + else if (((aux.expire - moment().unix()) - 4) > 0) { this.refreshApi.stopInterval(); - this.refreshApi.startInterval(((aux.expire - moment().unix())-4)*1000, aux); + this.refreshApi.startInterval(((aux.expire - moment().unix()) - 4) * 1000, aux); initFlowbite(); } this.router.events @@ -112,5 +99,5 @@ export class AppComponent implements OnInit { } }*/ - + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d660990c..f43e834d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,7 +6,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { PickerComponent } from '@ctrl/ngx-emoji-mart'; import { FaIconComponent, FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { NgxFileDropModule } from 'ngx-file-drop'; import { NotificationComponent } from './shared/notification/notification.component'; import { MarkdownModule } from 'ngx-markdown'; @@ -71,6 +70,11 @@ import { UserProfileComponent } from "./pages/user-profile/user-profile.componen import { AppInitService } from './services/app-init.service'; import { ThemeAwareTranslateLoader } from './services/theme-aware-translate.loader'; import { ThemeService } from './services/theme.service'; + +export function createThemeAwareLoader(http: HttpClient, themeService: ThemeService) { + return new ThemeAwareTranslateLoader(http, themeService); +} + import { BadgeComponent } from "./shared/badge/badge.component"; import { BillingAccountFormComponent } from "./shared/billing-account-form/billing-account-form.component"; import { CardComponent } from "./shared/card/card.component"; @@ -86,10 +90,6 @@ import { PricePlanDrawerComponent } from "./shared/price-plan-drawer/price-plan- import { RevenueReportComponent } from './shared/revenue-report/revenue-report.component'; import { SharedModule } from "./shared/shared.module"; -// Función Factory requerida para crear el cargador con sus dependencias -export function createThemeAwareLoader(http: HttpClient, themeService: ThemeService) { - return new ThemeAwareTranslateLoader(http, themeService); -} import { QuotesModule } from "src/app/features/quotes/quotes.module"; import { AboutDomeComponent } from "src/app/pages/about-dome/about-dome.component"; @@ -179,10 +179,9 @@ import { RequestValidationModalComponent } from './pages/seller-offerings/offeri QuotesModule, MarkdownModule.forRoot(), TranslateModule.forRoot({ - defaultLanguage: 'en', loader: { provide: TranslateLoader, - useFactory: (createThemeAwareLoader), + useFactory: createThemeAwareLoader, deps: [HttpClient, ThemeService] } }), @@ -222,6 +221,3 @@ import { RequestValidationModalComponent } from './pages/seller-offerings/offeri }) export class AppModule { } -export function HttpLoaderFactory(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/i18n/'); -} diff --git a/src/app/services/locale.service.ts b/src/app/services/locale.service.ts new file mode 100644 index 00000000..c1ebacf7 --- /dev/null +++ b/src/app/services/locale.service.ts @@ -0,0 +1,53 @@ +import { DOCUMENT } from '@angular/common'; +import { Injectable, inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { LocalStorageService } from './local-storage.service'; + +const LANG_KEY = 'current_language'; +const DEFAULT_LANG = 'en'; +const AVAILABLE_LANGS = ['en', 'es']; + +@Injectable({ + providedIn: 'root' +}) +export class LocaleService { + + private readonly translate = inject(TranslateService); + private readonly localStorage = inject(LocalStorageService); + private readonly document = inject(DOCUMENT); + + private lang$ = new BehaviorSubject(DEFAULT_LANG); + readonly currentLang$: Observable = this.lang$.asObservable(); + + init(): Observable { + this.translate.addLangs(AVAILABLE_LANGS); + this.translate.setDefaultLang(DEFAULT_LANG); + + this.translate.onLangChange.subscribe(({ lang }) => { + this.document.documentElement.lang = lang; + this.lang$.next(lang); + }); + + const stored = this.localStorage.getItem(LANG_KEY); + const browserLang = this.translate.getBrowserLang() ?? DEFAULT_LANG; + const lang = stored && stored.length > 0 + ? stored + : AVAILABLE_LANGS.includes(browserLang) ? browserLang : DEFAULT_LANG; + + return this.translate.use(lang); + } + + get currentLang(): string { + return this.lang$.value; + } + + get availableLangs(): string[] { + return [...AVAILABLE_LANGS]; + } + + setLanguage(lang: string): void { + this.localStorage.setItem(LANG_KEY, lang); + this.translate.use(lang); + } +} diff --git a/src/app/services/theme-aware-translate.loader.ts b/src/app/services/theme-aware-translate.loader.ts index 474ecb02..328a36bd 100644 --- a/src/app/services/theme-aware-translate.loader.ts +++ b/src/app/services/theme-aware-translate.loader.ts @@ -1,64 +1,56 @@ import { HttpClient } from '@angular/common/http'; import { TranslateLoader } from '@ngx-translate/core'; -import { forkJoin, of } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; -import { ThemeService } from './theme.service'; // Asegúrate que la ruta es correcta +import { Observable, forkJoin, of } from 'rxjs'; +import { catchError, filter, map, shareReplay, switchMap, take } from 'rxjs/operators'; +import { ThemeService } from './theme.service'; export class ThemeAwareTranslateLoader implements TranslateLoader { + private cache = new Map>(); + constructor( private http: HttpClient, private themeService: ThemeService ) {} - public getTranslation(lang: string): any { - const currentThemeName = this.themeService.getCurrentThemeConfig()?.name; - const themeJsonPath = `assets/i18n/themes/${lang}-${currentThemeName}.json`; - const commonJsonPath = `assets/i18n/${lang}.json`; + public getTranslation(lang: string): Observable { + return this.themeService.currentTheme$.pipe( + filter(theme => theme !== null), + take(1), + switchMap(theme => { + const themeName = theme?.name; + const cacheKey = themeName ? `${lang}-${themeName}` : lang; + + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + const common$ = this.http.get(`assets/i18n/${lang}.json`); + const translation$ = themeName + ? forkJoin([ + common$, + this.http.get(`assets/i18n/themes/${lang}-${themeName}.json`).pipe(catchError(() => of({}))) + ]).pipe(map(([common, theme]) => this.deepMerge(common, theme))) + : common$; - // forkJoin nos permite obtener ambas peticiones (base y tema) - return forkJoin([ - this.http.get(commonJsonPath), // Petición para el JSON base (obligatoria) - this.http.get(themeJsonPath).pipe( // Petición para el JSON del tema (opcional) - catchError(() => { - // Si el archivo del tema no existe (ej. tema BAE no tiene es-BAE.json), - // devolvemos un observable con un objeto vacío para no romper la fusión. - return of({}); - }) - ) - ]).pipe( - map(([commonTranslations, themeTranslations]) => { - // Fusionamos los objetos. Las propiedades de themeTranslations - // sobrescribirán las de commonTranslations si tienen la misma clave. - // Usamos una fusión profunda (deep merge) para que no reemplace objetos enteros como "DASHBOARD". - return this.deepMerge(commonTranslations, themeTranslations); + const cached$ = translation$.pipe(shareReplay(1)); + this.cache.set(cacheKey, cached$); + return cached$; }) ); } - /** - * Realiza una fusión profunda de dos objetos. - */ private deepMerge(target: any, source: any): any { const output = { ...target }; - - if (this.isObject(target) && this.isObject(source)) { - Object.keys(source).forEach(key => { - if (this.isObject(source[key])) { - if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); - } else { - output[key] = this.deepMerge(target[key], source[key]); - } - } else { - Object.assign(output, { [key]: source[key] }); - } - }); + for (const key of Object.keys(source)) { + output[key] = this.isObject(source[key]) && this.isObject(target?.[key]) + ? this.deepMerge(target[key], source[key]) + : source[key]; } return output; } - private isObject(item: any): boolean { - return (item && typeof item === 'object' && !Array.isArray(item)); + private isObject(value: any): boolean { + return value !== null && typeof value === 'object' && !Array.isArray(value); } } diff --git a/src/app/shared/header/header.component.html b/src/app/shared/header/header.component.html index a94a1ec8..1ee99243 100644 --- a/src/app/shared/header/header.component.html +++ b/src/app/shared/header/header.component.html @@ -270,6 +270,47 @@ + + @if (!mobile) { +
+ + @if (flagDropdownOpen) { + + } +
+ } @else { +
+ @for (lang of langs; track lang) { + @if (lang === defaultLang) { + + } @else { + + } + } +
+ } + @if (!is_logged) { @if (providerThemeName === 'DOME') { @if (mobile) { diff --git a/src/app/shared/header/header.component.ts b/src/app/shared/header/header.component.ts index 1492a279..4823c34c 100644 --- a/src/app/shared/header/header.component.ts +++ b/src/app/shared/header/header.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, ChangeDetectorRef, Component, DoCheck, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { faAddressCard, faAnglesLeft, @@ -18,17 +18,16 @@ import { faUser, faUsers } from '@fortawesome/sharp-solid-svg-icons'; -import { TranslateService } from '@ngx-translate/core'; import { initFlowbite } from 'flowbite'; import * as moment from 'moment'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { LocaleService } from 'src/app/services/locale.service'; import { environment } from 'src/environments/environment'; import * as uuid from 'uuid'; import { LoginInfo } from 'src/app/models/interfaces'; import { LoginServiceService } from 'src/app/services/login-service.service'; -import { ApiServiceService } from 'src/app/services/product-service.service'; import { QrVerifierService } from 'src/app/services/qr-verifier.service'; import { EventMessageService } from '../../services/event-message.service'; import { LocalStorageService } from '../../services/local-storage.service'; @@ -45,12 +44,10 @@ export class HeaderComponent implements OnInit, AfterViewInit, DoCheck, OnDestro @ViewChild('navbarbutton') navbarbutton?: ElementRef; constructor( - private translate: TranslateService, + private localeService: LocaleService, private localStorage: LocalStorageService, - private api: ApiServiceService, private loginService: LoginServiceService, private cdr: ChangeDetectorRef, - private route: ActivatedRoute, private eventMessage: EventMessageService, private router: Router, private qrVerifier: QrVerifierService, @@ -124,6 +121,9 @@ export class HeaderComponent implements OnInit, AfterViewInit, DoCheck, OnDestro if (this.isNavBarOpen) { this.isNavBarOpen = false; } + if (this.flagDropdownOpen) { + this.flagDropdownOpen = false; + } } @HostListener('window:resize') @@ -142,9 +142,11 @@ export class HeaderComponent implements OnInit, AfterViewInit, DoCheck, OnDestro } ngOnInit(): void { - this.langs = this.translate.getLangs(); - const currLang = this.localStorage.getItem('current_language'); - this.defaultLang = currLang ?? this.translate.getDefaultLang(); + this.langs = this.localeService.availableLangs; + this.defaultLang = this.localeService.currentLang; + this.localeService.currentLang$.pipe(takeUntil(this.destroy$)).subscribe(lang => { + this.defaultLang = lang; + }); this.themeSubscription = this.themeService.currentTheme$.subscribe(theme => { this.currentTheme = theme; @@ -284,9 +286,7 @@ export class HeaderComponent implements OnInit, AfterViewInit, DoCheck, OnDestro } switchLanguage(language: string) { - this.translate.use(language); - this.localStorage.setItem('current_language', language); - this.defaultLang = language; + this.localeService.setLanguage(language); } async logout() { From b879e3b0475e630e886a3be4b9d59c29418033f4 Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Mon, 22 Jun 2026 15:51:59 +0200 Subject: [PATCH 2/3] update spanish translation --- .../product-details.component.html | 769 +-- src/app/pages/search/search.component.html | 589 ++- .../create-tender-modal.component.ts | 24 +- src/assets/i18n/en.json | 4 +- src/assets/i18n/es.json | 4650 ++++++++--------- src/assets/i18n/themes/es-BAE.json | 14 +- 6 files changed, 3074 insertions(+), 2976 deletions(-) diff --git a/src/app/pages/product-details/product-details.component.html b/src/app/pages/product-details/product-details.component.html index 8b9fa4ae..90eff2ef 100644 --- a/src/app/pages/product-details/product-details.component.html +++ b/src/app/pages/product-details/product-details.component.html @@ -1,10 +1,11 @@ -
@@ -93,33 +101,45 @@
@if(check_logged){ - @if (!isCustom()) { - - } @else if(quotesEnabled) { - - } - } - - @if(check_logged) { @if (!isCustom()) { - + } @else if(quotesEnabled) { - + + } } + + @if(check_logged) { + @if (!isCustom()) { + + } @else if(quotesEnabled) { + + } } @@ -132,8 +152,10 @@
- - + +
@@ -143,19 +165,24 @@
- - + +
Last update - {{productOff?.lastUpdate | date:'dd/MM/yy, HH:mm'}} + {{productOff?.lastUpdate | date:'dd/MM/yy, + HH:mm'}}
- - + +
@@ -166,8 +193,10 @@ @if(prodSpec.productNumber){
- - + +
@@ -179,8 +208,10 @@ @if(prodSpec.brand){
- - + +
@@ -192,13 +223,16 @@ @if(orgInfo!=undefined){ } @@ -216,51 +250,52 @@

How does it work?

} @if(serviceSpecs.length > 0){ -
-

{{ 'PRODUCT_DETAILS._service_spec' | translate }}

- @for(service of serviceSpecs; track service.id){ -
- {{service.name}}: - -
- } +
+

{{ 'PRODUCT_DETAILS._service_spec' | translate }}

+ @for(service of serviceSpecs; track service.id){ +
+ {{service.name}}: +
+ } +
} @if(resourceSpecs.length > 0){ -
-

{{ 'PRODUCT_DETAILS._resource_spec' | translate }}

- @for(resource of resourceSpecs; track resource.id){ -
- {{resource.name}}: - -
- } +
+

{{ 'PRODUCT_DETAILS._resource_spec' | translate }}

+ @for(resource of resourceSpecs; track resource.id){ +
+ {{resource.name}}: +
+ } +
} - @if(prodSpec.productSpecificationRelationship != undefined && prodSpec.productSpecificationRelationship.length>0){ -

{{ 'PRODUCT_DETAILS._product_rels' | translate }}

-
- @for (rel of prodSpec.productSpecificationRelationship; track rel.id) { -
-

{{rel.name}}

-
-

{{rel.relationshipType}}

- @if(rel.relationshipType == 'dependency'){ - - } @else if(rel.relationshipType == 'migration'){ - - } @else if(rel.relationshipType == 'exclusivity'){ - - } @else if(rel.relationshipType == 'substitution'){ - - } -
-
- } + @if(prodSpec.productSpecificationRelationship != undefined && + prodSpec.productSpecificationRelationship.length>0){ +

{{ 'PRODUCT_DETAILS._product_rels' | translate }}

+
+ @for (rel of prodSpec.productSpecificationRelationship; track rel.id) { +
+

{{rel.name}}

+
+

{{rel.relationshipType}}

+ @if(rel.relationshipType == 'dependency'){ + + } @else if(rel.relationshipType == 'migration'){ + + } @else if(rel.relationshipType == 'exclusivity'){ + + } @else if(rel.relationshipType == 'substitution'){ + + } +
+ } +
}
@@ -268,240 +303,256 @@

{{ 'PRODUCT_DETAILS._produ @if (activeTab === 'features') { @if(prodSpec.productSpecCharacteristic != undefined && prodChars.length>0) { -
-

{{ 'PRODUCT_DETAILS._product_chars' | translate }}

-
- @for (char of prodChars; track char.id; let idx = $index) { -
-

{{char.name}}

- @if (char.description) { - - } - @if (isOptionalCharacteristic(char)) { - Optional - } -
- @if (isBooleanCharacteristic(char)) { -
- Default - - {{ getBooleanDefaultValue(char) ? 'Enabled' : 'Disabled' }} - -
- } @else { - @for (val of char?.productSpecCharacteristicValue; track val.value) { -
-
- @if(val?.isDefault) { - - - - } -
- @if(val.value !== undefined && val.value !== null){ - {{ getCharacteristicValuePreview(val) }} - } @else { - {{ getCharacteristicRangePreview(val) }} - } -
- } +
+

{{ 'PRODUCT_DETAILS._product_chars' | translate }}

+
+ @for (char of prodChars; track char.id; let idx = $index) { +
+

{{char.name}}

+ @if (char.description) { + + } + @if (isOptionalCharacteristic(char)) { + Optional + } +
+ @if (isBooleanCharacteristic(char)) { +
+ Default + + {{ getBooleanDefaultValue(char) ? 'Enabled' : 'Disabled' }} + +
+ } @else { + @for (val of char?.productSpecCharacteristicValue; track val.value) { +
+
+ @if(val?.isDefault) { + + + }
+ @if(val.value !== undefined && val.value !== null){ + {{ + getCharacteristicValuePreview(val) }} + } @else { + {{ + getCharacteristicRangePreview(val) }} + }
- } @empty { -
- {{ 'PRODUCT_DETAILS._no_chars' | translate }} -
- } + } + } +
+
+ } @empty { +
+ {{ 'PRODUCT_DETAILS._no_chars' | translate }}
+ }
+
} @else { -
- No features available for this product. -
+
+ No features available for this product. +
} } @if (activeTab === 'pricing') { -

{{ 'PRODUCT_DETAILS._product_pricing' | translate }}

- @if(productOff?.productOfferingPrice != undefined){ - @if(checkCustom){ -
- @for (price of productOff?.productOfferingPrice; track price.id) { - @if (price.priceType == 'custom') { -
-

{{price.name}}

- -
- } - } - @if(productOff?.productOfferingPrice?.length==0){ -
-

{{ 'SHOPPING_CART._free' | translate }}

-

{{ 'SHOPPING_CART._free_desc' | translate }}

-
- } -
- } @else { -
- @for (price of productOff?.productOfferingPrice; track price.id) { -
-
-

{{price.name}}

-
-
- -
-
- } - @if(productOff?.productOfferingPrice?.length==0){ -
-
-

{{ 'SHOPPING_CART._free' | translate }}

-
-

{{ 'SHOPPING_CART._free_desc' | translate }}

-
- } -
- } +

{{ 'PRODUCT_DETAILS._product_pricing' | translate }}

+ @if(productOff?.productOfferingPrice != undefined){ + @if(checkCustom){ +
+ @for (price of productOff?.productOfferingPrice; track price.id) { + @if (price.priceType == 'custom') { +
+

{{price.name}}

+ +
} - - @if(usageMetrics.length > 0){ -

Usage metrics

-
- @for (metric of usageMetrics; track metric.id) { -
-

{{metric.name}}

- -
- } + } + @if(productOff?.productOfferingPrice?.length==0){ +
+

{{ 'SHOPPING_CART._free' | translate }}

+

{{ 'SHOPPING_CART._free_desc' | translate }}

+
+ } +
+ } @else { +
+ @for (price of productOff?.productOfferingPrice; track price.id) { +
+
+

{{price.name}}

+
+
+
+
+ } + @if(productOff?.productOfferingPrice?.length==0){ +
+
+

{{ 'SHOPPING_CART._free' | translate }}

+
+

{{ 'SHOPPING_CART._free_desc' | translate }}

+
} +
+ } + } + + @if(usageMetrics.length > 0){ +

Usage metrics

+
+ @for (metric of usageMetrics; track metric.id) { +
+

{{metric.name}}

+ +
+ } +
+ } } @if (activeTab === 'compliance') { -
-

Certification

-
-
-
- - - -
-
- @if(complianceLevel=='NL'){ -

No level

- }@else if(complianceLevel=='BL'){ -

Baseline

- } @else if(complianceLevel=='P') { -

Professional

- } @else { -

Professional+

- } -

{{ complianceDescription }}

-
+
+

Certification

+
+
+
+ + + +
+
+ @if(complianceLevel=='NL'){ +

No level

+ }@else if(complianceLevel=='BL'){ +

Baseline

+ } @else if(complianceLevel=='P') { +

Professional

+ } @else { +

Professional+

+ } +

{{ complianceDescription }}

- @if (selfAtt) { - - {{ 'PRODUCT_DETAILS._self_attestation' | translate }} - - }
+ @if (selfAtt) { + + {{ 'PRODUCT_DETAILS._self_attestation' | translate }} + + }
+
- @if(complianceProf.length>0){ -
-

Key Capabilities

-
- @for (char of complianceProf; track char.id) { -
-
- -
-

{{char.name}}

-
- } + @if(complianceProf.length>0){ +
+

Key Capabilities

+
+ @for (char of complianceProf; track char.id) { +
+
+ +
+

{{char.name}}

+ }
- } +
+ } - @if(additionalCerts.length>0){ -
-

Additional certifications

-
- @for (char of additionalCerts; track char.id) { -
-
- -
-

{{normalizeName(char.name)}}

-
- } + @if(additionalCerts.length>0){ +
+

Additional certifications

+
+ @for (char of additionalCerts; track char.id) { +
+
+ +
+

{{normalizeName(char.name)}}

+ }
- } +
+ } - @if(licenseTerm || productOff?.serviceLevelAgreement){ - @if(licenseTerm && licenseTerm?.description != ''){ -

{{ 'PRODUCT_DETAILS._license' | translate }}

-
-
- -
-
- {{ 'PRODUCT_DETAILS._license' | translate }}

+
+
+ +
+
+ -
- @if(showReadMoreButton){ - - } -
- } - @if(productOff?.serviceLevelAgreement){ -

{{ 'PRODUCT_DETAILS._sla' | translate }}

-

{{productOff?.serviceLevelAgreement?.name}}.

- } + }" [data]="licenseTerm?.description" class="text-[16px] text-[#526179] break-words"> +
+ @if(showReadMoreButton){ + } +
+ } + @if(productOff?.serviceLevelAgreement){ +

{{ 'PRODUCT_DETAILS._sla' | translate }}

+

{{productOff?.serviceLevelAgreement?.name}}.

+ } + } - @if(attatchments.length>0){ -

{{ 'PRODUCT_DETAILS._product_att' | translate }}

-
- @for (att of attatchments; track att.id) { -
-

{{att.name}}

- -
- } -
+ @if(attatchments.length>0){ +

{{ 'PRODUCT_DETAILS._product_att' | translate }}

+
+ @for (att of attatchments; track att.id) { +
+

{{att.name}}

+ +
} +
+ } } @if (!check_logged) { -
-

Ready to get started?

-

Join thousands of companies using {{productOff?.name}}

- - Join DOME Marketplace - -
+
+

Ready to get started?

+

Join thousands of companies using {{productOff?.name}}

+ + Join DOME Marketplace + +
}
@@ -510,79 +561,85 @@

Ready to get started?

@if (toastVisibility) { -