fix(widgets): hero title/subtitle/CTA now locale-aware
P0-3: hero widget text rendered hardcoded English on every locale (ru/hy included) because HeroWidgetData source props were plain strings with no per-locale variant, unlike nav labels which already support a locale map (NavigationLocalizedText). DataSourceResolverService.toHeroData() now accepts either a plain string (existing tenants unaffected) or a per-locale text map for title/subtitle/ctaLabel and each slide's fields, resolved against the active language the same way footer group titles already are (LocalizedTextContent, current lang -> en -> first available). Updated the mock bootstrap fixture's hero props to a real ru/en/hy map so the demo tenant shows translated copy instead of English on every locale. No editor UI change needed: the Widgets section's existing JSON-fallback editor already accepts arbitrary prop shapes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@ import { Injectable, inject } from '@angular/core';
|
|||||||
import { Observable, of, map, switchMap } from 'rxjs';
|
import { Observable, of, map, switchMap } from 'rxjs';
|
||||||
import { CategoryFacade } from '../../facades/platform/category.facade';
|
import { CategoryFacade } from '../../facades/platform/category.facade';
|
||||||
import { ProductFacade } from '../../facades/platform/product.facade';
|
import { ProductFacade } from '../../facades/platform/product.facade';
|
||||||
import { SectionConfig, WidgetConfig } from '../../shared/models/config';
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
import { LocalizedTextContent, SectionConfig, WidgetConfig } from '../../shared/models/config';
|
||||||
import { WidgetManifestEntry, WidgetDataSourceName } from '../contracts/widget-manifest.contract';
|
import { WidgetManifestEntry, WidgetDataSourceName } from '../contracts/widget-manifest.contract';
|
||||||
import { BannerWidgetData, CategoriesWidgetData, FooterWidgetData, HeroWidgetData, HtmlWidgetData, PartnersWidgetData, ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
import { BannerWidgetData, CategoriesWidgetData, FooterWidgetData, HeroWidgetData, HtmlWidgetData, PartnersWidgetData, ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
||||||
import { WidgetManifestService } from '../registry/widget-manifest.service';
|
import { WidgetManifestService } from '../registry/widget-manifest.service';
|
||||||
@@ -14,6 +15,7 @@ export class DataSourceResolverService {
|
|||||||
private readonly widgetManifest = inject(WidgetManifestService);
|
private readonly widgetManifest = inject(WidgetManifestService);
|
||||||
private readonly categoryFacade = inject(CategoryFacade);
|
private readonly categoryFacade = inject(CategoryFacade);
|
||||||
private readonly productFacade = inject(ProductFacade);
|
private readonly productFacade = inject(ProductFacade);
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
resolve(widget: WidgetConfig, section: SectionConfig): Observable<unknown> {
|
resolve(widget: WidgetConfig, section: SectionConfig): Observable<unknown> {
|
||||||
return this.widgetManifest.getWidget(widget.type).pipe(
|
return this.widgetManifest.getWidget(widget.type).pipe(
|
||||||
@@ -84,15 +86,32 @@ export class DataSourceResolverService {
|
|||||||
return typeof source === 'string' ? source as WidgetDataSourceName : undefined;
|
return typeof source === 'string' ? source as WidgetDataSourceName : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero text props accept either a plain string (existing tenants, unchanged) or a
|
||||||
|
* per-locale map (`{ ru: '...', en: '...' }`, same shape as NavigationLocalizedText)
|
||||||
|
* so the hero doesn't render in one hardcoded language on every locale.
|
||||||
|
*/
|
||||||
|
private resolveLocalizedWidgetText(value: unknown): string | undefined {
|
||||||
|
if (value == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
const lang = this.languageService.currentLanguage();
|
||||||
|
const text = value as LocalizedTextContent;
|
||||||
|
return text[lang] ?? text['en'] ?? Object.values(text)[0];
|
||||||
|
}
|
||||||
|
|
||||||
private toHeroData(section: SectionConfig, settings: Record<string, unknown>): HeroWidgetData {
|
private toHeroData(section: SectionConfig, settings: Record<string, unknown>): HeroWidgetData {
|
||||||
const rawSlides = settings['slides'];
|
const rawSlides = settings['slides'];
|
||||||
const slides = Array.isArray(rawSlides)
|
const slides = Array.isArray(rawSlides)
|
||||||
? rawSlides
|
? rawSlides
|
||||||
.filter((slide): slide is Record<string, unknown> => !!slide && typeof slide === 'object')
|
.filter((slide): slide is Record<string, unknown> => !!slide && typeof slide === 'object')
|
||||||
.map(slide => ({
|
.map(slide => ({
|
||||||
title: String(slide['title'] ?? ''),
|
title: this.resolveLocalizedWidgetText(slide['title']) ?? '',
|
||||||
subtitle: slide['subtitle'] != null ? String(slide['subtitle']) : undefined,
|
subtitle: this.resolveLocalizedWidgetText(slide['subtitle']),
|
||||||
ctaLabel: slide['ctaLabel'] != null ? String(slide['ctaLabel']) : undefined,
|
ctaLabel: this.resolveLocalizedWidgetText(slide['ctaLabel']),
|
||||||
}))
|
}))
|
||||||
.filter(slide => slide.title)
|
.filter(slide => slide.title)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -100,9 +119,9 @@ export class DataSourceResolverService {
|
|||||||
return {
|
return {
|
||||||
section,
|
section,
|
||||||
settings,
|
settings,
|
||||||
title: String(settings['title'] ?? ''),
|
title: this.resolveLocalizedWidgetText(settings['title']) ?? '',
|
||||||
subtitle: settings['subtitle'] != null ? String(settings['subtitle']) : undefined,
|
subtitle: this.resolveLocalizedWidgetText(settings['subtitle']),
|
||||||
ctaLabel: settings['ctaLabel'] != null ? String(settings['ctaLabel']) : undefined,
|
ctaLabel: this.resolveLocalizedWidgetText(settings['ctaLabel']),
|
||||||
slides,
|
slides,
|
||||||
autoplay: settings['autoplay'] === true,
|
autoplay: settings['autoplay'] === true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -398,9 +398,21 @@
|
|||||||
},
|
},
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"props": {
|
"props": {
|
||||||
"title": "Welcome to Marketplace Platform",
|
"title": {
|
||||||
"subtitle": "Configuration-driven multi-tenant commerce",
|
"ru": "Добро пожаловать на Маркетплейс",
|
||||||
"ctaLabel": "Start Shopping"
|
"en": "Welcome to Marketplace Platform",
|
||||||
|
"hy": "Բարի գալուստ Մարքեթփլեյս"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"ru": "Конфигурируемая мультитенантная торговая площадка",
|
||||||
|
"en": "Configuration-driven multi-tenant commerce",
|
||||||
|
"hy": "Կոնֆիգուրացիայով կառավարվող բազմատենանտ առևտրի հարթակ"
|
||||||
|
},
|
||||||
|
"ctaLabel": {
|
||||||
|
"ru": "Начать покупки",
|
||||||
|
"en": "Start Shopping",
|
||||||
|
"hy": "Սկսել գնումները"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -33,9 +33,21 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"props": {
|
"props": {
|
||||||
"title": "Welcome to Marketplace Platform",
|
"title": {
|
||||||
"subtitle": "Configuration-driven multi-tenant commerce",
|
"ru": "Добро пожаловать на Маркетплейс",
|
||||||
"ctaLabel": "Start Shopping"
|
"en": "Welcome to Marketplace Platform",
|
||||||
|
"hy": "Բարի գալուստ Մարքեթփլեյս"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"ru": "Конфигурируемая мультитенантная торговая площадка",
|
||||||
|
"en": "Configuration-driven multi-tenant commerce",
|
||||||
|
"hy": "Կոնֆիգուրացիայով կառավարվող բազմատենանտ առևտրի հարթակ"
|
||||||
|
},
|
||||||
|
"ctaLabel": {
|
||||||
|
"ru": "Начать покупки",
|
||||||
|
"en": "Start Shopping",
|
||||||
|
"hy": "Սկսել գնումները"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user