feat(admin): collapse product translations behind default-language fields
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> P0 user feedback: four parallel description fields (default/ru/en/hy) read as duplicates and confused the admin. - The default-language name/short description/rich description now stand alone; per-language fields moved into a collapsed 'Translations' disclosure with a translated-count badge (e.g. 1/3) - The disclosure explains the fallback rule in merchant language: empty translation means customers see the default text - Translation inputs show the default value as placeholder, making the fallback visible instead of implied - Locale codes shown uppercase (RU/EN/HY) to read as language labels, not field suffixes
This commit is contained in:
@@ -49,17 +49,24 @@
|
||||
<app-input [ngModel]="product.shortDescription" (ngModelChange)="updateField('shortDescription', $event)" />
|
||||
</app-form-field>
|
||||
|
||||
<h4 class="form-card__subheading">{{ 'adminProducts.translations' | translate }}</h4>
|
||||
@for (locale of locales; track locale) {
|
||||
<div class="grid two sub-block">
|
||||
<app-form-field [label]="(('adminProducts.name' | translate) + ' — ' + locale)">
|
||||
<app-input [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" />
|
||||
</app-form-field>
|
||||
<app-form-field [label]="(('adminProducts.shortDescription' | translate) + ' — ' + locale)">
|
||||
<app-input [ngModel]="product.translations[locale]?.shortDescription || ''" (ngModelChange)="updateTranslation(locale, 'shortDescription', $event)" />
|
||||
</app-form-field>
|
||||
</div>
|
||||
}
|
||||
<details class="translations-block">
|
||||
<summary>
|
||||
<span class="pi pi-globe" aria-hidden="true"></span>
|
||||
{{ 'adminProducts.translations' | translate }}
|
||||
<span class="translations-block__count">{{ translatedLocalesCount() }}/{{ locales.length }}</span>
|
||||
</summary>
|
||||
<p class="translations-block__hint">{{ 'adminProducts.translationsHint' | translate }}</p>
|
||||
@for (locale of locales; track locale) {
|
||||
<div class="grid two sub-block">
|
||||
<app-form-field [label]="(('adminProducts.name' | translate) + ' — ' + locale.toUpperCase())">
|
||||
<app-input [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" [placeholder]="product.name" />
|
||||
</app-form-field>
|
||||
<app-form-field [label]="(('adminProducts.shortDescription' | translate) + ' — ' + locale.toUpperCase())">
|
||||
<app-input [ngModel]="product.translations[locale]?.shortDescription || ''" (ngModelChange)="updateTranslation(locale, 'shortDescription', $event)" [placeholder]="product.shortDescription" />
|
||||
</app-form-field>
|
||||
</div>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
|
||||
@if (activeGroup() === 'media') {
|
||||
@@ -233,14 +240,21 @@
|
||||
<h4 class="form-card__subheading">{{ 'adminProducts.htmlDescription' | translate }}</h4>
|
||||
<app-marketplace-html-editor [html]="product.htmlDescription" (htmlChange)="updateField('htmlDescription', $event)" />
|
||||
|
||||
@for (locale of locales; track locale) {
|
||||
<label class="full sub-block"><span>{{ 'adminProducts.htmlDescription' | translate }} {{ locale }}</span>
|
||||
<app-marketplace-html-editor
|
||||
[html]="product.translations[locale]?.htmlDescription || ''"
|
||||
(htmlChange)="updateTranslation(locale, 'htmlDescription', $event)"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
<details class="translations-block">
|
||||
<summary>
|
||||
<span class="pi pi-globe" aria-hidden="true"></span>
|
||||
{{ 'adminProducts.translations' | translate }}
|
||||
</summary>
|
||||
<p class="translations-block__hint">{{ 'adminProducts.translationsHint' | translate }}</p>
|
||||
@for (locale of locales; track locale) {
|
||||
<label class="full sub-block"><span>{{ 'adminProducts.htmlDescription' | translate }} — {{ locale.toUpperCase() }}</span>
|
||||
<app-marketplace-html-editor
|
||||
[html]="product.translations[locale]?.htmlDescription || ''"
|
||||
(htmlChange)="updateTranslation(locale, 'htmlDescription', $event)"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
</details>
|
||||
|
||||
<h4 class="form-card__subheading">{{ 'adminProducts.customer' | translate }}</h4>
|
||||
<div class="readonly-grid">
|
||||
|
||||
@@ -75,3 +75,33 @@ input[type='checkbox'] { width: auto; }
|
||||
.field-with-action { display: flex; gap: 8px; align-items: center; app-input { flex: 1; } }
|
||||
.seo-generate-row { display: flex; align-items: center; gap: 10px; }
|
||||
.seo-generate-row__hint { color: var(--text-secondary, #6b7280); font-size: 0.8rem; }
|
||||
|
||||
.translations-block {
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
|
||||
summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:focus-visible { outline: 2px solid var(--color-primary, #2f8f5b); outline-offset: 2px; }
|
||||
}
|
||||
|
||||
&[open] summary { margin-bottom: 8px; }
|
||||
}
|
||||
.translations-block__count {
|
||||
margin-left: auto;
|
||||
color: var(--text-secondary, #6b7280);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.translations-block__hint {
|
||||
margin: 0 0 10px;
|
||||
color: var(--text-secondary, #6b7280);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,13 @@ export class AdminProductFormComponent {
|
||||
return value.toLowerCase().trim().replace(/[^a-z0-9а-яёա-ֆ]+/gi, '-').replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
translatedLocalesCount(): number {
|
||||
return this.locales.filter(locale => {
|
||||
const tr = this.product.translations[locale];
|
||||
return !!(tr && (tr.name || tr.shortDescription || tr.htmlDescription));
|
||||
}).length;
|
||||
}
|
||||
|
||||
updateTranslation(locale: string, field: 'name' | 'shortDescription' | 'htmlDescription' | 'seoTitle' | 'seoDescription', value: string): void {
|
||||
this.productChange.emit({
|
||||
translations: {
|
||||
|
||||
@@ -1545,6 +1545,7 @@ export const en: Translations = {
|
||||
generate: 'Generate',
|
||||
seoGenerate: 'Fill from product details',
|
||||
seoGenerateHint: 'Fills empty fields from the product name and short description. Your existing text is kept.',
|
||||
translationsHint: 'The main fields above are shown in your default store language. Add translations here for other languages - if a translation is empty, customers see the default text instead.',
|
||||
seoExplain: 'This is what shows up in search results — a clear title and description help customers find this product.',
|
||||
searchTitle: 'Search title',
|
||||
searchTitleHint: 'The headline shown in search results. Keep it short and descriptive.',
|
||||
|
||||
@@ -1540,6 +1540,7 @@ export const hy: Translations = {
|
||||
generate: 'Գեներացնել',
|
||||
seoGenerate: 'Լրացնել ապրանքի տվյալներից',
|
||||
seoGenerateHint: 'Լրացնում է դատարկ դաշտերը անունից և կարճ նկարագրությունից։ Ձեր տեքստը չի փոխվում։',
|
||||
translationsHint: 'Վերևի հիմնական դաշտերը ցուցադրվում են խանութի հիմնական լեզվով։ Այստեղ ավելացրեք թարգմանություններ այլ լեզուների համար — եթե թարգմանությունը դատարկ է, գնորդները կտեսնեն հիմնական տեքստը։',
|
||||
seoExplain: 'Սա այն է, ինչ երևում է որոնման արդյունքներում․ հստակ վերնագիրն ու նկարագրությունը օգնում են գտնել այս ապրանքը։',
|
||||
searchTitle: 'Որոնման վերնագիր',
|
||||
searchTitleHint: 'Վերնագիրը, որ երևում է որոնման արդյունքներում։ Պահեք կարճ։',
|
||||
|
||||
@@ -1540,6 +1540,7 @@ export const ru: Translations = {
|
||||
generate: 'Сгенерировать',
|
||||
seoGenerate: 'Заполнить из данных товара',
|
||||
seoGenerateHint: 'Заполняет пустые поля из названия и краткого описания. Ваш текст не перезаписывается.',
|
||||
translationsHint: 'Основные поля выше отображаются на языке магазина по умолчанию. Здесь добавьте переводы для других языков — если перевод пуст, покупатели увидят текст по умолчанию.',
|
||||
seoExplain: 'Это то, что видно в результатах поиска — понятные заголовок и описание помогают найти товар.',
|
||||
searchTitle: 'Заголовок для поиска',
|
||||
searchTitleHint: 'Заголовок, показываемый в результатах поиска. Делайте его коротким.',
|
||||
|
||||
@@ -1552,6 +1552,7 @@ export interface Translations {
|
||||
generate: string;
|
||||
seoGenerate: string;
|
||||
seoGenerateHint: string;
|
||||
translationsHint: string;
|
||||
seoExplain: string;
|
||||
searchTitle: string;
|
||||
searchTitleHint: string;
|
||||
|
||||
Reference in New Issue
Block a user