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:
sdarbinyan
2026-07-25 23:54:08 +04:00
parent 0982397d6b
commit 1163bfd88a
14 changed files with 68 additions and 18 deletions

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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

View File

@@ -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',

View File

@@ -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: 'Բոլոր տարածաշրջանները',

View File

@@ -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: 'Все регионы',

View File

@@ -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;

View File

@@ -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>
} }

View File

@@ -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,
}, },

View File

@@ -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"
> >
&times; &times;
</button> </button>

View File

@@ -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

View File

@@ -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>
} }

View File

@@ -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)"
>&larr;</button> >&larr;</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)"
>&rarr;</button> >&rarr;</button>