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

در میان فایل های مختلف لاراول (فریم ورکی برای 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_TW


2- حالت جمع و مفرد

در این زبان ها حالت مفرد (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 : 1

3- حالت هیچ و مفرد در مقابل حالت جمع

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

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 : 1

4- زبان های شبیه روسی

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 : 1

10- زبان مالتی

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))))

حرف آخر

در این نوشته، کمی درباره ی یک نکته ی کوچک در لاراول کنجکاوی کردیم: چطور در سایت های چندزبانه ی خود مسائل مربوط به جمع بستن را بررسی کنیم. در نهایت یک طبقه بندی از زبان های جهان بر اساس قوانین جمع بستن ارائه دادیم.