fix(storefront): replace hardcoded strings with i18n, neutral empty-state wording
- Route aria-label/alt/title strings (rating, discount, carousel arrows, hero slides, dialog close, toast dismiss, QR code, bank payment iframe, guest checkout fallback) through the translate pipe/service instead of literal English. - Drop the "Oops!"/"Упс!" apology framing from category/subcategory empty states (en/ru/hy) - zero results is not an error.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<div class="product-image">
|
<div class="product-image">
|
||||||
<img [src]="getMainImage(item)" [alt]="title || item.name" loading="lazy" decoding="async" width="300" height="300" />
|
<img [src]="getMainImage(item)" [alt]="title || item.name" loading="lazy" decoding="async" width="300" height="300" />
|
||||||
@if (showDiscountBadge && item.discount > 0) {
|
@if (showDiscountBadge && item.discount > 0) {
|
||||||
<div class="discount-badge" [attr.aria-label]="'Discount ' + item.discount + '%'">-{{ item.discount }}%</div>
|
<div class="discount-badge" [attr.aria-label]="'common.discountLabel' | translate:{ value: item.discount }">-{{ item.discount }}%</div>
|
||||||
}
|
}
|
||||||
@if (showStock && item.remainings) {
|
@if (showStock && item.remainings) {
|
||||||
<span class="stock-badge" [class.out]="item.remainings.toLowerCase() === 'out'">{{ stockLabelKey(item.remainings) | translate }}</span>
|
<span class="stock-badge" [class.out]="item.remainings.toLowerCase() === 'out'">{{ stockLabelKey(item.remainings) | translate }}</span>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (showRating) {
|
@if (showRating) {
|
||||||
<div class="product-rating" [attr.aria-label]="'Rating ' + item.rating">
|
<div class="product-rating" [attr.aria-label]="'common.ratingLabel' | translate:{ value: item.rating }">
|
||||||
<span class="rating-stars">{{ item.rating | number:'1.1-1' }}</span>
|
<span class="rating-stars">{{ item.rating | number:'1.1-1' }}</span>
|
||||||
<span class="rating-count">({{ item.callbacks?.length || item.comments?.length || 0 }})</span>
|
<span class="rating-count">({{ item.callbacks?.length || item.comments?.length || 0 }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
@case ('ready') {
|
@case ('ready') {
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()"
|
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()"
|
||||||
alt="QR Code"
|
[attr.alt]="'common.qrCode' | translate"
|
||||||
width="180"
|
width="180"
|
||||||
height="180"
|
height="180"
|
||||||
loading="eager" />
|
loading="eager" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@for (note of notifications(); track note.id) {
|
@for (note of notifications(); track note.id) {
|
||||||
<article class="floating-note" [class]="'floating-note floating-note-' + note.type">
|
<article class="floating-note" [class]="'floating-note floating-note-' + note.type">
|
||||||
<p>{{ note.message }}</p>
|
<p>{{ note.message }}</p>
|
||||||
<button type="button" (click)="dismiss(note.id)" aria-label="Dismiss">×</button>
|
<button type="button" (click)="dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
import { UserNotificationService } from '../../services/user-notification.service';
|
import { UserNotificationService } from '../../services/user-notification.service';
|
||||||
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-floating-notifications',
|
selector: 'app-floating-notifications',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [TranslatePipe],
|
||||||
templateUrl: './floating-notifications.component.html',
|
templateUrl: './floating-notifications.component.html',
|
||||||
styleUrls: ['./floating-notifications.component.scss'],
|
styleUrls: ['./floating-notifications.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const en: Translations = {
|
|||||||
addToCart: 'Add to cart',
|
addToCart: 'Add to cart',
|
||||||
loadingMore: 'Loading...',
|
loadingMore: 'Loading...',
|
||||||
allLoaded: 'All products loaded',
|
allLoaded: 'All products loaded',
|
||||||
emptyTitle: 'Oops! Nothing here yet',
|
emptyTitle: 'Nothing here yet',
|
||||||
emptyDesc: 'There are no products in this category yet, but they will appear soon',
|
emptyDesc: 'There are no products in this category yet, but they will appear soon',
|
||||||
goHome: 'Go home',
|
goHome: 'Go home',
|
||||||
loading: 'Loading products...',
|
loading: 'Loading products...',
|
||||||
@@ -339,7 +339,7 @@ export const en: Translations = {
|
|||||||
subcategories: {
|
subcategories: {
|
||||||
loading: 'Loading subcategories...',
|
loading: 'Loading subcategories...',
|
||||||
retry: 'Try again',
|
retry: 'Try again',
|
||||||
emptyTitle: 'Oops! No subcategories yet',
|
emptyTitle: 'No subcategories yet',
|
||||||
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
|
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
|
||||||
goHome: 'Go home',
|
goHome: 'Go home',
|
||||||
itemsInCategory: 'Items in this category',
|
itemsInCategory: 'Items in this category',
|
||||||
@@ -1086,6 +1086,17 @@ export const en: Translations = {
|
|||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
errorTitle: 'Something went wrong',
|
errorTitle: 'Something went wrong',
|
||||||
errorDescription: 'We could not load this data. Please try again.',
|
errorDescription: 'We could not load this data. Please try again.',
|
||||||
|
ratingLabel: 'Rating {{value}}',
|
||||||
|
discountLabel: 'Discount {{value}}%',
|
||||||
|
slideLabel: 'Slide {{value}}',
|
||||||
|
slidesLabel: 'Slides',
|
||||||
|
previousProducts: 'Previous products',
|
||||||
|
nextProducts: 'Next products',
|
||||||
|
closeDialog: 'Close dialog',
|
||||||
|
dismiss: 'Dismiss',
|
||||||
|
qrCode: 'QR Code',
|
||||||
|
bankCardPayment: 'Bank card payment',
|
||||||
|
guest: 'Guest',
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
allRegions: 'All regions',
|
allRegions: 'All regions',
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const hy: Translations = {
|
|||||||
addToCart: 'Ավելացնել զամբյուղ',
|
addToCart: 'Ավելացնել զամբյուղ',
|
||||||
loadingMore: 'Բեռնում...',
|
loadingMore: 'Բեռնում...',
|
||||||
allLoaded: 'Բոլոր ապրանքները բեռնված են',
|
allLoaded: 'Բոլոր ապրանքները բեռնված են',
|
||||||
emptyTitle: 'Վա՜յ, այստեղ դեռ դատարկ է',
|
emptyTitle: 'Այստեղ դեռ դատարկ է',
|
||||||
emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան',
|
emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան',
|
||||||
goHome: 'Գլխավոր',
|
goHome: 'Գլխավոր',
|
||||||
loading: 'Ապրանքների բեռնում...',
|
loading: 'Ապրանքների բեռնում...',
|
||||||
@@ -1086,6 +1086,17 @@ export const hy: Translations = {
|
|||||||
remove: 'Հեռացնել',
|
remove: 'Հեռացնել',
|
||||||
errorTitle: 'Ինչ-որ բան այն չէ',
|
errorTitle: 'Ինչ-որ բան այն չէ',
|
||||||
errorDescription: 'Չհաջողվեց բեռնել տվյալները։ Փորձեք կրկին։',
|
errorDescription: 'Չհաջողվեց բեռնել տվյալները։ Փորձեք կրկին։',
|
||||||
|
ratingLabel: 'Վարկանիշ {{value}}',
|
||||||
|
discountLabel: 'Զեղչ {{value}}%',
|
||||||
|
slideLabel: 'Սլայդ {{value}}',
|
||||||
|
slidesLabel: 'Սլայդներ',
|
||||||
|
previousProducts: 'Նախորդ ապրանքները',
|
||||||
|
nextProducts: 'Հաջորդ ապրանքները',
|
||||||
|
closeDialog: 'Փակել պատուհանը',
|
||||||
|
dismiss: 'Փակել',
|
||||||
|
qrCode: 'QR կոդ',
|
||||||
|
bankCardPayment: 'Վճարում բանկային քարտով',
|
||||||
|
guest: 'Հյուր',
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
allRegions: 'Բոլոր տարածաշրջանները',
|
allRegions: 'Բոլոր տարածաշրջանները',
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const ru: Translations = {
|
|||||||
addToCart: 'В корзину',
|
addToCart: 'В корзину',
|
||||||
loadingMore: 'Загрузка...',
|
loadingMore: 'Загрузка...',
|
||||||
allLoaded: 'Все товары загружены',
|
allLoaded: 'Все товары загружены',
|
||||||
emptyTitle: 'Упс! Здесь пока пусто',
|
emptyTitle: 'Здесь пока пусто',
|
||||||
emptyDesc: 'В этой категории ещё нет товаров, но скоро они появятся',
|
emptyDesc: 'В этой категории ещё нет товаров, но скоро они появятся',
|
||||||
goHome: 'На главную',
|
goHome: 'На главную',
|
||||||
loading: 'Загрузка товаров...',
|
loading: 'Загрузка товаров...',
|
||||||
@@ -339,7 +339,7 @@ export const ru: Translations = {
|
|||||||
subcategories: {
|
subcategories: {
|
||||||
loading: 'Загрузка подкатегорий...',
|
loading: 'Загрузка подкатегорий...',
|
||||||
retry: 'Попробовать снова',
|
retry: 'Попробовать снова',
|
||||||
emptyTitle: 'Упс! Подкатегорий пока нет',
|
emptyTitle: 'Подкатегорий пока нет',
|
||||||
emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся',
|
emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся',
|
||||||
goHome: 'На главную',
|
goHome: 'На главную',
|
||||||
itemsInCategory: 'Товары в этой категории',
|
itemsInCategory: 'Товары в этой категории',
|
||||||
@@ -1086,6 +1086,17 @@ export const ru: Translations = {
|
|||||||
remove: 'Удалить',
|
remove: 'Удалить',
|
||||||
errorTitle: 'Что-то пошло не так',
|
errorTitle: 'Что-то пошло не так',
|
||||||
errorDescription: 'Не удалось загрузить данные. Попробуйте ещё раз.',
|
errorDescription: 'Не удалось загрузить данные. Попробуйте ещё раз.',
|
||||||
|
ratingLabel: 'Рейтинг {{value}}',
|
||||||
|
discountLabel: 'Скидка {{value}}%',
|
||||||
|
slideLabel: 'Слайд {{value}}',
|
||||||
|
slidesLabel: 'Слайды',
|
||||||
|
previousProducts: 'Предыдущие товары',
|
||||||
|
nextProducts: 'Следующие товары',
|
||||||
|
closeDialog: 'Закрыть диалог',
|
||||||
|
dismiss: 'Скрыть',
|
||||||
|
qrCode: 'QR-код',
|
||||||
|
bankCardPayment: 'Оплата банковской картой',
|
||||||
|
guest: 'Гость',
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
allRegions: 'Все регионы',
|
allRegions: 'Все регионы',
|
||||||
|
|||||||
@@ -1085,6 +1085,17 @@ export interface Translations {
|
|||||||
remove: string;
|
remove: string;
|
||||||
errorTitle: string;
|
errorTitle: string;
|
||||||
errorDescription: string;
|
errorDescription: string;
|
||||||
|
ratingLabel: string;
|
||||||
|
discountLabel: string;
|
||||||
|
slideLabel: string;
|
||||||
|
slidesLabel: string;
|
||||||
|
previousProducts: string;
|
||||||
|
nextProducts: string;
|
||||||
|
closeDialog: string;
|
||||||
|
dismiss: string;
|
||||||
|
qrCode: string;
|
||||||
|
bankCardPayment: string;
|
||||||
|
guest: string;
|
||||||
};
|
};
|
||||||
location: {
|
location: {
|
||||||
allRegions: string;
|
allRegions: string;
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
<button class="close-modal-btn" (click)="closeBankPaymentPopup()" [attr.aria-label]="'cart.close' | translate">
|
<button class="close-modal-btn" (click)="closeBankPaymentPopup()" [attr.aria-label]="'cart.close' | translate">
|
||||||
<app-icon name="x" [size]="20" />
|
<app-icon name="x" [size]="20" />
|
||||||
</button>
|
</button>
|
||||||
<iframe [src]="bankPaymentFrameUrl()" class="bank-payment-frame" title="Bank card payment"></iframe>
|
<iframe [src]="bankPaymentFrameUrl()" class="bank-payment-frame" [attr.title]="'common.bankCardPayment' | translate"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ export class CartComponent implements OnDestroy {
|
|||||||
price: item.discount > 0 ? item.price * (1 - item.discount / 100) : item.price,
|
price: item.discount > 0 ? item.price * (1 - item.discount / 100) : item.price,
|
||||||
})),
|
})),
|
||||||
customer: {
|
customer: {
|
||||||
name: this.getTelegramUsername() || 'Guest',
|
name: this.getTelegramUsername() || this.i18n.t('common.guest'),
|
||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="app-dialog-panel__close"
|
class="app-dialog-panel__close"
|
||||||
(click)="requestClose()"
|
(click)="requestClose()"
|
||||||
aria-label="Close dialog"
|
[attr.aria-label]="'common.closeDialog' | translate"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
|
|
||||||
export type DialogSize = 'sm' | 'md' | 'lg';
|
export type DialogSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const FOCUSABLE_SELECTOR =
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dialog',
|
selector: 'app-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [TranslatePipe],
|
||||||
templateUrl: './dialog.component.html',
|
templateUrl: './dialog.component.html',
|
||||||
styleUrl: './dialog.component.scss',
|
styleUrl: './dialog.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges, computed, signal } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges, computed, signal } from '@angular/core';
|
||||||
import { SectionConfig } from '../../shared/models/config';
|
import { SectionConfig } from '../../shared/models/config';
|
||||||
import { HeroSlideData, HeroWidgetData } from '../contracts/widget-data.contract';
|
import { HeroSlideData, HeroWidgetData } from '../contracts/widget-data.contract';
|
||||||
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
|
|
||||||
const AUTOPLAY_INTERVAL_MS = 5000;
|
const AUTOPLAY_INTERVAL_MS = 5000;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hero-widget',
|
selector: 'app-hero-widget',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, TranslatePipe],
|
||||||
template: `
|
template: `
|
||||||
<section class="hero-widget">
|
<section class="hero-widget">
|
||||||
@if (activeSlide(); as slide) {
|
@if (activeSlide(); as slide) {
|
||||||
@@ -21,7 +22,7 @@ const AUTOPLAY_INTERVAL_MS = 5000;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (allSlides().length > 1) {
|
@if (allSlides().length > 1) {
|
||||||
<div class="hero-widget__dots" role="tablist" aria-label="Slides">
|
<div class="hero-widget__dots" role="tablist" [attr.aria-label]="'common.slidesLabel' | translate">
|
||||||
@for (dot of allSlides(); track $index) {
|
@for (dot of allSlides(); track $index) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -29,7 +30,7 @@ const AUTOPLAY_INTERVAL_MS = 5000;
|
|||||||
[class.hero-widget__dot--active]="$index === activeIndex()"
|
[class.hero-widget__dot--active]="$index === activeIndex()"
|
||||||
role="tab"
|
role="tab"
|
||||||
[attr.aria-selected]="$index === activeIndex()"
|
[attr.aria-selected]="$index === activeIndex()"
|
||||||
[attr.aria-label]="'Slide ' + ($index + 1)"
|
[attr.aria-label]="'common.slideLabel' | translate:{ value: $index + 1 }"
|
||||||
(click)="goTo($index)"
|
(click)="goTo($index)"
|
||||||
></button>
|
></button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Ou
|
|||||||
import { SectionConfig } from '../../shared/models/config';
|
import { SectionConfig } from '../../shared/models/config';
|
||||||
import { CatalogProductGridComponent } from '../../features/website/catalog/components/product-grid/product-grid.component';
|
import { CatalogProductGridComponent } from '../../features/website/catalog/components/product-grid/product-grid.component';
|
||||||
import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
||||||
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-carousel-widget',
|
selector: 'app-product-carousel-widget',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, CatalogProductGridComponent],
|
imports: [CommonModule, CatalogProductGridComponent, TranslatePipe],
|
||||||
template: `
|
template: `
|
||||||
<section class="product-carousel-widget">
|
<section class="product-carousel-widget">
|
||||||
@if (data?.title) {
|
@if (data?.title) {
|
||||||
@@ -20,7 +21,7 @@ import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="product-carousel-widget__arrow product-carousel-widget__arrow--prev"
|
class="product-carousel-widget__arrow product-carousel-widget__arrow--prev"
|
||||||
aria-label="Previous products"
|
[attr.aria-label]="'common.previousProducts' | translate"
|
||||||
[disabled]="atStart()"
|
[disabled]="atStart()"
|
||||||
(click)="scrollBy(-1)"
|
(click)="scrollBy(-1)"
|
||||||
>←</button>
|
>←</button>
|
||||||
@@ -32,7 +33,7 @@ import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="product-carousel-widget__arrow product-carousel-widget__arrow--next"
|
class="product-carousel-widget__arrow product-carousel-widget__arrow--next"
|
||||||
aria-label="Next products"
|
[attr.aria-label]="'common.nextProducts' | translate"
|
||||||
[disabled]="atEnd()"
|
[disabled]="atEnd()"
|
||||||
(click)="scrollBy(1)"
|
(click)="scrollBy(1)"
|
||||||
>→</button>
|
>→</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user