fix: i18n gaps in popular searches and compare table
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

- SearchFacade.popularSearches hardcoded English titles regardless of
  active locale. Converted to a getter using translate.t() for the
  displayed title/text; the underlying search query stays the stable
  English canonical term the backend index matches against.
- Compare table and compare page rendered product.name raw instead of
  through getTranslatedField(), same pattern used everywhere else
  product titles are shown (catalog, product detail).
- SearchTrendingService.loadTrending() is a genuine backend gap (no
  trending-search endpoint exists) - already degrades gracefully,
  documented as a gap in BACKEND-API-REFERENCE.md \u00a712.6 rather than
  faked client-side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 10:04:29 +04:00
parent 6cc5d43a10
commit 178b5f0dc7
10 changed files with 83 additions and 36 deletions

View File

@@ -522,3 +522,13 @@ Separately, `createCartPayment()` (payment-gateway charge creation) still sends
{ "telegramUserId": "8823771" } { "telegramUserId": "8823771" }
``` ```
`telegramUserId` may be `null` for a non-Telegram web session - decide whether to also accept an email address as an alternative identifier (the frontend has no email capture on this flow today, so that would need a small frontend addition too). Once this ships, the frontend's localStorage fallback becomes purely a resilience path rather than the common case, and could optionally sync any locally-queued subscriptions on next successful call. `telegramUserId` may be `null` for a non-Telegram web session - decide whether to also accept an email address as an alternative identifier (the frontend has no email capture on this flow today, so that would need a small frontend addition too). Once this ships, the frontend's localStorage fallback becomes purely a resilience path rather than the common case, and could optionally sync any locally-queued subscriptions on next successful call.
### 12.6 Trending search terms
**Gap:** `SearchTrendingService.loadTrending()` is a stub returning `of(null)` - no trending-searches endpoint exists. It already degrades gracefully (UI hides the trending section rather than showing an error), so this is purely a missing-feature gap, not a bug.
**Ask:** an endpoint returning the top N search queries over some recent window, e.g.:
```json
{ "trending": [{ "query": "wireless earbuds", "count": 214 }, { "query": "winter jacket", "count": 187 }] }
```
Once it exists, wire `loadTrending()` to it and map `query` -> `SearchSuggestion.title/text`.

View File

@@ -55,40 +55,47 @@ export class SearchFacade {
readonly state = this.store.state; readonly state = this.store.state;
readonly popularSearches: SearchSuggestion[] = [ /**
* Search query text stays the stable English canonical term (what the
* backend search index matches against); only the displayed title/text
* are translated.
*/
get popularSearches(): SearchSuggestion[] {
return [
{ {
id: 'popular-smartphones', id: 'popular-smartphones',
type: 'collection', type: 'collection',
title: 'Smartphones', title: this.translate.t('search.popularSmartphones'),
text: 'Smartphones', text: this.translate.t('search.popularSmartphones'),
icon: 'trendingUp', icon: 'trendingUp',
target: { route: '/search', query: { q: 'Smartphones' } } target: { route: '/search', query: { q: 'Smartphones' } }
}, },
{ {
id: 'popular-sneakers', id: 'popular-sneakers',
type: 'collection', type: 'collection',
title: 'Sneakers', title: this.translate.t('search.popularSneakers'),
text: 'Sneakers', text: this.translate.t('search.popularSneakers'),
icon: 'trendingUp', icon: 'trendingUp',
target: { route: '/search', query: { q: 'Sneakers' } } target: { route: '/search', query: { q: 'Sneakers' } }
}, },
{ {
id: 'popular-headphones', id: 'popular-headphones',
type: 'collection', type: 'collection',
title: 'Headphones', title: this.translate.t('search.popularHeadphones'),
text: 'Headphones', text: this.translate.t('search.popularHeadphones'),
icon: 'trendingUp', icon: 'trendingUp',
target: { route: '/search', query: { q: 'Headphones' } } target: { route: '/search', query: { q: 'Headphones' } }
}, },
{ {
id: 'popular-laptops', id: 'popular-laptops',
type: 'collection', type: 'collection',
title: 'Laptops', title: this.translate.t('search.popularLaptops'),
text: 'Laptops', text: this.translate.t('search.popularLaptops'),
icon: 'trendingUp', icon: 'trendingUp',
target: { route: '/search', query: { q: 'Laptops' } } target: { route: '/search', query: { q: 'Laptops' } }
}, },
]; ];
}
constructor() { constructor() {
const history = this.historyService.getSnapshot(); const history = this.historyService.getSnapshot();

View File

@@ -6,7 +6,7 @@
<th scope="col">{{ 'ux.compareAttribute' | translate }}</th> <th scope="col">{{ 'ux.compareAttribute' | translate }}</th>
@for (product of products; track product.itemID) { @for (product of products; track product.itemID) {
<th scope="col"> <th scope="col">
<div class="compare-product-title">{{ product.name }}</div> <div class="compare-product-title">{{ productTitle(product) }}</div>
</th> </th>
} }
</tr> </tr>

View File

@@ -2,6 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, computed, inject } from '@an
import { Product } from '../../../../../core/products/models/product-domain.model'; import { Product } from '../../../../../core/products/models/product-domain.model';
import { TranslateService } from '../../../../../i18n/translate.service'; import { TranslateService } from '../../../../../i18n/translate.service';
import { TranslatePipe } from '../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { LanguageService } from '../../../../../services/language.service';
import { getTranslatedField } from '../../../../../utils/item.utils';
interface CompareRow { interface CompareRow {
key: string; key: string;
@@ -27,6 +29,7 @@ const STOCK_LABEL_KEYS: Record<string, string> = {
}) })
export class CompareTableComponent { export class CompareTableComponent {
private readonly i18n = inject(TranslateService); private readonly i18n = inject(TranslateService);
private readonly languageService = inject(LanguageService);
@Input() products: Product[] = []; @Input() products: Product[] = [];
@Input() hideIdentical = false; @Input() hideIdentical = false;
@@ -61,6 +64,10 @@ export class CompareTableComponent {
return this.hideIdentical ? baseRows.filter(row => !row.identical) : baseRows; return this.hideIdentical ? baseRows.filter(row => !row.identical) : baseRows;
}); });
productTitle(product: Product): string {
return getTranslatedField(product, 'name', this.languageService.currentLanguage());
}
isDifferentRow(row: CompareRow): boolean { isDifferentRow(row: CompareRow): boolean {
return this.highlightDifferences && !row.identical; return this.highlightDifferences && !row.identical;
} }

View File

@@ -26,7 +26,7 @@
<section class="compare-products-list"> <section class="compare-products-list">
@for (product of products(); track product.itemID) { @for (product of products(); track product.itemID) {
<article class="compare-product-chip"> <article class="compare-product-chip">
<a [routerLink]="['/product', product.itemID] | langRoute">{{ product.name }}</a> <a [routerLink]="['/product', product.itemID] | langRoute">{{ productTitle(product) }}</a>
<button type="button" [attr.aria-label]="'ux.removeFromCompare' | translate" (click)="remove(product.itemID)">×</button> <button type="button" [attr.aria-label]="'ux.removeFromCompare' | translate" (click)="remove(product.itemID)">×</button>
</article> </article>
} }

View File

@@ -5,6 +5,8 @@ import { Product } from '../../../../../core/products/models/product-domain.mode
import { UserExperienceFacade } from '../../../../../facades/platform/user-experience.facade'; import { UserExperienceFacade } from '../../../../../facades/platform/user-experience.facade';
import { TranslatePipe } from '../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
import { LanguageService } from '../../../../../services/language.service';
import { getTranslatedField } from '../../../../../utils/item.utils';
import { DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../../../shared/models/config'; import { DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../../../shared/models/config';
import { CompareTableComponent } from '../components/compare-table.component'; import { CompareTableComponent } from '../components/compare-table.component';
import { EmptyStateComponent } from '../../../../../shared/ui/empty-state/empty-state.component'; import { EmptyStateComponent } from '../../../../../shared/ui/empty-state/empty-state.component';
@@ -21,6 +23,7 @@ import { ButtonComponent } from '../../../../../shared/ui/button/button.componen
export class ComparePageComponent { export class ComparePageComponent {
private readonly uxFacade = inject(UserExperienceFacade); private readonly uxFacade = inject(UserExperienceFacade);
private readonly configService = inject(ConfigService); private readonly configService = inject(ConfigService);
private readonly languageService = inject(LanguageService);
private readonly compareConfig = this.resolveCompareConfig(); private readonly compareConfig = this.resolveCompareConfig();
@@ -39,6 +42,10 @@ export class ComparePageComponent {
this.uxFacade.clearCompare(); this.uxFacade.clearCompare();
} }
productTitle(product: Product): string {
return getTranslatedField(product, 'name', this.languageService.currentLanguage());
}
private resolveCompareConfig() { private resolveCompareConfig() {
const raw = (this.configService.getBootstrapSnapshot() as any)?.userExperience?.compare ?? {}; const raw = (this.configService.getBootstrapSnapshot() as any)?.userExperience?.compare ?? {};
return { return {

View File

@@ -144,6 +144,10 @@ export const en: Translations = {
noResultsHint: 'Try changing your query or using different keywords', noResultsHint: 'Try changing your query or using different keywords',
emptyResultsAria: 'Empty search results', emptyResultsAria: 'Empty search results',
popularCategories: 'Popular categories', popularCategories: 'Popular categories',
popularSmartphones: 'Smartphones',
popularSneakers: 'Sneakers',
popularHeadphones: 'Headphones',
popularLaptops: 'Laptops',
recommendedProducts: 'Recommended products', recommendedProducts: 'Recommended products',
aiSuggestionHint: 'AI suggestion (future)', aiSuggestionHint: 'AI suggestion (future)',
suggestionType: { suggestionType: {

View File

@@ -144,6 +144,10 @@ export const hy: Translations = {
noResultsHint: 'Փորձեք փոխել հարցումը կամ օգտագործել այլ բանալի բառեր', noResultsHint: 'Փորձեք փոխել հարցումը կամ օգտագործել այլ բանալի բառեր',
emptyResultsAria: 'Դատարկ որոնման արդյունքներ', emptyResultsAria: 'Դատարկ որոնման արդյունքներ',
popularCategories: 'Հանրաճանաչ կատեգորիաներ', popularCategories: 'Հանրաճանաչ կատեգորիաներ',
popularSmartphones: 'Սմարթֆոններ',
popularSneakers: 'Կեդեր',
popularHeadphones: 'Ականջակալներ',
popularLaptops: 'Նոութբուքեր',
recommendedProducts: 'Առաջարկվող ապրանքներ', recommendedProducts: 'Առաջարկվող ապրանքներ',
aiSuggestionHint: 'AI առաջարկ (ապագայում)', aiSuggestionHint: 'AI առաջարկ (ապագայում)',
suggestionType: { suggestionType: {

View File

@@ -144,6 +144,10 @@ export const ru: Translations = {
noResultsHint: 'Попробуйте изменить запрос или используйте другие ключевые слова', noResultsHint: 'Попробуйте изменить запрос или используйте другие ключевые слова',
emptyResultsAria: 'Пустые результаты поиска', emptyResultsAria: 'Пустые результаты поиска',
popularCategories: 'Популярные категории', popularCategories: 'Популярные категории',
popularSmartphones: 'Смартфоны',
popularSneakers: 'Кроссовки',
popularHeadphones: 'Наушники',
popularLaptops: 'Ноутбуки',
recommendedProducts: 'Рекомендуемые товары', recommendedProducts: 'Рекомендуемые товары',
aiSuggestionHint: 'AI-подсказка (в будущем)', aiSuggestionHint: 'AI-подсказка (в будущем)',
suggestionType: { suggestionType: {

View File

@@ -142,6 +142,10 @@ export interface Translations {
noResultsHint: string; noResultsHint: string;
emptyResultsAria: string; emptyResultsAria: string;
popularCategories: string; popularCategories: string;
popularSmartphones: string;
popularSneakers: string;
popularHeadphones: string;
popularLaptops: string;
recommendedProducts: string; recommendedProducts: string;
aiSuggestionHint: string; aiSuggestionHint: string;
suggestionType: { suggestionType: {