اصلاحات بازبینی: تطبیق backfill با متن فارسی، رفع باگ متای /blog (ستون image ناموجود)، پاکسازی og:locale و برند در پوسته سئو، محدودکردن Traefik به پروژه خود، اصلاح مسیریابی Routes و عنوان برند در صفحات جزئیات
This commit is contained in:
+57
-45
@@ -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()
|
||||
|
||||
|
||||
@@ -136,8 +136,23 @@ def _shell(
|
||||
)
|
||||
|
||||
if lang == "en":
|
||||
# پوسته بیلدشده فارسی است — برای نسخه انگلیسی جهت و زبان سند عوض میشود
|
||||
# پوسته بیلدشده فارسی است — برای نسخه انگلیسی جهت، زبان و برند سند عوض میشود
|
||||
raw = re.sub(r'<html[^>]*>', '<html lang="en" dir="ltr">', raw, count=1)
|
||||
raw = raw.replace(FALLBACK_FA_BRAND, FALLBACK_EN_BRAND)
|
||||
raw = raw.replace('"inLanguage":"fa"', '"inLanguage":"en"')
|
||||
# description استاتیک حذف میشود تا با متای تزریقی تکراری نشود
|
||||
raw = re.sub(r'<meta name="description"[^>]*/>\s*', '', raw, count=1)
|
||||
# og:locale — در پوسته استاتیک بهروز میشود و از بلوک تزریقی حذف تا دوباره نوشته نشود
|
||||
og_locale = "en_US" if lang == "en" else "fa_IR"
|
||||
if re.search(r'<meta property="og:locale"', raw):
|
||||
raw = re.sub(
|
||||
r'(<meta property="og:locale" content=")[^"]*(")',
|
||||
lambda m: f'{m.group(1)}{og_locale}{m.group(2)}',
|
||||
raw,
|
||||
count=1,
|
||||
)
|
||||
tags = [t for t in tags if 'og:locale' not in t]
|
||||
block = "\n ".join(tags)
|
||||
raw = re.sub(r"<title>.*?</title>", f"<title>{esc(title)}</title>", raw, count=1, flags=re.S)
|
||||
raw = raw.replace("</head>", f" {alternates}\n {block}\n </head>", 1)
|
||||
if noscript:
|
||||
@@ -223,7 +238,6 @@ def blog_list(db: Session = Depends(get_db)):
|
||||
title=meta.title if meta and meta.title else "وبلاگ | " + _brand_name(db),
|
||||
description=meta.description if meta else "",
|
||||
path="/blog",
|
||||
og_image=meta.image if meta else "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user