دسته‌ها
یادداشت‌ فنی

چندزبانه‌ کردن وب‌اپلیکیشن‌ها بدون درد و خونریزی

بر خلاف چیزی که به نظر می‌رسد توسعه وب‌اپلیکیشن‌هایی که مشتری‌هایی از زبان‌ها و مناطق جغرافیایی مختلف دارد پیچیده است، چراکه بخش‌های زیادی از نرم‌افزار را درگیر می‌کند.

علاوه بر تفاوت‌های زبانی، ما از اعداد، جهت نوشتاری و تقویم متفاوتی نسبت به بیشتر دنیا استفاده می‌کنیم که باعث می‌شود این پیچیدگی‌ها دوچندان باشد.

در کنار این چالش‌های قابل پیش‌بینی، ما برای چندزبانه کردن نیوزباکس با چالش‌های بیشتری هم روبه‌رو شدیم، چون با کدبیس بسیار بزرگی مواجه بودیم که میراث تیم توسعه دیگری بود.

در این نوشته، می‌خواهیم در کنار اشتراک تجربه برخورد با این چالش، در مورد راهکار‌هایی صحبت کنیم که چندزبانه‌ کردن وب‌اپلیکیشن‌ها را توسعه‌پذیر و ساده‌تر کند.

چرا شیطان در جزئیات است؟

همانطور که اشاره کردیم، از آن‌جایی که کدبیس میراث تیم دیگری بود و دانش کاملاً دقیقی از آن در تیم توسعه وجود نداشت، برای تخمین بررسی اوّلیه‌ای از کد انجام دادیم.

با توجّه به وجود کتابخانه i18n در کد و جدا بودن متن‌ها در یک فایل به خصوص، به نظر رسید که بخشی از کار آماده است و کار راحتی پیش‌رو داریم.

بنابراین با نگاه به بخش‌های موجود، کار باقی‌مانده را به چهار بخش تقسیم کردیم:

۱. پیاده‌سازی تغییر زبان با استفاده از کتابخانه i18n

۲. ترجمه متن‌ها برای زبان جدید

۳. اصلاح فرمت تاریخ و اعداد

۴. دوجهته کردن سایت

به نظر می‌رسید که انجام بخش اوّل و سوم ساده است. با توجّه به این‌که کتابخانه i18n در پروژه وجود داشت، حدس می‌زدیم زمان زیادی لازم نیست.

ترجمه متن‌ها هم جز‌ء وظایف تیم فنی نبود.

برای بخش آخر هم با یک نگاه کلّی به CSSهای پروژه فکر می‌کردیم که این بخش کار زیادتری دارد ولی تصوّر این را نداشتیم که با چالش زیادی رو‌به‌رو شویم. با توجّه به این ذهنیّت کار را شروع کردیم امّا داستان به همین سادگی که فکر می‌کردیم پیش نرفت.

تخمین ما برای این قسمت دو روز کاری بود که در عمل حدود دو هفته طول کشید!

علاوه بر صرف زمان زیادتر، بخش‌های زیادی از کد تغییر کرد. تغییرات بسیار زیاد باعث بهم‌ریختگی و ایجاد باگ‌های جدید شد که پیدا کردن و رفع آن‌ها نیز زمان‌بر بود. این بدترین بخش کار بود ولی مشکل فقط به همین‌جا ختم نمی‌شد.

متوجّه شدیم حتّی ترجمه متن‌ها هم که کار تیم ما نبود، به خوبی پیش نمی‌رود. باید چندین هزار جمله و عبارت ترجمه می‌شد. ویرایش ترجمه‌ها در یک فایل چندهزار خطی قابل تقسیم به نفرات بیشتری نبود و برای پیشگیری از دوگانگی‌ها کل کار به یک نفر سپرده شده بود.

بخش‌های مختلف چندزبانه کردن

به شکل کلّی، چندزبانه کردن یک وب‌اپلیکیشن را می‌توانیم به سه بخش «سازمان‌دهی متن‌ها»، «قالب‌بندی اعداد، تاریخ و نوشته‌ها» و «استایل» تقسیم‌بندی کنیم. در ادامه به بررسی هر بخش و نکاتی که اگر در پروژه رعایت شده بود می‌توانستیم سریع‌تر و بهتر کار را به اتمام برسانیم می‌پردازیم.

۱. سازمان‌دهی متن‌ها

برای سازماندهی متن‌ها در زبان‌های مختلف، ابزارها و کتابخانه‌های زیادی وجود دارد که کارهای مربوط به چندزبانه‌سازی را انجام می‌دهد. با توجّه به چهارچوب توسعه‌ای که پروژه شما از آن استفاده می‌کند، می‌توانید ابزار مناسب را انتخاب کنید.

البته این ابزارها علاوه بر سازمان‌دهی متن‌ها، کارهای دیگری مرتبط با چندزبانه کردن انجام می‌دهند که معمولاً این کارها توسط شئ intl زبان جاوااسکریت قابل پیاده‌سازی است (در عنوان بعدی توضیح داده شده است).

چند فایل به جای یک فایل

در پروژه‌های بزرگ بهتر است که فایل متن‌ها را به چند فایل بشکنید. اساس این تقسیم‌بندی می‌تواند بر مبنای بخش‌های مختلف منطق کسب و کار، یا بر اساس کامپوننت‌های اصلی، یا به هر شکلی که ترجیح می‌دهید باشد.

این جداسازی علاوه بر این که باعث می‌شود که در یک فایل با چندین هزار خط دنبال چیزها نگردید، کمک می‌کند هنگام توسعه یا پیاده‌سازی زبان جدید بتوانید فرایند ترجمه را به شکل موازی به چندین نفر بسپارید.

جلوگیری از استفاده از متن بدون نشانه‌گذاری در کامپوننت‌ها

بودن متن‌هایی در بخش‌هایی از کد که با استفاده از کتابخانه‌های سازمان‌دهی متن مشخص نشده باشند، بعداً شما را دچار دردسر خواهد کرد و باید زمان زیادی را دنبال این متن‌ها بگردید.

حین توسعه می‌توان با فرایند مرور دقیق Merge Request‌ها یا با استفاده از linter‌هایی (مثل eslint-plugin-i18next) به شکل خودکار جلوی اضافه شدن متن بدون نشانه‌گذاری را گرفت.

راه‌کارهای ترجمه پیوسته

اگر تعداد جمله‌ها و متن‌های شما زیاد است، یا قرار است ترجمه‌ها توسط مترجمین حرفه‌ای یا مشارکت‌ کننده‌های داوطلب تغییرات زیادی داشته باشند، می‌توانید ابزارهای ترجمه پیوسته‌ای مثل Mojito را در اختیار آن‌ها قرار دهید.

۲. قالب‌بندی تاریخ، اعداد، و نوشته‌ها

برای انجام این کار می‌توانید از شئ intl استفاده کنید که به شکل پیش‌فرض در خود جاوااسکریپت وجود دارد و نیاز به ابزارهای شخص ثالث را رفع می‌کند. این شئ constructorهای زیادی دارد که در اینجا به دو مورد پرکاربرد آن اشاره می‌کنیم. سایر امکانات آن را می‌توانید از مستندات MDN مشاهده کنید.

Intl.NumberFormat

پرکاربردترین کانستراکتور intl که می‌توانید برای قالب‌بندی اعداد، واحدها و قیمت استفاده کنید. مثلاً:

let amount = 3500;

console.log(new Intl.NumberFormat('fa').format(amount));
// →۳٬۵۰۰ if in fa locale

new Intl.NumberFormat('en-US', {style: 'percent'}).format(amount);
// → '350,000%'

new Intl.NumberFormat('fa', {
  style: 'currency',
  currency: 'IRR',
  currencySign: 'accounting',
  signDisplay: 'always'
}).format(3500)
// "‎+‎ریال ۳٬۵۰۰"
Intl.DateTimeFormat

از این Constructor برای قالب‌بندی تاریخ استفاده می‌شود که با توجّه به زبان و گزینه‌هایی که در ورودی می‌گیرد تاریخ و زمان را نمایش می‌دهد.

const date = new Date();

// قالب بندی پیش‌فرض در زبان فارسی
console.log(new Intl.DateTimeFormat('fa').format(date));
// Expected output: "۱۴۰۰/۹/۱۱"

برای قالب‌بندی تاریخ و زمان کتابخانه Moment.js هم ابزارهای خوبی را فراهم کرده است. پروژه ما از ابتدا با moment.js پیاده شده بود، بنابراین برای حفظ سرعت توسعه تغییری در این قسمت ایجاد نکردیم.

در تصویر زیر پشتیبانی مرورگرهای مختلف از intl را مشاهده می‌کنید. اگر نگران نسخه‌های خیلی قدیمی نباشید، پشیبانی تقریباٌ کاملی از مرورگرها داریم.

پشتیبانی مرورگرهای مختلف از Internationalization API

۳. استایل‌ها

در فرایند چندزبانه کردن، CSS‌ها بیشترین جزئیات را در بین بخش‌های مختلف دارد و علی رغم اهمیّت آن، معمولاً توجّه زیادی به آن نمی‌شود. طبق تجربه‌ای که ما داشتیم اگر یکسری از نکات در کد مد نظر قرار می‌گرفت، فرایند توسعه خیلی سریع‌تر پیش می‌رفت. جدا از سرعت توسعه، اصلاح CSS‌ها مخصوصاً وقتی که با یک پروژه بزرگ سر و کار داشته باشیم کاری خسته‌کننده و فرسایشی است.

در ادامه به نکاتی اشاره خواهیم کرد که بهتر است در توسعه وب‌اپلکیشن رعایت شوند تا چندزبانه کردن بدون درد باشد.

استفاده از Logical Properties و Logical Values در CSS

Logical Properties و Logical Values این امکان را فراهم می‌کند که به جای استفاده از جهت‌های فیزیکی در طراحی layout، از جهت‌های منطقی استفاده کنیم.

به عنوان مثال فرض کنید یک پاراگراف داریم که می‌خواهیم محتوای آن در خلاف جهتِ طبیعی آن زبان باشد، یعنی برای انگلیسی در سمت راست و برای فارسی در سمت چپ قرار گیرد. (در زبان انگلیسی direction: ltr و در فارسی از direction: rtl استفاده کرده ایم)، پس باید چنین کاری انجام دهیم:

<article>
  <p class="opposite">
    Lorem ipsum dolor sit amis ..
  </p>
</article>

و در فایل CSS :

.opposite {
  text-align: right;
}

برای نسخه RTL باید استایل این کلاس را به این شکل بازنویسی کنیم:

html[dir="rtl"] .opposite {
  text-align : left;
}

این کار باعث می‌شود که مجبور باشیم برای نسخه‌های RTL و LTR دو استایل متفاوت تعریف کنیم.

برای حل این مشکل Logical Properties و Logical Values ساخته شده‌اند و می‌توانیم به جای استفاده از جهت‌ها، از مقادیر منطقی استفاده کنیم.

به زبان ساده در المان‌های LTR مقدار left به معنی آغاز یا start آن المان است در حالی که در المان RTL، مقدار right به معنی start است.

بنابراین به جای هر آنچه قبلا نوشته بودیم می‌توانیم از Logical Values استفاده کنیم:

.opposite {
   text-align: end
}

چیزی که در بالا دیدیم کاربرد Logical Values یا مقادیر منطقی بود.

حالا ببینیم Logical Propertyها چه چیزی است. ویژگی‌های منطقی، ویژگی‌های جدیدی هستند که با همین ایده استفاده از جهت‌های منطقی پیاده‌سازی شده‌اند و بر اساس جهت چینش المان‌ها کار می‌کنند. برای مثال margin را ببینیم.

قبلا برای اینکه یک فضای خالی در شروع یک المان ایجاد کنیم، در حالت LTR:

p {
    margin-left: 20px
}

و حالا در نسخه RTL به margin‌ای در جهت مخالف نیاز داریم، به علاوه اینکه باید مقدار margin-left را هم reset کنیم.

 p {
  margin-left: 0;
  margin-right: 20px;
}

ولی با استفاده از Logical Propertyها می‌توانیم به سادگی پیاده‌سازی را به این شکل تغییر دهیم:

p {
  margin-inline-start: 20px;
}

وقتی قسمت -inline-start به margin اضافه می‌شود این margin به ابتدای المان در جهت افقی اعمال می‌شود که در RTL معادل margin-right و در LTR معادل margin-left است.

به همین شکل برای جهت مخالف هم می‌توانیم از پسوند -inline-end استفاده کنیم. همچنین برای جهت های بالا و پایین هم پسوند‌های -block-start و -block-end را داریم که می‌توانیم به propertyهای جهت‌دار اضافه کنیم.

Logical Properties
Logical Properties
padding و marginهای منطقی

حالا که با Logical Propertyها آشنا شدیم می‌دانیم که به جای padding-left و padding-right بهتر است از padding-inline-start و padding-inline-end و به جای margin-right و margin-left از margin-inline-start و margin-inline-end استفاده کنیم.

وقتی اپ شما راست‌چین است یعنی المان‌ها از راست به چپ چیده می شوند پس margin-inline-start به معنی margin-right است و در صورت چپ‌چین شدن که المان‌ها از چپ به راست چیده می‌شوند margin-inline-start به معنی margin-left است و دیگر نیازی نیست این استایل‌ها را برای جهت‌‌های مختلف بازنویسی کنید.

در جدول زیر Propertyها و Logical Propertyهای مترادف آنها را برای margin می‌بینید:

Logical PropertyProperty
margin-block-startmargin-top
margin-inline-startmargin-left
margin-inline-endmargin-right
margin-block-endmargin-bottom
Propertyهای margin و معادل منطقی آن‌ها

و برای padding هم به شکل مشابه داریم:

Logical PropertyProperty
padding-block-startpadding-top
padding-inline-startpadding-left
padding-inline-endpadding-right
padding-block-endpadding-bottom
Propertyهای padding و معادل منطقی آن‌ها

همچنین می‌توانیم با استفاده از کلمه کلیدی logical به شکل کوتاه‌شده از این مقادیر استفاده کنیم:

selector {
  margin: logical 10px 20px 30px 40px;
}
Logical Positioning Properties

بالاتر دیدیم که نام propertyها با پسوندهایی معنی‌دار اصلاح می‌شوند ولی در position‌ها نام propertyها کامل تغییر می‌کند:

selector {
  position: absolute;
  inset-block-start: 0;  /* فاصله از بالا */
  inset-block-end: 0;    /* فاصله از پایین */
  inset-inline-start: 0; /* فاصله از چپ در چپ‌چین و فاصله از راست در راست‌چین */
  inset-inline-end: 0;   /* فاصله از راست در چپ‌چین و فاصله از چپ در راست‌چین  */
}

به شکل ساده، برای حالت کوتاه شده هم می‌توانیم از inset استفاده کنیم:

selector {
  position: absolute;
  inset: logical 10px 20px 30px 40px;
}

که این کد معادل مقادیر زیر است:

selector {
  position: absolute;
  inset-block-start: 10px;
  inset-inline-start: 20px;
  inset-block-end: 30px;
  inset-inline-end: 40px;
}
Logical border properties

ویژگی border هم با استفاده از پسوندهای -inline-start و -block-start به ویژگی‌های منطقی تبدیل می‌شوند:

Logical PropertyProperty
border-block-start{-size|style|color}border-top{-size|style|color}
border-inline-start{-size|style|color}border-left{-size|style|color}
border-inline-end{-size|style|color}border-right{-size|style|color}
border-block-end{-size|style|color}border-bottom{-size|style|color}
Propertyهای border و معادل منطقی آن‌ها
float

اگر هنوز برای چیدن افقی المان‌ها از float استفاده می‌کنید بهتر است فراموشش کنید. سعی کنید flex را جایگزین کنید تا دیگر مجبور نباشید برای جهت‌های مختلف استایل‌ها را بازنویسی کنید.

پشتیبانی مرورگرها از Logical Properties

همان‌طور که در شکل می‌بینید مرورگر IE از ویژگی‌های منطقی پشتیبانی نمی‌کند. مرورگر Chrome در نسخه‌های ۴ تا ۶۸ با پیشوند -webkit-، و مرورگر Firefox در نسخه‌های ۳ تا ۴۰ با پیشوند -moz- از برخی از Logical Propertyها پشتبانی می‌کنند (برای جزئیات بیشتر سایت caniuse را ببینید.). در Chrome از نسخه ۸۹ و در Firefox از نسخه ۴۱ به شکل کامل پشتیبانی می‌شود.

ساده امّا نجات‌بخش

مواردی که اشاره کردیم، کارهای ساده‌ای هستند که اگر از ابتدا مورد توجه قرار بگیرند توسعه‌پذیری را به طور چشم‌گیری بهبود می‌بخشند. بنابراین اگر قصد پیاده‌سازی یک اپلیکیشن چندزبانه را دارید یا در چشم‌انداز پروژه چندزبانه شدن وجود دارد از ابتدا این موارد را در نظر بگیرید.

در این نوشته، سعی کردیم براساس تجربه تبدیل یک پروژه فرانت‌اند فارسی به یک پروژه چندزبانه، به مواردی که به نظرمان مهم‌تر آمد بپردازیم. تجربه شما در این زمینه چیست؟ آیا موارد دیگری هم به ذهن شما می‌رسد که در حین تجربه‌های مشابه با آن‌ها برخورد کرده باشید؟ اگر تجربه مشابهی دارید یا نکات دیگری به ذهنتان می‌رسد خوشحال می‌شویم که دیدگاه خود را به اشتراک بگذارید.

از زهرا کبیری

توسعه‌دهنده فرانت‌اند

2 دیدگاه دربارهٔ «چندزبانه‌ کردن وب‌اپلیکیشن‌ها بدون درد و خونریزی»

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *