diff --git a/backend/app/migrations.py b/backend/app/migrations.py index 3e392bb..60ca893 100644 --- a/backend/app/migrations.py +++ b/backend/app/migrations.py @@ -212,64 +212,74 @@ def _backfill_row(row, en_fields: dict) -> int: def backfill_en_content() -> None: """پر کردن ترجمههای انگلیسی سطرهای موجود از seed_data_en — idempotent. - تطبیق سطرها: جدولهای تکسطری مستقیم؛ جدولهای دارای کلید طبیعی (slug/key/ - page_key) با همان کلید؛ بقیه فقط وقتی سطرها با seed دستنخوردهاند (تعداد و - idها مطابق) و بهترتیب. هیچ مقدار انگلیسیِ ثبتشده بازنویسی نمیشود. + تطبیق سطرها: جدولهای تکسطری مستقیم؛ بقیه با کلیدهای طبیعی/متنی فارسی + (مثلاً slug پروژه، href منو، عنوان خدمت، متن پرسش) — اگر ادمین متن فارسی را + ویرایش کرده باشد سطر تطبیق نمیخورد و ترجمهاش خالی میماند تا از پنل پر شود. + هیچ مقدار انگلیسیِ ثبتشده بازنویسی نمیشود. """ from . import models, seed_data, seed_data_en # lazy — جلوگیری از import حلقهای - # (مدل، فهرست فارسی، فهرست انگلیسی، کلید تطبیق یا None برای ترتیبی) - PLAN: list[tuple[object, list[dict], list[dict], str | None]] = [ - (models.Brand, [seed_data.BRAND], [seed_data_en.BRAND], None), - (models.Hero, [seed_data.HERO], [seed_data_en.HERO], None), - (models.StorySection, [seed_data.STORY], [seed_data_en.STORY], None), - (models.SectionHeadingContent, seed_data.SECTION_HEADINGS, seed_data_en.SECTION_HEADINGS, "key"), - (models.PageHeroContent, seed_data.PAGE_HEROES, seed_data_en.PAGE_HEROES, "key"), - (models.NavItem, seed_data.NAV_ITEMS, seed_data_en.NAV_ITEMS, None), - (models.InvestmentModel, seed_data.INVESTMENT_MODELS, seed_data_en.INVESTMENT_MODELS, None), - (models.ProcessStep, seed_data.PROCESS_STEPS, seed_data_en.PROCESS_STEPS, None), - (models.Office, seed_data.OFFICES, seed_data_en.OFFICES, None), - (models.GalleryImage, seed_data.GALLERY_IMAGES, seed_data_en.GALLERY_IMAGES, None), - (models.StatusLabel, seed_data.STATUS_LABELS, seed_data_en.STATUS_LABELS, "key"), - (models.ContactSectionContent, [seed_data.CONTACT_SECTION], [seed_data_en.CONTACT_SECTION], None), - (models.NewsletterSectionContent, [seed_data.NEWSLETTER], [seed_data_en.NEWSLETTER], None), - (models.FooterContent, [seed_data.FOOTER], [seed_data_en.FOOTER], None), - (models.Service, seed_data.SERVICES, seed_data_en.SERVICES, None), - (models.Stat, seed_data.STATS, seed_data_en.STATS, None), - (models.Project, seed_data.PROJECTS, seed_data_en.PROJECTS, "slug"), - (models.Article, seed_data.ARTICLES, seed_data_en.ARTICLES, "slug"), - (models.Testimonial, seed_data.TESTIMONIALS, seed_data_en.TESTIMONIALS, None), - (models.Faq, seed_data.FAQS, seed_data_en.FAQS, None), - (models.TeamMember, seed_data.TEAM, seed_data_en.TEAM, None), - (models.TimelineItem, seed_data.TIMELINE, seed_data_en.TIMELINE, None), - (models.WhyUsItem, seed_data.WHY, seed_data_en.WHY, None), - (models.BeforeAfter, [seed_data.BEFORE_AFTER], [seed_data_en.BEFORE_AFTER], None), - (models.SeoMeta, seed_data.SEO_META, seed_data_en.SEO_META, "page_key"), + # (مدل، فهرست فارسی، فهرست انگلیسی، کلیدهای تطبیق — خالی برای تکسطریها) + PLAN: list[tuple[object, list[dict], list[dict], tuple[str, ...]]] = [ + (models.Brand, [seed_data.BRAND], [seed_data_en.BRAND], ()), + (models.Hero, [seed_data.HERO], [seed_data_en.HERO], ()), + (models.StorySection, [seed_data.STORY], [seed_data_en.STORY], ()), + (models.SectionHeadingContent, seed_data.SECTION_HEADINGS, seed_data_en.SECTION_HEADINGS, ("key", "title")), + (models.PageHeroContent, seed_data.PAGE_HEROES, seed_data_en.PAGE_HEROES, ("key", "title")), + (models.NavItem, seed_data.NAV_ITEMS, seed_data_en.NAV_ITEMS, ("href", "label")), + (models.InvestmentModel, seed_data.INVESTMENT_MODELS, seed_data_en.INVESTMENT_MODELS, ("title",)), + (models.ProcessStep, seed_data.PROCESS_STEPS, seed_data_en.PROCESS_STEPS, ("title",)), + (models.Office, seed_data.OFFICES, seed_data_en.OFFICES, ("city",)), + (models.GalleryImage, seed_data.GALLERY_IMAGES, seed_data_en.GALLERY_IMAGES, ("alt",)), + (models.StatusLabel, seed_data.STATUS_LABELS, seed_data_en.STATUS_LABELS, ("key",)), + (models.ContactSectionContent, [seed_data.CONTACT_SECTION], [seed_data_en.CONTACT_SECTION], ()), + (models.NewsletterSectionContent, [seed_data.NEWSLETTER], [seed_data_en.NEWSLETTER], ()), + (models.FooterContent, [seed_data.FOOTER], [seed_data_en.FOOTER], ()), + (models.Service, seed_data.SERVICES, seed_data_en.SERVICES, ("title",)), + (models.Stat, seed_data.STATS, seed_data_en.STATS, ("label",)), + (models.Project, seed_data.PROJECTS, seed_data_en.PROJECTS, ("slug", "title")), + (models.Article, seed_data.ARTICLES, seed_data_en.ARTICLES, ("slug", "title")), + (models.Testimonial, seed_data.TESTIMONIALS, seed_data_en.TESTIMONIALS, ("name",)), + (models.Faq, seed_data.FAQS, seed_data_en.FAQS, ("question",)), + (models.TeamMember, seed_data.TEAM, seed_data_en.TEAM, ("name",)), + (models.TimelineItem, seed_data.TIMELINE, seed_data_en.TIMELINE, ("title",)), + (models.WhyUsItem, seed_data.WHY, seed_data_en.WHY, ("title",)), + (models.BeforeAfter, [seed_data.BEFORE_AFTER], [seed_data_en.BEFORE_AFTER], ()), + (models.SeoMeta, seed_data.SEO_META, seed_data_en.SEO_META, ("page_key",)), ] db = SessionLocal() try: total_rows = 0 total_fields = 0 - skipped: list[str] = [] - for model, fa_rows, en_rows, match_key in PLAN: + unmatched: list[str] = [] + for model, fa_rows, en_rows, match_keys in PLAN: rows = db.query(model).all() if not rows or not en_rows: continue + table_name = getattr(model, "__tablename__", str(model)) - if match_key is not None: - en_by_key = {r[match_key]: r for r in en_rows if match_key in r} - matched = [(row, en_by_key.get(getattr(row, match_key, None))) for row in rows] - pairs = [(row, en) for row, en in matched if en is not None] - elif len(rows) == 1 and len(en_rows) == 1: + if not match_keys: + if len(rows) != 1 or len(en_rows) != 1: + unmatched.append(table_name) + continue pairs = [(rows[0], en_rows[0])] else: - # تطبیق ترتیبی فقط برای جدولهای دستنخورده — idها از ۱ پیوسته - ids = [row.id for row in rows] - if ids != list(range(1, len(rows) + 1)) or len(rows) != len(en_rows): - skipped.append(getattr(model, "__tablename__", str(model))) - continue - pairs = list(zip(rows, en_rows)) + # برای هر کلید، نگاشت مقدار → سطر ترجمه (اولین کلید برنده است) + en_lookups: list[dict[object, dict]] = [] + for key in match_keys: + en_lookups.append({r[key]: r for r in en_rows if r.get(key) is not None}) + pairs = [] + matched_en_ids: set[int] = set() + for row in rows: + en_row = None + for key, lookup in zip(match_keys, en_lookups): + en_row = lookup.get(getattr(row, key, None)) + if en_row is not None: + break + if en_row is not None and id(en_row) not in matched_en_ids: + matched_en_ids.add(id(en_row)) + pairs.append((row, en_row)) for row, en_row in pairs: # بدنه مقاله انگلیسی هم مثل فارسی از پاراگراف به سند تبدیل میشود @@ -284,8 +294,10 @@ def backfill_en_content() -> None: db.commit() if total_fields: print(f"ترجمه انگلیسی پر شد: {total_fields} فیلد در {total_rows} سطر.") - if skipped: - print(f"جدولهای تغییرکرده (بدون backfill خودکار — از پنل مدیریت ترجمه کنید): {', '.join(skipped)}") + if unmatched: + print("سطرهایی که با seed تطبیق نکردند (از پنل مدیریت ترجمه کنید):") + for name in unmatched: + print(f" - {name}") finally: db.close() diff --git a/backend/app/seo_pages.py b/backend/app/seo_pages.py index 4b4783a..3ad0baf 100644 --- a/backend/app/seo_pages.py +++ b/backend/app/seo_pages.py @@ -136,8 +136,23 @@ def _shell( ) if lang == "en": - # پوسته بیلدشده فارسی است — برای نسخه انگلیسی جهت و زبان سند عوض میشود + # پوسته بیلدشده فارسی است — برای نسخه انگلیسی جهت، زبان و برند سند عوض میشود raw = re.sub(r']*>', '', raw, count=1) + raw = raw.replace(FALLBACK_FA_BRAND, FALLBACK_EN_BRAND) + raw = raw.replace('"inLanguage":"fa"', '"inLanguage":"en"') + # description استاتیک حذف میشود تا با متای تزریقی تکراری نشود + raw = re.sub(r']*/>\s*', '', raw, count=1) + # og:locale — در پوسته استاتیک بهروز میشود و از بلوک تزریقی حذف تا دوباره نوشته نشود + og_locale = "en_US" if lang == "en" else "fa_IR" + if re.search(r'.*?", f"