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:
sdarbinyan
2026-07-19 22:44:45 +04:00
parent 1a5d43523a
commit 5b063d5462
3 changed files with 56 additions and 13 deletions

View File

@@ -2,7 +2,8 @@ import { Injectable, inject } from '@angular/core';
import { Observable, of, map, switchMap } from 'rxjs';
import { CategoryFacade } from '../../facades/platform/category.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 { BannerWidgetData, CategoriesWidgetData, FooterWidgetData, HeroWidgetData, HtmlWidgetData, PartnersWidgetData, ProductCollectionWidgetData } from '../contracts/widget-data.contract';
import { WidgetManifestService } from '../registry/widget-manifest.service';
@@ -14,6 +15,7 @@ export class DataSourceResolverService {
private readonly widgetManifest = inject(WidgetManifestService);
private readonly categoryFacade = inject(CategoryFacade);
private readonly productFacade = inject(ProductFacade);
private readonly languageService = inject(LanguageService);
resolve(widget: WidgetConfig, section: SectionConfig): Observable<unknown> {
return this.widgetManifest.getWidget(widget.type).pipe(
@@ -84,15 +86,32 @@ export class DataSourceResolverService {
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 {
const rawSlides = settings['slides'];
const slides = Array.isArray(rawSlides)
? rawSlides
.filter((slide): slide is Record<string, unknown> => !!slide && typeof slide === 'object')
.map(slide => ({
title: String(slide['title'] ?? ''),
subtitle: slide['subtitle'] != null ? String(slide['subtitle']) : undefined,
ctaLabel: slide['ctaLabel'] != null ? String(slide['ctaLabel']) : undefined,
title: this.resolveLocalizedWidgetText(slide['title']) ?? '',
subtitle: this.resolveLocalizedWidgetText(slide['subtitle']),
ctaLabel: this.resolveLocalizedWidgetText(slide['ctaLabel']),
}))
.filter(slide => slide.title)
: undefined;
@@ -100,9 +119,9 @@ export class DataSourceResolverService {
return {
section,
settings,
title: String(settings['title'] ?? ''),
subtitle: settings['subtitle'] != null ? String(settings['subtitle']) : undefined,
ctaLabel: settings['ctaLabel'] != null ? String(settings['ctaLabel']) : undefined,
title: this.resolveLocalizedWidgetText(settings['title']) ?? '',
subtitle: this.resolveLocalizedWidgetText(settings['subtitle']),
ctaLabel: this.resolveLocalizedWidgetText(settings['ctaLabel']),
slides,
autoplay: settings['autoplay'] === true,
};

View File

@@ -398,9 +398,21 @@
},
"visible": true,
"props": {
"title": "Welcome to Marketplace Platform",
"subtitle": "Configuration-driven multi-tenant commerce",
"ctaLabel": "Start Shopping"
"title": {
"ru": "Добро пожаловать на Маркетплейс",
"en": "Welcome to Marketplace Platform",
"hy": "Բարի գալուստ Մարքեթփլեյս"
},
"subtitle": {
"ru": "Конфигурируемая мультитенантная торговая площадка",
"en": "Configuration-driven multi-tenant commerce",
"hy": "Կոնֆիգուրացիայով կառավարվող բազմատենանտ առևտրի հարթակ"
},
"ctaLabel": {
"ru": "Начать покупки",
"en": "Start Shopping",
"hy": "Սկսել գնումները"
}
}
}
]

View File

@@ -33,9 +33,21 @@
"version": "1.0.0",
"visible": true,
"props": {
"title": "Welcome to Marketplace Platform",
"subtitle": "Configuration-driven multi-tenant commerce",
"ctaLabel": "Start Shopping"
"title": {
"ru": "Добро пожаловать на Маркетплейс",
"en": "Welcome to Marketplace Platform",
"hy": "Բարի գալուստ Մարքեթփլեյս"
},
"subtitle": {
"ru": "Конфигурируемая мультитенантная торговая площадка",
"en": "Configuration-driven multi-tenant commerce",
"hy": "Կոնֆիգուրացիայով կառավարվող բազմատենանտ առևտրի հարթակ"
},
"ctaLabel": {
"ru": "Начать покупки",
"en": "Start Shopping",
"hy": "Սկսել գնումները"
}
}
}
]