دسته بندی زبان‌ها بر اساس قوانین جمع بستن: درسی از لاراول و زند

در میان فایل های مختلف لاراول (فریم ورکی برای php) می گشتم که به متد getPluralIndex از کلاس MessageSelector رسیدم. در مسیر زیر:vendor/laravel/framework/src/illuminate/Translation/MessageSelector.phpدر توضیح این متد نوشته شده: «گرفتن اندیس مورد استفاده برای جمع بستن». اما قضیه چیست؟مختصری درباره ی چندزبانگی در لاراولبرای فهمیدن قضیه باید به مستندات لاراول برای جمع بستن (در بخش محلی سازی یا همان ترجمه) مراجعه کنیم. اما قبلش روش چندزبانه کردن سایت در لاراول را مرور کنیم. در لاراول برای چندزبانه کردن سایت، می‌توانیم در پوشه ی resources/lang پوشه ی زبان مد نظر خود را اضافه کنیم و برای هر زبان ترجمه ی عبارت‌ها را بنویسیم.نمونه ساختار پوشه ی زبان ها در لاراول. به مشابهت نام فایل ها در پوشه ی زبان فارسی و انگلیسی توجه کنید.در تصویر زیر می بینید که دو فایل موازی ترجمه را باز کرده ایم. در هر فایل چند کلید داریم که به یک عبارت متصل شده اند. در این جا می بینید که عبارت مربوط به دو کلید در فارسی و انگلیسی ترجمه شده اند اما یک کلید (password) در فارسی ترجمه ای ندارد.مقایسه ی دو فایل موازی ترجمه. سمت راست فایل فارسی و سمت چپ فایل انگلیسی (یکی از عبارات در فارسی ترجمه نشده).بعد از انجام این ترجمه ها، به راحتی در برنامه ی خود می توانیم از آنها استفاده کنیم. در مثال تصویر بالا، می توانیم برای چاپ عبارت مربوط به نادرست بودن اطلاعات، این کد را بنویسیم:echo __('auth.failed');می بینید که از تابع __() استفاده کردیم و نام فایل و نام کلید را به آن دادیم. حالا کافی است زبان انتخابی کاربر را تشخیص دهیم تا لاراول به صورت خودکار، عبارت مربوط به آن را به او نشان دهد.جمع بستن در لاراولترجمه در لاراول جزئیات بیشتری هم دارد اما در این جا می خواهیم درباره ی یک موضوع خاص صحبت کنیم: جمع بستن. طبق مثال های مستندات خود لاراول پیش می رویم. فرض کنید یک کلید به نام apples داریم و می خواهیم یک عبارت را به آن نسبت دهیم. اما عبارت این کلید هم حالت جمع دارد هم حالت مفرد. پس به صورت زیر تعریفش می کنیم و حالت جمع و مفرد را با «|» جدا می کنیم (در بخش بعدی می گوییم که لاراول چطور می فهمد کدام عبارت را انتخاب کند):'apples' => 'There is one apple|There are many apples',می توانیم به لاراول بگوییم اگر چند سیب داشتیم، چه عبارتی را انتخاب کن. مثل کد زیر که در آن گفته ایم اگر هیچ سیبی نداشتیم یک عبارت خاص، اگر بین 1 تا 19 سیب داشتیم، عبارتی دیگر و اگر بیش از 20 سیب داشتیم از عبارت سوم استفاده کن:'apples' => '{0} There are none|[1,19] There are some|[20,*] There are many',برای استفاده کردن از چنین ترجمه ای در برنامه ی خود باید چنین کدی را بنویسیم:echo trans_choice('messages.apples', 10);در این کد با استفاده از تابع trans_choice() گفته ایم که پیام مربوط به apples را برای 10 سیب چاپ کن. طبق این کد و کد قبلی، الان می‌دانیم که در این حالت چه عبارتی چاپ می شود.همچنین می توانیم مقدار یک نگه دارنده (placeholder) را هم در عبارت ترجمه شده بنویسیم. این کار با استفاده از ورودی (آرگومان) سوم تابع trans_choice انجام می شود://in translation file: 'minutes_ago' => '{1} :value minute ago|[2,*] :value minutes ago', //in our program: echo trans_choice('time.minutes_ago', 5, ['value' => 5]);در کد بالا، value را با مقدار 5 به ترجمه ی خود داده ایم و در ترجمه با نوشتن :value آن را چاپ کرده ایم.حالا اگر به جای پاس دادن value بخواهیم، مقدار آرگومان دوم تابع trans_choice را در ترجمه ی خود بیاوریم، می توانیم از نگه دارنده ی پیش فرض :count استفاده کنیم:'apples' => '{0} There are none|{1} There is one|[2,*] There are :count',می توانید حدس بزنید که اگر مثلن مقدار 10 به تابع پاس داده شد، چه عبارتی چاپ می شود؟قوانین جمع بستن زبان های مختلف در لاراولاما همه ی زبان ها، مثل انگلیسی نیستند که یک حالت جمع و یک حالت مفرد داشته باشند. از درس‌های عربی در دبیرستان به یاد داریم که عربی، حالت مثنا (دو تایی) هم داشت. اما قضیه باز هم به این سادگی نیست. :) به تصویر زیر که از صفحه ی قوانین جمع بستن در زبان ها برداشتم، دقت کنید:قوانین جمع بستن اعداد اصلی در عربیمی بینید که اگر 3 تا 10 چیز یا 103 تا 110 چیز داشته باشیم، به یک روش جمع می بندیم و اگر 11 تا 99 یا 111 تا 199 چیز داشته باشیم، به روشی دیگر. حالا اگر قرار باشد در برنامه ی خود زبان عربی را اضافه کنیم و چندین عبارت داشته باشیم که جمع هم بسته می شوند، آیا لازم است که هر بار یک مجموعه شرط بنویسیم؟مشکل اول این است که لاراول در فایل های ترجمه، امکان نوشتن شرط های پیچیده را به ما نمی دهد. مشکل دوم این که چرا باید برای قوانین ثابت جمع بستن، مدام شرط بنویسیم؟این جاست که لاراول کار ما را راحت کرده است. متدی را که در ابتدای این نوشته گفتم یادتان هست؟ این متد دو آرگومان دریافت می کند: کد زبان و تعداد. بعد بررسی می کند که این تعداد در آن زبان چطور بیان می شود. فرض کنید در فایل ترجمه ی خود چنین خطی را نوشته ایم:'apples' => 'There is one apple|There are many apples',لاراول از کجا می فهمد که در حالت مفرد باید عبارت اول را انتخاب کند و در حالت جمع عبارت دوم را؟ متد getPluralIndex وظیفه ی تشخیص را برعهده دارد. عبارت بالا یک عبارت انگلیسی است. فرض کنید می خواهیم عبارت مربوط به تعداد 20 سیب را چاپ کنیم. پس لاراول کد زبان انگلیسی (en) و تعداد 20 را به این متد پاس می دهد. این متد شامل یک switch-case بزرگ بر اساس کد زبان (locale) است. در بخشی از این سوئیچ-کیس نوشته شده:case 'en': return ($number == 1) ? 0 : 1;در این کد گفته شده که اگر زبان انگلیسی بود و تعداد پاس داده شده، برابر یک بود، عبارت اول (عبارت صفر از آرایه) انتخاب شود و در غیر این صورت عبارت دوم (عبارت یک از آرایه). جداسازی عبارت های ترجمه بر اساس «|»، کد زبان و تعداد پاس داده شده بر عهده ی متد choose() در همین کلاس است:public function choose($line, $number, $locale) { $segments = explode('|', $line); if (($value = $this->extract($segments, $number)) !== null) { return trim($value); } $segments = $this->stripConditions($segments); $pluralIndex = $this->getPluralIndex($locale, $number); if (count($segments) === 1 || ! isset($segments[$pluralIndex])) { return $segments[0]; } return $segments[$pluralIndex]; }می بینید که در این متد از getPluralIndex برای تشخیص اندیس جمع بستن (شماره عبارتی که باید انتخاب شود) استفاده شده است.در توضیحات متد getPluralIndex نوشته شده که این متد در اصل از فریم ورک زند (Zend) گرفته شده است. اگر به مستندات زند مراجعه کنیم می بینیم که زند هم روشی مشابه برای این کار دارد:$translate = new Zend_Translate( array( 'adapter' => 'gettext', 'content' => '/path/to/german.mo', 'locale' => 'de' ) ); $translate->translate( array( 'Car', 'Cars first plural', 'Cars second plural', $number, 'ru' ) );در کد بالا، کد زبان روسی (ru) به متد translate از شیء ساخته شده از کلاس Zend_Translate پاس داده شده و بر اساس این زبان، دو حالت جمع مختلف به این متد پاس داده شده است.دسته بندی زبان ها بر اساس قوانین جمع بستنحالا بر اساس switch-case نوشته شده در متد جمع بستن، بیایید زبان ها را دسته بندی کنیم. دقت کنید که قوانین در این دسته بندی ها، فقط می گوید بر اساس تعداد چیزها، چند حالت ترجمه وجود دارد اما نمی گوید که هر حالت ترجمه به چه شکل است (از نظر نوع کلمات، تطابق فعل و فاعل و...).1- بدون حالت جمعدر این زبان ها حالت جمع و مفرد به یک شکل ترجمه می شود:az, az_AZ, bo, bo_CN, bo_IN, dz, dz_BT, id, id_ID, ja, ja_JP, jv, ka, ka_GE, km, km_KH, kn, kn_IN, ko, ko_KR, ms, ms_MY, th, th_TH, tr, tr_CY, tr_TR, vi, vi_VN, zh, zh_CN, zh_HK, zh_SG, zh_TW2- حالت جمع و مفرددر این زبان ها حالت مفرد (1 چیز) از حالت جمع (بیش از 1 چیز) جدا می شود. زبان آلمانی، انگلیسی، اسپانیایی، فارسی، فنلاندی و سوئدی جزو این حالت هستند.af, af_ZA, bn, bn_BD, bn_IN, bg, bg_BG, ca, ca_AD, ca_ES, ca_FR, ca_IT, da, da_DK, de, de_AT, de_BE, de_CH, de_DE, de_LI, de_LU, el, el_CY, el_GR, en, en_AG, en_AU, en_BW, en_CA, en_DK, en_GB, en_HK, en_IE, en_IN, en_NG, en_NZ, en_PH, en_SG, en_US, en_ZA, en_ZM, en_ZW, eo, eo_US, es, es_AR, es_BO, es_CL, es_CO, es_CR, es_CU, es_DO, es_EC, es_ES, es_GT, es_HN, es_MX, es_NI, es_PA, es_PE, es_PR, es_PY, es_SV, es_US, es_UY, es_VE, et, et_EE, eu, eu_ES, eu_FR, fa, fa_IR, fi, fi_FI, fo, fo_FO, fur, fur_IT, fy, fy_DE, fy_NL, gl, gl_ES, gu, gu_IN, ha, ha_NG, he, he_IL, hu, hu_HU, is, is_IS, it, it_CH, it_IT, ku, ku_TR, lb, lb_LU, ml, ml_IN, mn, mn_MN, mr, mr_IN, nah, nb, nb_NO, ne, ne_NP, nl, nl_AW, nl_BE, nl_NL, nn, nn_NO, no, om, om_ET, om_KE, or, or_IN, pa, pa_IN, pa_PK, pap, pap_AN, pap_AW, pap_CW, ps, ps_AF, pt, pt_BR, pt_PT, so, so_DJ, so_ET, so_KE, so_SO, sq, sq_AL, sq_MK, sv, sv_FI, sv_SE, sw, sw_KE, sw_TZ, ta, ta_IN, ta_LK, te, te_IN, tk, tk_TM, ur, ur_IN, ur_PK, zu, zu_ZA:قانون:($number == 1) ? 0 : 13- حالت هیچ و مفرد در مقابل حالت جمعدر این زبان ها، حالت هیچ چیز و مفرد به یک شکل و حالت جمع به شکل دیگر ترجمه می شود. زبان فرانسوی در این حالت است.am, am_ET, bh, fil, fil_PH, fr, fr_BE, fr_CA, fr_CH, fr_FR, fr_LU, gun, hi, hi_IN, hy, hy_AM, ln, ln_CD, mg, mg_MG, nso, nso_ZA, ti, ti_ER, ti_ET, wa, wa_BE, xbrقانون:(($number == 0) || ($number == 1)) ? 0 : 14- زبان های شبیه روسیbe, be_BY, bs, bs_BA, hr, hr_HR, ru, ru_RU, ru_UA, sr, sr_ME, sr_RS, uk, uk_UAقانون:(($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2)5- زبان های چکی و اسلواکیاییcs, cs_CZ, sk, sk_SKقانون:($number == 1) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2)6- زبان ایرلندیga, ga_IEقانون:($number == 1) ? 0 : (($number == 2) ? 1 : 2)7- زبان لیتوانیاییlt, lt_LTقانون:(($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2)8- زبان اسلوونیاییsl, sl_SIقانون:($number % 100 == 1) ? 0 : (($number % 100 == 2) ? 1 : ((($number % 100 == 3) || ($number % 100 == 4)) ? 2 : 3))9- زبان مقدونیه ایmk, mk_MKقانون:($number % 10 == 1) ? 0 : 110- زبان مالتیmt, mt_MTقانون:($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3))11- زبان لتونیاییlv, lv_LVقانون:($number == 0) ? 0 : ((($number % 10 == 1) && ($number % 100 != 11)) ? 1 : 2)12- زبان لهستانیpl, pl_PLقانون:($number == 1) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2)13- زبان ولزیcy, cy_GBقانون:($number == 1) ? 0 : (($number == 2) ? 1 : ((($number == 8) || ($number == 11)) ? 2 : 3))14- زبان رومانیاییro, ro_ROقانون:($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2)15- زبان عربیar, ar_AE, ar_BH, ar_DZ, ar_EG, ar_IN, ar_IQ, ar_JO, ar_KW, ar_LB, ar_LY, ar_MA, ar_OM, ar_QA, ar_SA, ar_SD, ar_SS, ar_SY, ar_TN, ar_YEقانون:($number == 0) ? 0 : (($number == 1) ? 1 : (($number == 2) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5))))حرف آخردر این نوشته، کمی درباره ی یک نکته ی کوچک در لاراول کنجکاوی کردیم: چطور در سایت های چندزبانه ی خود مسائل مربوط به جمع بستن را بررسی کنیم. در نهایت یک طبقه بندی از زبان های جهان بر اساس قوانین جمع بستن ارائه دادیم.