قابلیت چند زبانی در اپلیکیشن ری اکت با جاوا اسکریپت — از صفر تا صد
چندزبانه ساختن وبسایتها در زمانهای قدیم مشکلی جدی محسوب میشد. اگر میخواستید ترجمهها بدون نیاز به رفرش صفحه ظاهر شوند، باید مسیری بسیار طولانی را طی میکردید. خوشبختانه امروز گزینههای زیادی برای پیادهسازی قابلیت چند زبانی وجود دارند. هر هفته کتابخانههای جدیدی معرفی میشوند و صرفاً یک فریمورک منفرد وجود ندارد، لذا در زمینه انتخاب ابزار مناسب خود با محدودیت مواجه نخواهید شد. در این مقاله با روش افزودن قابلیت چند زبانی در اپلیکیشن ری اکت با جاوا اسکریپت صرف آشنا خواهیم شد.
در ادامه قصد داریم به بررسی شیوه پیادهسازی یک سرویس بومیسازی و سفارشیسازی نیازهای خود بدون نیاز به وابستگی بیرونی بپردازیم. تنها چیزی که نیاز داریم یک اپلیکیشن ریاکت و مقداری زمان است. بنابراین بدون این که زمان را از دست بدهیم، دست به کار میشویم.
بوتاسترپ کردن ریاکت
ما نمیخواهیم یک اپلیکیشن ریاکت را از صفر بسازیم، اما لازم است که همه چیز را از آغاز در اختیار داشته باشیم و از این رو از اسکریپت create-react-app (+) کمک میگیریم.
برای بوتاسترپ کردن یک اپلیکیشن باید دستور زیر را اجرا کنید:
npx create-react-app i18n
در دستور فوق منظور از i18n نام پوشه است. npx از نسخه npm 5.2 به بعد معرفی شده است و از این رو باید آن را نیز روی سیستم خود نصب داشته باشید. اگر create-react-app را به صورت سراسری نصب کردهاید، آن را با دستور زیر لغو نصب کنید تا مطمئن شوید که npx از جدیدترین نسخه استفاده میکند:
uninstall -g create-react-app
با اجرای دستور زیر باید با صفحه زیر مواجه شوید:
npm run start
ایجاد سرویس
کار خود را با افزودن برخی پوشهها و فایلها به طرح کلی ساختار پروژه آغاز میکنیم. صرفاً برای این که همه چیز متمایز بماند، یک پوشه جدید برای سرویس ایجاد میکنیم و نام آن را localizationService میگذاریم.
همچنین پوشه i18n را اضافه میکنیم که همه کلیدهای بومیسازی را در آن میگنجانیم و همه زبانهای مورد نظر در پوشه مجزای خود قرار میگیرند.
هر فایل زبان یک شیء اکسپورت میکند که همه رشتههای مورد استفاده در اپلیکیشن را نگهداری خواهد کرد:
1export default {
2 'learnReact': 'Learn React'
3};
درون کامپوننتها از کلیدها و با بهرهگیری از میانیابی به صورت زیر استفاده میکنیم:
1{i18n('learnReact')}
کد فوق در صورتی که سایت از زبان انگلیسی استفاده کند، به صورت عبارت Learn React تحلیل میشود. در صورت استفاده از فایل hu.js، ترجمه «مجاری» (Hungarian) عبارت نمایش خواهد یافت. بنابراین برای شروع باید فایل زبان را در سرویس ایمپورت کنیم. میتوانیم شیئی بسازیم که همه زبانها را نگهداری میکند تا مطمئن شویم که کلیدها در اختیار پنجره قرار میگیرند:
1import en from '../i18n/en'
2import hu from '../i18n/hu'
3
4const languages = {
5 en,
6 hu
7};
8
9let defaultLanguage = window.navigator.language === 'en' ? 'en' : 'hu';
10
11window.i18nData = languages[defaultLanguage];
همچنین یک متغیر به نام defaultLanguage اضافه میکنیم که زبان مرورگر را بررسی میکند. اگر این زبان انگلیسی باشد، i18nData را با مقادیر انگلیسی پر میکنیم و در غیر این صورت از زبان مجاری استفاده میکنیم. برای دریافت یک مقدار بومیسازی شده برای یک کلید مفروض، میتوانیم تابع i18n را اضافه کنیم. این تابع در سادهترین شکل خود تنها رشتهای بر مبنای کلید ارائهشده بازگشت میدهد:
1window.i18n = (key) => window.i18nData[key];
استفاده از سرویس در کامپوننتها
برای بررسی سرویس در عمل دو دکمه زیر لینک Learn React اضافه میکنیم که برای سوئیچ بین زبانها استفاده میشوند:
1<img src="https://bit.ly/2NR57Sj" alt="en" data-language="en" onClick={this.changeLanguage} />
2<img src="https://bit.ly/36C7DV5" alt="hu" data-language="hu" onClick={this.changeLanguage} />
با کلیک روی تصویر متد changeLanguage فراخوانی میشود که data-language را به آن ارسال میکنیم تا تصمیم بگیرد کدام زبان باید فعال شود. سپس باید کامپوننت را رندر مجدد بگیریم تا تغییر نمایان شود. در حال حاضر App ما یک کامپوننت تابعی بیحالت (Stateless) است، از این رو به this.forceUpdate که برای الزام به رندر مجدد استفاده میشود، دسترسی نداریم. برای اصلاح این مسئله تابع App را به یک کلاس تبدیل میکنیم و سپس متد changeLanguage را اضافه میکنیم:
1import './services/localizationService';
2
3class App extends React.Component {
4
5 changeLanguage = (e) => {
6 window.changeLanguage(e.target.dataset.language);
7 this.forceUpdate();
8 }
9
10 render() {
11 return (
12 ...
13 );
14 }
15}
ضمناً سرویس بومیسازی برای استفاده درون کامپوننت ایمپورت میشود. ما هنوز window.changeLanguage را تعریف نکردهایم، از این رو به localizationService بازمیگردیم و آن را با تابع زیر بسط میدهیم:
1window.changeLanguage = (lang) => {
2 window.i18nData = languages[lang];
3}
در کد فوق i18nData را به زبان ارسالی به صورت پارامتر مجدداً انتساب دادهایم. برای امتحان کردن آن در کامپوننت، Learn React را با {i18n(‘learnReact’)} عوض کنید. اگر اپلیکیشن را با create-react-app بوتاسترپ کرده باشید، ESLint خطایی به خاطر استفاده از یک متغیر تعریفنشده نمایش میدهد. برای پیکربندی صحیح آن میتوانید دستور npm run eject را اجرا کنید تا به فایلهای پیکربندی دسترسی پیدا کرده و یک قاعده درون globals در فایل .eslintrc اضافه کنید:
1{
2 "globals": {
3 "i18n": false
4 }
5}
همچنین میتوانید برای حذف آن بدون کار اضافی، خط زیر را به ابتدای فایل خود اضافه کنید:
/* global i18n */
اکنون میتوانید changeLanguage را فراخوانی کنید تا ترجمه درون i18nData تغییر یابد:
آن را با یک بهروزرسانی الزامی ترکیب میکنیم تا رندر مجدد آغاز شود و موجب تغییر یافتن زبان سایت شود:
افزودن پشتیبانی پارامتر
هم اینک سرویس کاملاً در حالت ابتدایی خود قرار دارد و تنها میتواند رشتههای استاتیک بازگشت دهد. اما اگر بخواهیم پارامتر داشته باشیم چطور؟ فرض کنید رشتهای داریم که شرایط آب و هوا را نمایش میدهد.
برای مثال فرض کنید رشتهای به صورت زیر داریم:
Today it’s 32 degrees
بدیهی است که دمای هوا همیشه 32 درجه نیست. این دما میتواند تغییر یابد و از این رو باید آن را به صورت پارامتر ارسال کنیم. برای نمونه به مثال زیر توجه کنید:
1{i18n(‘weatherCondition’, 32)}
برای تشخیص این که پارامترها در کجای رشته تزریق شدهاند، باید یک ساختار خاصی برای «میانیابی» (interpolation) داشته باشیم. اغلب موتورهای قالبسازی از آکولاد استفاده میکنند، از این رو میتوانیم از این رسم سنتی بهره بگیریم. برای ایجاد امکان درج پارامترهای چندگانه، میتوانیم آنها را شمارهگذاری کنیم که از 0 آغاز میشوند. بنابراین پارامترها با {0}, {1}, {2} و غیره نمایش مییابند.
برای این که از مثال قبلی دور نشویم و همه چیز را یک جا گردآوری کنیم، رشتهای درون فایلهای زبان به صورت زیر مینویسیم:
Today it’s {0} degrees
در رشته فوق {0} نماینده پارامتر نخست است و با متغیری که به تابع i18n ارسال میشود، جایگزین خواهد شد. با در نظر گرفتن همه مواردی که تاکنون مورد اشاره قرار گفتند، میتوانیم سرویسهای بومیسازی را با خطوط زیر بسط دهیم:
1window.i18n = (key, params) => {
2 if (params || params === 0) {
3 let i18nKey = window.i18nData[key];
4
5 if (typeof params !== 'object') {
6 i18nKey = i18nKey.replace('{0}', params);
7 } else {
8 for (let i = 0; i < params.length; i++) {
9 i18nKey = i18nKey.replace(`{${i}}`, params[i]);
10 }
11 }
12
13 return i18nKey;
14 } else {
15 return window.i18nData[key];
16 }
17};
تابع i18n ما هم اکنون یک پارامتر دوم به نام params میپذیرد. اگر هیچ پارامتری ارسال نشده باشد، از راهحل اولیه یعنی کد زیر استفاده میکنیم:
1return window.i18nData[key]
اگر یک پارامتر داشته باشیم، متغیر جدیدی به نام i18nKey ایجاد میکنیم. این همان چیزی است که باید بازگشت دهیم. توجه کنید که باید شرط صفر بودن را بررسی کنیم، چون در غیر این صورت ممکن است به اشتباه false ارزیابی شود.
در آغاز، مقدار آن رشتهای خالی از i18nData خواهد بود. برای تبدیل آن باید ابتدا بررسی کنیم که این params که ارسال کردهایم به صورت یک شیء است یا نه، چون چندین پارامتر ورودی داریم که ترجیحاً به صورت آرایه است. اگر یک مقدار منفرد داشته باشیم، می تونیم آن را به سادگی با {0} عوض کنیم که به تابع ارسال میشود. در غیر این صورت حلقهای روی آرایه تعریف میکنیم و هر مقدار را با مقداری که به تابع ارسال میشود، جایگزین میکنیم. برای تست کردن آن یک کلید جدید به فایلهای زبان اضافه میکنیم:
1'weatherCondition': 'Today it\'s {0} degrees'
و آن را درون کامپوننت فرامیخوانیم تا ببینیم resolve شده است یا نه. این کلید با پارامترهای چندگانه نیز کار میکند:
1// this will output "Today it's 32 degrees"
2<p>{i18n('weatherCondition', 32)}</p>
3
4// Replace the localization text with the following: "Today it's {0} degrees{1}"
5// this will output "Today it's 32 degrees!"
6<p>{i18n('weatherCondition', [32, '!'])}</p>
افزودن پشتیبانی از الگوی انتخابی
اینک کد ما در حال تکمیل شدن است، اما اگر بخواهیم ترجمههای پیچیدهتری داشته باشیم چطور؟ تصور کنید میخواهیم نشان دهیم آیا کاربر تنظیمات خود را بهروزرسانی کرده است یا نه. از این رو یک کلید بومیسازی به صورت زیر داریم:
updated x days ago
این رشته میتواند بر مبنای پارامتر چندین نسخه داشته باشد. برای نمونه:
updated 1 day ago updated 10 days ago
و همه اینها به شماره روزها وابسته هستند. بدین ترتیب کلمه day نیز ممکن است چند شکل داشته باشد. این همان جایی است که الگوی های انتخابی وارد بازی میشوند. کار خود را با یک رشته نمونه آغاز میکنیم تا ببینیم چگونه می تونیم الگو را درون فایلهای زبان بنویسیم:
1'lastUpdated': 'updated {choice {0} #>=1 day | <1 days#} ago'
همه چیز را درون آکولاد قرار میدهیم و میتوانیم الگوی انتخابی را با کلمه choice درون آن و سپس پارامتر نشان دهیم. بین هشتگها میتوان یک عملگر با یک شماره تعریف کرد که در ادامه آن کلمه مورد استفاده میآید. به مثالهای زیر توجه کنید:
- اگر پارامتر کمتر یا مساوی 1 باشد، از کلمه day استفاده میکنیم.
- اگر عدد بزرگتر از 1 باشد، از کلمه days استفاده میکنیم.
همچنین میتوانیم نسخههای بیشتری را با اتصال آنها به هم بسازیم:
برای شناسایی چنین الگوهایی از regex استفاده میکنیم. اگر با regex آشنا نیستید، پیشنهاد میکنیم مطلب زیر را مطالعه کنید:
بسط دادن تابع i18n با گزاره if زیر موجب میشود که الگوهای انتخابی در اختیار ما قرار گیرند:
1// Parse choice patterns
2const choiceRegex = /{choice[a-zA-Z0-9{}\s<>=|#]+}/g;
3const choicesRegex = /#[<>=0-9a-zA-Z|\s]+#/g
4
5if (i18nKey.match(choiceRegex)) {
6 for (const choicePattern of i18nKey.match(choiceRegex)) {
7 const decisionMaker = parseInt(choicePattern.replace(choicesRegex, '')
8 .replace('{choice', '')
9 .replace('}', '')
10 .trim(), 10);
11
12 const choices = choicePattern.match(choicesRegex)[0]
13 .replace(/#/g, '');
14
15 const operators = choices.match(/[<>=]+/g);
16 const numbers = choices.match(/[0-9]+/g).map(num => parseInt(num, 10));
17 const words = choices.match(/[a-zA-Z]+/g);
18
19 let indexToUse = 0;
20
21 for (let i = 0; i < words.length; i++) {
22 switch (operators[i]) {
23 case '<': indexToUse = numbers[i] < decisionMaker ? i : indexToUse; break;
24 case '>': indexToUse = numbers[i] > decisionMaker ? i : indexToUse; break;
25 case '<=': indexToUse = numbers[i] <= decisionMaker ? i : indexToUse; break;
26 case '>=': indexToUse = numbers[i] >= decisionMaker ? i : indexToUse; break;
27 case '=': indexToUse = numbers[i] === decisionMaker ? i : indexToUse; break;
28 default: indexToUse = numbers[i] === decisionMaker ? i : indexToUse; break;
29 }
30 }
31
32 i18nKey = i18nKey.replace(choicePattern, [decisionMaker, words[indexToUse]].join(' '));
33 }
34}
اگر بخواهیم کد فوق را توضیح دهیم، دو regex داریم که یکی برای الگوی انتخابی و دیگری برای انتخابهای درون آن است. اگر مورد مطابقت یافت شود، برخی متغیرها ایجاد میشوند:
ما میخواهیم شکل صحیح کلمه را از الگوی انتخابی به دست آوریم. به این منظور در خط 21 روی کلمهها حلقهای تعریف میکنیم و بین عملگرهای متناظرشان سوئیچ میکنیم. اگر عدد ارسالی به تابع بر اساس operator و عدد بعد از آن، یک مقدار true بدهد، اندیس جدید را به آن انتساب میدهیم. در نهایت در خط 32 الگوی انتخابی را با پارامتر ارسالی و کلمهای که با یک فاصله به آن الحاق شده، عوض میکنیم:
1<p>{i18n('lastUpdated', 1)}</p>
اگر این پاراگراف جدید را به کامپوننت اضافه کنید، الگوی انتخابی را در عمل میبینید. با تغییر دادن 1 به 10، کلمه day نیز به days تغییر مییابد:
بهبودهای ممکن
این کد مطابق انتظار ما عمل میکند و میتوانیم برخی ترجمههای کاملاً پیچیده را با آن تولید کنیم، اما مانند هر چیز دیگر امکان بهبود هر چه بیشتر آن وجود دارد. در ادامه برخی ایدههای بهبود این کد ارائه شدهاند:
- زبان را به بکاند انتقال دهید و صرفاً برای زبانی که در سایت مورد استفاده قرار میگیرد درخواست ارسال کنید. بدین ترتیب از بارگذاری همه زبانها در سمت کلاینت جلوگیری میکنیم.
- از آلوده شدن فضای نام سراسری با افشای هر تابع از سوی سرویس بومیسازی دریک شیء کانتینر جلوگیری کنید.
- الگوی انتخابی بتواند نهتنها کلمههای منفرد را بپذیرد، بلکه از جمله نیز پشتیبانی کند.
سخن پایانی
برای جمعبندی این مقاله باید اشاره کنیم که پیادهسازی یک سرویس بومیسازی کار خارقالعادهای نیست و با دانستن مفاهیم کلیدی میتوان به سادگی یک نسخه سفارشی از آن را ساخت. بدین ترتیب میتوانید از مزیت داشتن کاربرانی در خارج از مرزهای کشور خود بهرهمند شوید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- هشت ترفند مفید برای توسعه اپلیکیشنهای React — راهنمای کاربردی
- ۲۲ ابزار مهم برای توسعهدهندگان React — فهرست کاربردی
==