فرانت‌اند: نسخه انگلیسی با مسیر /en/ — مسیریابی دوزبانه، دیکشنری رابط، بومی‌سازی داده، تغییر زبان در هدر، ویرایش انگلیسی در پنل و پشتیبانی LTR

This commit is contained in:
MMD
2026-09-12 23:37:55 +03:30
parent 6bb6226e06
commit 6b49d035c2
43 changed files with 1194 additions and 262 deletions
+34 -16
View File
@@ -7,6 +7,8 @@ export type FieldConfig = {
label: string
type: FieldType
hint?: string
/** فیلدهای مشترک بین دو زبان (تصویر، لینک، اسلاگ، وضعیت، آیکون، عدد) ترجمه نمی‌شوند */
shared?: boolean
}
export type EntityConfig = {
@@ -24,11 +26,12 @@ export type EntityConfig = {
defaults: Record<string, unknown>
}
const F = (name: string, label: string, type: FieldType = 'text', hint?: string): FieldConfig => ({
const F = (name: string, label: string, type: FieldType = 'text', hint?: string, shared = false): FieldConfig => ({
name,
label,
type,
hint,
shared,
})
/** پیکربندی همه منابع قابل ویرایش — هم مجموعه‌ها و هم رکوردهای یگانه */
@@ -36,16 +39,16 @@ export const ENTITIES: Record<string, EntityConfig> = {
services: {
resource: 'services', dataKey: 'services', title: 'خدمات', kind: 'collection',
titleKey: 'title',
fields: [F('title', 'عنوان'), F('desc', 'توضیح', 'textarea'), F('icon', 'آیکون', 'text', 'draft / tower / mall / villa / renew / manage / invest / landscape')],
fields: [F('title', 'عنوان'), F('desc', 'توضیح', 'textarea'), F('icon', 'آیکون', 'text', 'draft / tower / mall / villa / renew / manage / invest / landscape', true)],
defaults: { title: 'خدمت جدید', desc: '', icon: 'draft' },
},
projects: {
resource: 'projects', dataKey: 'projects', title: 'پروژه‌ها', kind: 'collection',
titleKey: 'title',
fields: [
F('title', 'عنوان'), F('slug', 'نشانی (slug)'), F('location', 'موقعیت'),
F('year', 'سال'), F('status', 'وضعیت', 'text', 'done یا progress'),
F('image', 'تصویر شاخص', 'image'), F('gallery', 'گالری (هر خط یک URL)', 'list'),
F('title', 'عنوان'), F('slug', 'نشانی (slug)', 'text', undefined, true), F('location', 'موقعیت'),
F('year', 'سال'), F('status', 'وضعیت', 'text', 'done یا progress', true),
F('image', 'تصویر شاخص', 'image', undefined, true), F('gallery', 'گالری (هر خط یک URL)', 'list', undefined, true),
F('area', 'زیربنا'), F('floors', 'طبقات'), F('units', 'واحدها'), F('client', 'کارفرما'),
F('summary', 'خلاصه', 'textarea'), F('body', 'متن پروژه (هر پاراگراف یک خط)', 'list'),
],
@@ -58,11 +61,11 @@ export const ENTITIES: Record<string, EntityConfig> = {
F('title', 'عنوان'),
F('body', 'متن مقاله', 'wysiwyg'),
F('excerpt', 'چکیده', 'textarea'),
F('image', 'تصویر شاخص', 'image'),
F('image', 'تصویر شاخص', 'image', undefined, true),
F('category', 'دسته'),
F('date', 'تاریخ'),
F('read', 'زمان مطالعه'),
F('slug', 'نشانی (slug)'),
F('slug', 'نشانی (slug)', 'text', undefined, true),
],
defaults: { title: 'مقاله جدید', slug: `article-${Date.now()}`, date: '', category: '', image: '', read: '', excerpt: '', body: { type: 'doc', content: [{ type: 'paragraph' }] } },
},
@@ -87,13 +90,13 @@ export const ENTITIES: Record<string, EntityConfig> = {
timeline: {
resource: 'timeline', dataKey: 'timeline', title: 'تایم‌لاین', kind: 'collection',
titleKey: 'title',
fields: [F('year', 'سال'), F('title', 'عنوان'), F('desc', 'توضیح', 'textarea'), F('image', 'تصویر', 'image')],
fields: [F('year', 'سال'), F('title', 'عنوان'), F('desc', 'توضیح', 'textarea'), F('image', 'تصویر', 'image', undefined, true)],
defaults: { year: '۱۴۰۰', title: 'رویداد جدید', desc: '', image: '' },
},
stats: {
resource: 'stats', dataKey: 'stats', title: 'آمار', kind: 'collection',
titleKey: 'label',
fields: [F('label', 'برچسب'), F('value', 'عدد', 'number'), F('suffix', 'پسوند (مثل + یا هزار)')],
fields: [F('label', 'برچسب'), F('value', 'عدد', 'number', undefined, true), F('suffix', 'پسوند (مثل + یا هزار)')],
defaults: { label: 'شاخص جدید', value: 0, suffix: '' },
},
why_us: {
@@ -123,13 +126,13 @@ export const ENTITIES: Record<string, EntityConfig> = {
gallery: {
resource: 'gallery', dataKey: 'gallery', title: 'گالری تصاویر', kind: 'collection',
titleKey: 'alt',
fields: [F('src', 'تصویر', 'image'), F('alt', 'متن جایگزین')],
fields: [F('src', 'تصویر', 'image', undefined, true), F('alt', 'متن جایگزین')],
defaults: { src: '', alt: 'تصویر گالری' },
},
nav: {
resource: 'nav', dataKey: 'nav', title: 'منوی ناوبری', kind: 'collection',
titleKey: 'label',
fields: [F('label', 'عنوان'), F('href', 'مسیر (مثل /about)')],
fields: [F('label', 'عنوان'), F('href', 'مسیر (مثل /about)', 'text', undefined, true)],
defaults: { label: 'آیتم جدید', href: '/' },
},
@@ -137,7 +140,7 @@ export const ENTITIES: Record<string, EntityConfig> = {
resource: 'seo-meta', dataKey: 'seo_meta', title: 'سئوی صفحه', kind: 'collection',
titleKey: 'page_key',
fields: [
F('page_key', 'کلید صفحه', 'text', 'home / about / services / projects / investment / blog / faq / contact'),
F('page_key', 'کلید صفحه', 'text', 'home / about / services / projects / investment / blog / faq / contact', true),
F('title', 'عنوان سئو (عنوان تب مرورگر و گوگل)'),
F('description', 'توضیح سئو (متا دیسکریپشن)', 'textarea'),
],
@@ -148,25 +151,25 @@ export const ENTITIES: Record<string, EntityConfig> = {
brand: {
resource: 'brand', dataKey: 'brand', title: 'برند و تماس', kind: 'singleton',
titleKey: 'name',
fields: [F('name', 'نام برند'), F('logo', 'لوگو', 'image'), F('hero', 'تصویر هیرو', 'image'), F('contact_bg', 'پس‌زمینه بخش تماس', 'image'), F('phone', 'تلفن نمایشی'), F('phone_href', 'لینک تلفن'), F('email', 'ایمیل'), F('address', 'نشانی', 'textarea')],
fields: [F('name', 'نام برند'), F('logo', 'لوگو', 'image', undefined, true), F('hero', 'تصویر هیرو', 'image', undefined, true), F('contact_bg', 'پس‌زمینه بخش تماس', 'image', undefined, true), F('phone', 'تلفن نمایشی', 'text', undefined, true), F('phone_href', 'لینک تلفن', 'text', undefined, true), F('email', 'ایمیل', 'text', undefined, true), F('address', 'نشانی', 'textarea')],
defaults: {},
},
hero: {
resource: 'hero', dataKey: 'hero', title: 'تنظیمات هیرو', kind: 'singleton',
titleKey: 'title',
fields: [F('title_highlight', 'واژه مسیِ تیتر'), F('badge_value', 'عدد کارت کنار'), F('badge_label', 'متن کارت کنار', 'textarea'), F('cta_primary_label', 'متن دکمه اصلی'), F('cta_primary_href', 'لینک دکمه اصلی'), F('cta_secondary_label', 'متن دکمه دوم'), F('cta_secondary_href', 'لینک دکمه دوم'), F('scroll_label', 'متن اسکرول'), F('kicker', 'متن بالای تیتر', 'textarea')],
fields: [F('title_highlight', 'واژه مسیِ تیتر'), F('badge_value', 'عدد کارت کنار'), F('badge_label', 'متن کارت کنار', 'textarea'), F('cta_primary_label', 'متن دکمه اصلی'), F('cta_primary_href', 'لینک دکمه اصلی', 'text', undefined, true), F('cta_secondary_label', 'متن دکمه دوم'), F('cta_secondary_href', 'لینک دکمه دوم', 'text', undefined, true), F('scroll_label', 'متن اسکرول'), F('kicker', 'متن بالای تیتر', 'textarea')],
defaults: {},
},
story: {
resource: 'story', dataKey: 'story', title: 'تصاویر بخش درباره ما', kind: 'singleton',
titleKey: 'title',
fields: [F('quote', 'نقل‌قول مدیرعامل', 'textarea'), F('quote_author', 'استناد نقل‌قول'), F('stat1_value', 'آمار ۱ — عدد'), F('stat1_label', 'آمار ۱ — برچسب'), F('stat2_value', 'آمار ۲ — عدد'), F('stat2_label', 'آمار ۲ — برچسب'), F('link_label', 'متن دکمه'), F('link_href', 'لینک دکمه')],
fields: [F('title', 'عنوان بخش'), F('paragraphs', 'پاراگراف‌های معرفی', 'list'), F('quote', 'نقل‌قول مدیرعامل', 'textarea'), F('quote_author', 'استناد نقل‌قول'), F('stat1_value', 'آمار ۱ — عدد'), F('stat1_label', 'آمار ۱ — برچسب'), F('stat2_value', 'آمار ۲ — عدد'), F('stat2_label', 'آمار ۲ — برچسب'), F('link_label', 'متن دکمه'), F('link_href', 'لینک دکمه', 'text', undefined, true)],
defaults: {},
},
before_after: {
resource: 'before-after', dataKey: 'before_after', title: 'بخش قبل/بعد', kind: 'singleton',
titleKey: 'title',
fields: [F('title', 'عنوان'), F('lead', 'توضیح', 'textarea'), F('before', 'تصویر قبل', 'image'), F('after', 'تصویر بعد', 'image'), F('before_label', 'برچسب قبل'), F('after_label', 'برچسب بعد')],
fields: [F('title', 'عنوان'), F('lead', 'توضیح', 'textarea'), F('before', 'تصویر قبل', 'image', undefined, true), F('after', 'تصویر بعد', 'image', undefined, true), F('before_label', 'برچسب قبل'), F('after_label', 'برچسب بعد')],
defaults: {},
},
contact_section: {
@@ -197,6 +200,21 @@ export const ENTITIES: Record<string, EntityConfig> = {
},
}
/** آیا فیلد در منبع داده قابل ترجمه است؟ (فیلدهای مشترک و پسوندهای انگلیسی قبلی خیر) */
export function isTranslatable(resource: string, name: string): boolean {
if (name.endsWith('_en')) return false
const config = ENTITIES[resource]
return !!config?.fields.some((f) => f.name === name && !f.shared)
}
/** فیلدهای قابل ویرایش یک منبع در زبان داده‌شده — انگلیسی: بدون فیلدهای مشترک و با پسوند _en */
export function editableFields(config: EntityConfig, lang: 'fa' | 'en'): FieldConfig[] {
if (lang === 'fa') return config.fields
return config.fields
.filter((f) => !f.shared)
.map((f) => ({ ...f, name: `${f.name}_en` }))
}
/** دسترسی به آرایه/شیء منبع از درخت داده سایت */
export function getEntityData(data: SiteData, config: EntityConfig): Record<string, unknown> & { id: number }[] | (Record<string, unknown> & { id: number }) {
return data[config.dataKey as keyof SiteData] as never