fix: i18n gaps in popular searches and compare table
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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:
@@ -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`.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user