فرانتاند: نسخه انگلیسی با مسیر /en/ — مسیریابی دوزبانه، دیکشنری رابط، بومیسازی داده، تغییر زبان در هدر، ویرایش انگلیسی در پنل و پشتیبانی LTR
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
// دیکشنری دوزبانه رشتههای هاردکد رابط کاربری (بدون محتوای دیتابیس).
|
||||
// کلید فارسی مرجع است؛ متنهای دیتابیس (سئو، پروژهها، مقالهها و...) از localize.ts میآیند.
|
||||
|
||||
export const fa = {
|
||||
// عمومی
|
||||
brandFallback: 'گروه ساختمانی آرامش',
|
||||
loading: 'در حال بارگذاری',
|
||||
errorTitle: 'اتصال به سرور برقرار نشد',
|
||||
errorText: 'محتوای سایت از سرور بارگذاری میشود؛ لطفاً اتصال اینترنت یا وضعیت سرور را بررسی کنید.',
|
||||
retry: 'تلاش دوباره',
|
||||
backToTop: 'بازگشت به بالا',
|
||||
switchLang: 'English',
|
||||
switchLangAria: 'تغییر زبان به انگلیسی',
|
||||
understood: 'متوجه شدم',
|
||||
|
||||
// هدر و ناوبری
|
||||
mainNav: 'ناوبری اصلی',
|
||||
mobileNav: 'ناوبری موبایل',
|
||||
openMenu: 'باز کردن منو',
|
||||
closeMenu: 'بستن منو',
|
||||
callBrand: 'تماس تلفنی با گروه ساختمانی آرامش',
|
||||
|
||||
// فرم تماس — پیامهای اعتبارسنجی و خطا
|
||||
incompleteInfo: 'اطلاعات ناقص است',
|
||||
nameRequired: 'لطفاً نام و نام خانوادگی خود را کامل وارد کنید (حداقل ۳ حرف).',
|
||||
phoneInvalidTitle: 'شماره تلفن نامعتبر است',
|
||||
phoneInvalidText: 'لطفاً یک شماره تماس معتبر وارد کنید تا کارشناسان ما بتوانند با شما ارتباط بگیرند.',
|
||||
submitErrorTitle: 'خطا در ثبت درخواست',
|
||||
formCheck: 'لطفاً اطلاعات فرم را بررسی و دوباره تلاش کنید.',
|
||||
connectionErrorTitle: 'خطا در اتصال به سرور',
|
||||
connectionErrorText: 'ارتباط با سرور برقرار نشد. لطفاً بعداً دوباره تلاش کنید.',
|
||||
|
||||
// خبرنامه
|
||||
emailRequired: 'لطفاً نشانی ایمیل خود را برای عضویت در خبرنامه وارد کنید.',
|
||||
emailInvalidTitle: 'فرمت ایمیل نامعتبر است',
|
||||
emailInvalidText: 'لطفاً ایمیل خود را به درستی وارد کنید (مثال: example@domain.com).',
|
||||
subscribeErrorTitle: 'خطا در عضویت',
|
||||
emailCheck: 'لطفاً ایمیل را بررسی و دوباره تلاش کنید.',
|
||||
|
||||
// تیتر بخشها — فقط جایگزین وقتی دیتابیس خالی است
|
||||
fallbackGalleryTitle: 'گالری تصاویر',
|
||||
fallbackGalleryLead: 'قابهایی از کارگاهها و پروژههای تحویلشده گروه ساختمانی آرامش.',
|
||||
fallbackTeamTitle: 'تیم ما',
|
||||
fallbackTeamLead: 'کسانی که تصمیمهای اصلی هر پروژه را میگیرند و پاسخگوی آن هستند.',
|
||||
fallbackTimelineTitle: 'مسیر ما',
|
||||
fallbackTimelineLead: 'از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژههای ما را ببینید.',
|
||||
fallbackFaqTitle: 'سوالات متداول',
|
||||
fallbackFaqLead: 'پاسخ کوتاه به پرتکرارترین پرسشهای سرمایهگذاران و خریداران.',
|
||||
|
||||
// کارت پروژه
|
||||
yearLabel: 'سال:',
|
||||
sqm: 'متر مربع',
|
||||
|
||||
// جزئیات پروژه
|
||||
projectNotFound: 'این پروژه پیدا نشد',
|
||||
projectNotFoundText: 'ممکن است نشانی تغییر کرده باشد. فهرست کامل پروژهها را ببینید.',
|
||||
backToProjects: 'بازگشت به پروژهها',
|
||||
specLocation: 'موقعیت',
|
||||
specYear: 'سال',
|
||||
specArea: 'زیربنا',
|
||||
specFloors: 'تعداد طبقات',
|
||||
specUnits: 'واحدها',
|
||||
specClient: 'کارفرما',
|
||||
breadcrumbAria: 'مسیر صفحه',
|
||||
home: 'صفحه اصلی',
|
||||
projects: 'پروژهها',
|
||||
projectDetailsAria: 'جزئیات پروژه',
|
||||
aboutProject: 'درباره پروژه',
|
||||
similarProjectCta: 'پروژهای مشابه دارید؟',
|
||||
relatedProjectsAria: 'پروژههای مرتبط',
|
||||
otherProjects: 'پروژههای دیگر',
|
||||
|
||||
// مقاله و وبلاگ
|
||||
articleNotFound: 'این مقاله پیدا نشد',
|
||||
articleNotFoundText: 'فهرست کامل مقالات وبلاگ را ببینید.',
|
||||
backToBlog: 'بازگشت به وبلاگ',
|
||||
blog: 'وبلاگ',
|
||||
relatedArticlesAria: 'مقالات مرتبط',
|
||||
relatedArticles: 'مقالات مرتبط',
|
||||
readArticlePrefix: 'خواندن مقاله',
|
||||
articleImagePrefix: 'تصویر مقاله',
|
||||
featuredArticle: 'مقاله ویژه',
|
||||
readMore: 'ادامه مطلب',
|
||||
draft: 'پیشنویس',
|
||||
writeNewArticle: 'نوشتن مقاله جدید',
|
||||
blogEmpty: 'هنوز مقالهای منتشر نشده است',
|
||||
blogEmptyText: 'بهزودی نخستین نوشتههای ما اینجا منتشر میشود.',
|
||||
articlesAria: 'مقالات',
|
||||
|
||||
// برچسبهای دسترسپذیری بخشها
|
||||
statsAria: 'آمار و ارقام',
|
||||
projectsListAria: 'فهرست پروژهها',
|
||||
projectFilterAria: 'فیلتر وضعیت پروژه',
|
||||
officesAria: 'دفاتر ما',
|
||||
heroAria: 'معرفی گروه ساختمانی آرامش',
|
||||
heroImageAlt: 'نمای شبانه برج مسکونی ساختهشده توسط گروه ساختمانی آرامش',
|
||||
heroTitleAria: 'تیتر اصلی صفحه',
|
||||
scrollDown: 'رفتن به بخش بعدی',
|
||||
compareAria: 'مقایسه کانسپت و پروژه ساختهشده',
|
||||
zoomImagePrefix: 'بزرگنمایی تصویر شماره',
|
||||
viewImage: 'نمایش تصویر',
|
||||
close: 'بستن',
|
||||
prevImage: 'تصویر قبلی',
|
||||
nextImage: 'تصویر بعدی',
|
||||
prevTestimonial: 'نظر قبلی',
|
||||
nextTestimonial: 'نظر بعدی',
|
||||
selectTestimonialAria: 'انتخاب نظر',
|
||||
testimonialByPrefix: 'نظر',
|
||||
}
|
||||
|
||||
export type Dict = typeof fa
|
||||
|
||||
export const en: Dict = {
|
||||
brandFallback: 'Aramesh Construction Group',
|
||||
loading: 'Loading',
|
||||
errorTitle: 'Could not reach the server',
|
||||
errorText: 'Site content is loaded from the server; please check your internet connection or the server status.',
|
||||
retry: 'Try again',
|
||||
backToTop: 'Back to top',
|
||||
switchLang: 'فا',
|
||||
switchLangAria: 'Switch language to Persian (Farsi)',
|
||||
understood: 'Got it',
|
||||
|
||||
mainNav: 'Main navigation',
|
||||
mobileNav: 'Mobile navigation',
|
||||
openMenu: 'Open menu',
|
||||
closeMenu: 'Close menu',
|
||||
callBrand: 'Call Aramesh Construction Group',
|
||||
|
||||
incompleteInfo: 'Incomplete information',
|
||||
nameRequired: 'Please enter your full name (at least 3 characters).',
|
||||
phoneInvalidTitle: 'Invalid phone number',
|
||||
phoneInvalidText: 'Please enter a valid phone number so our experts can get in touch with you.',
|
||||
submitErrorTitle: 'Error submitting the request',
|
||||
formCheck: 'Please check the form and try again.',
|
||||
connectionErrorTitle: 'Server connection error',
|
||||
connectionErrorText: 'Could not reach the server. Please try again later.',
|
||||
|
||||
emailRequired: 'Please enter your email address to subscribe to the newsletter.',
|
||||
emailInvalidTitle: 'Invalid email format',
|
||||
emailInvalidText: 'Please enter your email correctly (e.g. example@domain.com).',
|
||||
subscribeErrorTitle: 'Subscription error',
|
||||
emailCheck: 'Please check your email and try again.',
|
||||
|
||||
fallbackGalleryTitle: 'Gallery',
|
||||
fallbackGalleryLead: 'Frames from the construction sites and delivered projects of Aramesh Construction Group.',
|
||||
fallbackTeamTitle: 'Our Team',
|
||||
fallbackTeamLead: 'The people who make each project’s key decisions — and answer for them.',
|
||||
fallbackTimelineTitle: 'Our Path',
|
||||
fallbackTimelineLead: 'From a four-person office to 47 projects. Walk through the timeline of our work.',
|
||||
fallbackFaqTitle: 'Frequently Asked Questions',
|
||||
fallbackFaqLead: 'Short answers to the questions investors and buyers ask most.',
|
||||
|
||||
yearLabel: 'Year:',
|
||||
sqm: 'm²',
|
||||
|
||||
projectNotFound: 'Project not found',
|
||||
projectNotFoundText: 'The address may have changed. Browse the full list of projects.',
|
||||
backToProjects: 'Back to projects',
|
||||
specLocation: 'Location',
|
||||
specYear: 'Year',
|
||||
specArea: 'Built area',
|
||||
specFloors: 'Floors',
|
||||
specUnits: 'Units',
|
||||
specClient: 'Client',
|
||||
breadcrumbAria: 'Breadcrumb',
|
||||
home: 'Home',
|
||||
projects: 'Projects',
|
||||
projectDetailsAria: 'Project details',
|
||||
aboutProject: 'About the project',
|
||||
similarProjectCta: 'Have a similar project in mind?',
|
||||
relatedProjectsAria: 'Related projects',
|
||||
otherProjects: 'More projects',
|
||||
|
||||
articleNotFound: 'Article not found',
|
||||
articleNotFoundText: 'Browse the full list of blog articles.',
|
||||
backToBlog: 'Back to blog',
|
||||
blog: 'Blog',
|
||||
relatedArticlesAria: 'Related articles',
|
||||
relatedArticles: 'Related articles',
|
||||
readArticlePrefix: 'Read article',
|
||||
articleImagePrefix: 'Image of article',
|
||||
featuredArticle: 'Featured article',
|
||||
readMore: 'Read more',
|
||||
draft: 'Draft',
|
||||
writeNewArticle: 'Write a new article',
|
||||
blogEmpty: 'No articles published yet',
|
||||
blogEmptyText: 'Our first articles will be published here soon.',
|
||||
articlesAria: 'Articles',
|
||||
|
||||
statsAria: 'Stats and figures',
|
||||
projectsListAria: 'Project list',
|
||||
projectFilterAria: 'Filter projects by status',
|
||||
officesAria: 'Our offices',
|
||||
heroAria: 'Introduction of Aramesh Construction Group',
|
||||
heroImageAlt: 'Night view of a residential tower built by Aramesh Construction Group',
|
||||
heroTitleAria: 'Main headline',
|
||||
scrollDown: 'Go to the next section',
|
||||
compareAria: 'Compare the concept with the built project',
|
||||
zoomImagePrefix: 'Zoom image No.',
|
||||
viewImage: 'View image',
|
||||
close: 'Close',
|
||||
prevImage: 'Previous image',
|
||||
nextImage: 'Next image',
|
||||
prevTestimonial: 'Previous testimonial',
|
||||
nextTestimonial: 'Next testimonial',
|
||||
selectTestimonialAria: 'Select testimonial',
|
||||
testimonialByPrefix: 'Testimonial by',
|
||||
}
|
||||
|
||||
export const dict = { fa, en }
|
||||
export type DictKey = keyof Dict
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createContext, useContext, useEffect, useMemo } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { dict } from './dict'
|
||||
import type { DictKey } from './dict'
|
||||
|
||||
export type Lang = 'fa' | 'en'
|
||||
|
||||
// زبان سایت از خود URL میآید: /en/... انگلیسی و بقیه فارسی — هر زبان یک نشانی واقعی و قابل ایندکس است.
|
||||
const LANG_COOKIE = 'lang'
|
||||
|
||||
/** تشخیص زبان از مسیر — /en و هر زیرمسیر آن انگلیسی است */
|
||||
export function langFromPathname(pathname: string): Lang {
|
||||
return pathname === '/en' || pathname.startsWith('/en/') ? 'en' : 'fa'
|
||||
}
|
||||
|
||||
/** مسیر معادل در زبان دیگر — برای دکمه تغییر زبان */
|
||||
export function counterpartPath(pathname: string, lang: Lang): string {
|
||||
if (lang === 'fa') return pathname === '/' ? '/en' : `/en${pathname}`
|
||||
return pathname === '/en' ? '/' : pathname.slice(3)
|
||||
}
|
||||
|
||||
/** تبدیل مسیر داخلی دیتابیس (مثل /about) به مسیر زبان جاری — لینکهای خارجی و tel/mailto دستنخورده میمانند */
|
||||
export function localeHref(href: string, lang: Lang): string {
|
||||
if (!href.startsWith('/')) return href
|
||||
if (lang === 'fa') return href
|
||||
return href === '/' ? '/en' : `/en${href}`
|
||||
}
|
||||
|
||||
export function readLangCookie(): Lang | null {
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${LANG_COOKIE}=(fa|en)`))
|
||||
return (match?.[1] as Lang) ?? null
|
||||
}
|
||||
|
||||
export function writeLangCookie(lang: Lang): void {
|
||||
document.cookie = `${LANG_COOKIE}=${lang}; path=/; max-age=31536000; samesite=lax`
|
||||
}
|
||||
|
||||
type LangContextValue = {
|
||||
lang: Lang
|
||||
dir: 'rtl' | 'ltr'
|
||||
/** متنهای هاردکد رابط کاربری به زبان جاری */
|
||||
t: (key: DictKey) => string
|
||||
/** مسیر داخلی دیتابیس با پیشوند زبان جاری */
|
||||
href: (path: string) => string
|
||||
}
|
||||
|
||||
const LangContext = createContext<LangContextValue>({
|
||||
lang: 'fa',
|
||||
dir: 'rtl',
|
||||
t: (key) => dict.fa[key],
|
||||
href: (path) => path,
|
||||
})
|
||||
|
||||
export function LangProvider({ children }: { children: ReactNode }) {
|
||||
const { pathname } = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const lang = langFromPathname(pathname)
|
||||
const dir = lang === 'fa' ? 'rtl' : 'ltr'
|
||||
|
||||
// زبان و جهت سند — واریانتهای rtl:/ltr: تیلویند و دسترسپذیری به آن وابستهاند
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('lang', lang)
|
||||
document.documentElement.setAttribute('dir', dir)
|
||||
}, [lang, dir])
|
||||
|
||||
// انتخاب زبان در کوکی — برای بازدید بعدی؛ صفحههای مدیریتی زبان بازدیدکننده را عوض نمیکنند
|
||||
useEffect(() => {
|
||||
if (!pathname.startsWith('/admin')) writeLangCookie(lang)
|
||||
}, [lang, pathname])
|
||||
|
||||
// بازدیدکنندهای که قبلاً انگلیسی انتخاب کرده، هنگام فرود روی / یک بار به /en میرود
|
||||
useEffect(() => {
|
||||
if (pathname !== '/' || readLangCookie() !== 'en') return
|
||||
if (sessionStorage.getItem('langAutoRedirect') === '1') return
|
||||
sessionStorage.setItem('langAutoRedirect', '1')
|
||||
navigate('/en', { replace: true })
|
||||
}, [pathname, navigate])
|
||||
|
||||
const value = useMemo<LangContextValue>(
|
||||
() => ({
|
||||
lang,
|
||||
dir,
|
||||
t: (key) => dict[lang][key],
|
||||
href: (path) => localeHref(path, lang),
|
||||
}),
|
||||
[lang, dir],
|
||||
)
|
||||
|
||||
return <LangContext.Provider value={value}>{children}</LangContext.Provider>
|
||||
}
|
||||
|
||||
export function useLang(): LangContextValue {
|
||||
return useContext(LangContext)
|
||||
}
|
||||
|
||||
/** عدد با رقمهای زبان جاری — فارسی fa-IR و انگلیسی لاتین */
|
||||
export function formatNumber(value: number, lang: Lang): string {
|
||||
return value.toLocaleString(lang === 'fa' ? 'fa-IR' : 'en-US')
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// بومیسازی درخت داده سایت — انتخاب فیلدهای انگلیسی با برگشت به فارسی.
|
||||
// فرانتاند هر دو زبان را در یک درخواست از GET /api/site میگیرد و سوییچ زبان
|
||||
// بیدرنگ و بدون درخواست تازه انجام میشود. داده خام برای پنل مدیریت دستنخورده میماند.
|
||||
|
||||
import type { Article, ArticleBodyDoc } from '../data/site'
|
||||
import type { SiteData } from '../api'
|
||||
import type { Lang } from './lang'
|
||||
|
||||
/** متن انگلیسی فقط وقتی جایگزین میشود که واقعاً چیزی نوشته شده باشد */
|
||||
const s = (fa: string, en?: string | null): string => (en && en.trim() ? en : fa)
|
||||
|
||||
/** لیست انگلیسی فقط وقتی جایگزین میشود که غیرخالی باشد */
|
||||
const list = <T,>(fa: T[], en?: T[] | null): T[] => (en && en.length ? en : fa)
|
||||
|
||||
/** سند TipTap انگلیسی فقط وقتی متن واقعی دارد جایگزین بدنه فارسی میشود */
|
||||
function docHasText(doc: unknown): boolean {
|
||||
if (!doc || typeof doc !== 'object') return false
|
||||
const content = (doc as ArticleBodyDoc).content
|
||||
if (!Array.isArray(content)) return false
|
||||
let found = false
|
||||
const walk = (node: unknown): void => {
|
||||
if (!node || typeof node !== 'object' || found) return
|
||||
const n = node as { type?: string; text?: string; content?: unknown[] }
|
||||
if (n.type === 'text' && typeof n.text === 'string' && n.text.trim()) found = true
|
||||
n.content?.forEach(walk)
|
||||
}
|
||||
content.forEach(walk)
|
||||
return found
|
||||
}
|
||||
|
||||
export function localizeSite(data: SiteData, lang: Lang): SiteData {
|
||||
if (lang === 'fa') return data
|
||||
return {
|
||||
brand: {
|
||||
...data.brand,
|
||||
name: s(data.brand.name, data.brand.name_en),
|
||||
address: s(data.brand.address, data.brand.address_en),
|
||||
},
|
||||
nav: data.nav.map((n) => ({ ...n, label: s(n.label, n.label_en) })),
|
||||
hero: {
|
||||
...data.hero,
|
||||
kicker: s(data.hero.kicker, data.hero.kicker_en),
|
||||
title: s(data.hero.title, data.hero.title_en),
|
||||
title_highlight: s(data.hero.title_highlight, data.hero.title_highlight_en),
|
||||
subtitle: s(data.hero.subtitle, data.hero.subtitle_en),
|
||||
badge_value: s(data.hero.badge_value, data.hero.badge_value_en),
|
||||
badge_label: s(data.hero.badge_label, data.hero.badge_label_en),
|
||||
cta_primary_label: s(data.hero.cta_primary_label, data.hero.cta_primary_label_en),
|
||||
cta_secondary_label: s(data.hero.cta_secondary_label, data.hero.cta_secondary_label_en),
|
||||
scroll_label: s(data.hero.scroll_label, data.hero.scroll_label_en),
|
||||
},
|
||||
story: {
|
||||
...data.story,
|
||||
title: s(data.story.title, data.story.title_en),
|
||||
paragraphs: list(data.story.paragraphs, data.story.paragraphs_en),
|
||||
quote: s(data.story.quote, data.story.quote_en),
|
||||
quote_author: s(data.story.quote_author, data.story.quote_author_en),
|
||||
stat1_value: s(data.story.stat1_value, data.story.stat1_value_en),
|
||||
stat1_label: s(data.story.stat1_label, data.story.stat1_label_en),
|
||||
stat2_value: s(data.story.stat2_value, data.story.stat2_value_en),
|
||||
stat2_label: s(data.story.stat2_label, data.story.stat2_label_en),
|
||||
link_label: s(data.story.link_label, data.story.link_label_en),
|
||||
bento: list(data.story.bento, data.story.bento_en),
|
||||
},
|
||||
section_headings: data.section_headings.map((h) => ({
|
||||
...h,
|
||||
label: s(h.label, h.label_en),
|
||||
title: s(h.title, h.title_en),
|
||||
lead: s(h.lead, h.lead_en),
|
||||
cta_label: s(h.cta_label, h.cta_label_en),
|
||||
})),
|
||||
page_heroes: data.page_heroes.map((p) => ({
|
||||
...p,
|
||||
title: s(p.title, p.title_en),
|
||||
lead: s(p.lead, p.lead_en),
|
||||
})),
|
||||
services: data.services.map((sv) => ({
|
||||
...sv,
|
||||
title: s(sv.title, sv.title_en),
|
||||
desc: s(sv.desc, sv.desc_en),
|
||||
})),
|
||||
projects: data.projects.map((p) => ({
|
||||
...p,
|
||||
title: s(p.title, p.title_en),
|
||||
location: s(p.location, p.location_en),
|
||||
year: s(p.year, p.year_en),
|
||||
area: s(p.area, p.area_en),
|
||||
floors: s(p.floors, p.floors_en),
|
||||
units: s(p.units, p.units_en),
|
||||
client: s(p.client, p.client_en),
|
||||
summary: s(p.summary, p.summary_en),
|
||||
body: list(p.body, p.body_en),
|
||||
})),
|
||||
articles: data.articles.map((a: Article) => ({
|
||||
...a,
|
||||
title: s(a.title, a.title_en),
|
||||
date: s(a.date, a.date_en),
|
||||
category: s(a.category, a.category_en),
|
||||
read: s(a.read, a.read_en),
|
||||
excerpt: s(a.excerpt, a.excerpt_en),
|
||||
body: docHasText(a.body_en) ? a.body_en! : a.body,
|
||||
})),
|
||||
testimonials: data.testimonials.map((tm) => ({
|
||||
...tm,
|
||||
name: s(tm.name, tm.name_en),
|
||||
role: s(tm.role, tm.role_en),
|
||||
quote: s(tm.quote, tm.quote_en),
|
||||
})),
|
||||
faqs: data.faqs.map((f) => ({
|
||||
...f,
|
||||
question: s(f.question, f.question_en),
|
||||
answer: s(f.answer, f.answer_en),
|
||||
})),
|
||||
team: data.team.map((m) => ({
|
||||
...m,
|
||||
name: s(m.name, m.name_en),
|
||||
role: s(m.role, m.role_en),
|
||||
bio: s(m.bio, m.bio_en),
|
||||
})),
|
||||
timeline: data.timeline.map((tm) => ({
|
||||
...tm,
|
||||
year: s(tm.year, tm.year_en),
|
||||
title: s(tm.title, tm.title_en),
|
||||
desc: s(tm.desc, tm.desc_en),
|
||||
})),
|
||||
stats: data.stats.map((st) => ({
|
||||
...st,
|
||||
suffix: s(st.suffix, st.suffix_en),
|
||||
label: s(st.label, st.label_en),
|
||||
})),
|
||||
why_us: data.why_us.map((w) => ({
|
||||
...w,
|
||||
title: s(w.title, w.title_en),
|
||||
desc: s(w.desc, w.desc_en),
|
||||
})),
|
||||
before_after: {
|
||||
...data.before_after,
|
||||
title: s(data.before_after.title, data.before_after.title_en),
|
||||
lead: s(data.before_after.lead, data.before_after.lead_en),
|
||||
before_label: s(data.before_after.before_label, data.before_after.before_label_en),
|
||||
after_label: s(data.before_after.after_label, data.before_after.after_label_en),
|
||||
},
|
||||
investment_models: data.investment_models.map((im) => ({
|
||||
...im,
|
||||
title: s(im.title, im.title_en),
|
||||
desc: s(im.desc, im.desc_en),
|
||||
points: list(im.points, im.points_en),
|
||||
})),
|
||||
process_steps: data.process_steps.map((ps) => ({
|
||||
...ps,
|
||||
step: s(ps.step, ps.step_en),
|
||||
title: s(ps.title, ps.title_en),
|
||||
desc: s(ps.desc, ps.desc_en),
|
||||
})),
|
||||
offices: data.offices.map((o) => ({
|
||||
...o,
|
||||
city: s(o.city, o.city_en),
|
||||
address: s(o.address, o.address_en),
|
||||
hours: s(o.hours, o.hours_en),
|
||||
})),
|
||||
gallery: data.gallery.map((g) => ({ ...g, alt: s(g.alt, g.alt_en) })),
|
||||
status_labels: data.status_labels.map((sl) => ({ ...sl, label: s(sl.label, sl.label_en) })),
|
||||
contact_section: {
|
||||
...data.contact_section,
|
||||
label: s(data.contact_section.label, data.contact_section.label_en),
|
||||
title: s(data.contact_section.title, data.contact_section.title_en),
|
||||
lead: s(data.contact_section.lead, data.contact_section.lead_en),
|
||||
address_label: s(data.contact_section.address_label, data.contact_section.address_label_en),
|
||||
phone_label: s(data.contact_section.phone_label, data.contact_section.phone_label_en),
|
||||
email_label: s(data.contact_section.email_label, data.contact_section.email_label_en),
|
||||
name_label: s(data.contact_section.name_label, data.contact_section.name_label_en),
|
||||
name_placeholder: s(data.contact_section.name_placeholder, data.contact_section.name_placeholder_en),
|
||||
phone_field_label: s(data.contact_section.phone_field_label, data.contact_section.phone_field_label_en),
|
||||
phone_placeholder: s(data.contact_section.phone_placeholder, data.contact_section.phone_placeholder_en),
|
||||
note_label: s(data.contact_section.note_label, data.contact_section.note_label_en),
|
||||
note_optional_label: s(data.contact_section.note_optional_label, data.contact_section.note_optional_label_en),
|
||||
note_placeholder: s(data.contact_section.note_placeholder, data.contact_section.note_placeholder_en),
|
||||
submit_label: s(data.contact_section.submit_label, data.contact_section.submit_label_en),
|
||||
sending_label: s(data.contact_section.sending_label, data.contact_section.sending_label_en),
|
||||
success_title: s(data.contact_section.success_title, data.contact_section.success_title_en),
|
||||
success_text: s(data.contact_section.success_text, data.contact_section.success_text_en),
|
||||
success_button_label: s(data.contact_section.success_button_label, data.contact_section.success_button_label_en),
|
||||
},
|
||||
newsletter: {
|
||||
...data.newsletter,
|
||||
title: s(data.newsletter.title, data.newsletter.title_en),
|
||||
lead: s(data.newsletter.lead, data.newsletter.lead_en),
|
||||
placeholder: s(data.newsletter.placeholder, data.newsletter.placeholder_en),
|
||||
button_label: s(data.newsletter.button_label, data.newsletter.button_label_en),
|
||||
sending_label: s(data.newsletter.sending_label, data.newsletter.sending_label_en),
|
||||
success_text: s(data.newsletter.success_text, data.newsletter.success_text_en),
|
||||
},
|
||||
footer: {
|
||||
...data.footer,
|
||||
about_text: s(data.footer.about_text, data.footer.about_text_en),
|
||||
pages_heading: s(data.footer.pages_heading, data.footer.pages_heading_en),
|
||||
services_heading: s(data.footer.services_heading, data.footer.services_heading_en),
|
||||
contact_heading: s(data.footer.contact_heading, data.footer.contact_heading_en),
|
||||
copyright: s(data.footer.copyright, data.footer.copyright_en),
|
||||
tagline: s(data.footer.tagline, data.footer.tagline_en),
|
||||
socials: list(data.footer.socials, data.footer.socials_en),
|
||||
},
|
||||
seo_meta: data.seo_meta.map((m) => ({
|
||||
...m,
|
||||
title: s(m.title, m.title_en),
|
||||
description: s(m.description, m.description_en),
|
||||
})),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user