سرویس ترجمه

سرویس ترجمه

تمرین یادگیری

مقدمه

شیء Promise نشان‌دهنده‌ی تکمیل نهایی (یا شکست) یک عملیات ناهم‌زمان و مقدار حاصل از آن است.

Note

این موضوع برای بسیاری از افراد دشوار است، به‌ویژه اگر در زبانی برنامه‌نویسی کرده باشید که کاملاً هم‌زمان است. اگر احساس می‌کنید غرق شده‌اید، یا می‌خواهید درباره‌ی «هم‌روندی» و «موازی‌سازی» بیشتر بدانید، سخنرانی درخشان «Concurrency is not parallelism» را تماشا کنید (از طریق go.dev) یا مستقیماً از طریق vimeo تماشا کنید و اسلایدهای آن را بخوانید.

چرخه‌ی حیات یک Promise

یک Promise سه حالت دارد:

  1. pending
  2. fulfilled
  3. rejected

وقتی ساخته می‌شود، یک Promise در حالت pending است. در نقطه‌ای در آینده ممکن است resolve یا reject شود. وقتی یک Promise یک بار resolve یا reject شد، دیگر هرگز نمی‌تواند دوباره resolve یا reject شود و حالتش هم تغییر نمی‌کند.

به بیان دیگر:

  1. یک Promise در حالت pending:
    • می‌تواند به حالت fulfilled یا rejected منتقل شود.
  2. یک Promise در حالت fulfilled:
    • نباید به هیچ حالت دیگری منتقل شود.
    • باید مقداری داشته باشد که نباید تغییر کند.
  3. یک Promise در حالت rejected:
    • نباید به هیچ حالت دیگری منتقل شود.
    • باید دلیلی داشته باشد که نباید تغییر کند.

resolve کردن یک Promise

یک Promise به روش‌های مختلفی resolve می‌شود:

// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
  resolve(value);
});

// Chaining a promise leads to a resolved promise
somePromise.then(() => {
  // ...
  return value;
});

در مثال‌های بالا value می‌تواند هر چیزی باشد، از جمله یک خطا، undefined، null یا یک Promise دیگر. معمولاً می‌خواهید با مقداری که خطا نیست resolve کنید.

reject کردن یک Promise

یک Promise به روش‌های مختلفی reject می‌شود:

// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
  reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
  // ...
  throw reason
})

در مثال‌های بالا reason می‌تواند هر چیزی باشد، از جمله یک خطا، undefined یا null. معمولاً می‌خواهید با یک خطا reject کنید.

زنجیره کردن یک Promise

پس از آنکه یک Promise resolve یا reject شد، می‌توان با یک اقدام آینده آن را ادامه داد.

  • promise.then() زمانی فراخوانی می‌شود که promise resolve شود
  • promise.catch() زمانی فراخوانی می‌شود که promise reject شود
  • promise.finally() زمانی فراخوانی می‌شود که promise یا resolve شود یا reject

then

هر Promise «thenable» است. یعنی یک تابع then در دسترس است که وقتی Promise اصلی resolve شد، اجرا می‌شود. با promise.then(onResolved)، تابع بازگشتی onResolved مقداری را دریافت می‌کند که Promise اصلی با آن resolve شده است. این همیشه یک Promise «زنجیره‌ای» جدید برمی‌گرداند.

بازگرداندن یک value از then باعث resolve شدن Promise «زنجیره‌ای» می‌شود. پرتاب یک reason در then باعث reject شدن Promise «زنجیره‌ای» می‌شود.

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

const promise2 = promise1.then(function (value) {
  console.log(value);
  // expected output: "Success!"

  return true;
});

این پس از حدود ۱۰۰۰ میلی‌ثانیه "Success!" را لاگ می‌کند. حالت و مقدار promise1 برابر resolved و "Success!" خواهد بود. حالت و مقدار promise2 برابر resolved و true خواهد بود.

آرگومان دومی هم در دسترس است که وقتی Promise اصلی reject می‌شود، اجرا می‌شود. با promise.then(onResolved, onRejected)، تابع بازگشتی onResolved مقداری را دریافت می‌کند که Promise اصلی با آن resolve شده است، یا تابع بازگشتی onRejected دلیلی را دریافت می‌کند که Promise با آن reject شده است.

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return true;
}

function shout(reason) {
  console.error(reason.toUpperCase());
  return false;
}

const promise2 = promise1.then(log, shout);
  • در حدود ۱/۲ موارد، این پس از حدود ۱۰۰۰ میلی‌ثانیه "Success!" را لاگ می‌کند.
    • حالت و مقدار promise1 برابر resolved و "Success!" خواهد بود.
    • حالت و مقدار promise2 برابر resolved و true خواهد بود.
  • در حدود ۱/۲ موارد، این بلافاصله "NOPE!" را لاگ می‌کند.
    • حالت و مقدار promise1 برابر rejected و Nope! خواهد بود.
    • حالت و مقدار promise2 برابر resolved و false خواهد بود.

درک این نکته مهم است که به دلیل قواعد چرخه‌ی حیات، وقتی یک Promise عمل reject را انجام می‌دهد، resolveی که حدود ۱۰۰۰ میلی‌ثانیه بعد می‌آید بی‌صدا نادیده گرفته می‌شود، چون حالت داخلی پس از reject یا resolve شدن دیگر نمی‌تواند تغییر کند. درک این نکته هم مهم است که بازگرداندن یک مقدار از یک Promise آن را resolve می‌کند و پرتاب یک مقدار آن را reject می‌کند. وقتی promise1 resolve می‌شود و یک onResolved زنجیره‌شده دارد: then(onResolved)، آن ادامه یک Promise جدید است که می‌تواند resolve یا reject شود. وقتی promise1 reject می‌شود اما یک onRejected زنجیره‌شده دارد: then(, onRejected)، آن ادامه یک Promise جدید است که می‌تواند resolve یا reject شود.

catch

گاهی می‌خواهید خطاها را بگیرید و فقط زمانی ادامه دهید که Promise اصلی reject شود. با promise.catch(onCatch)، تابع بازگشتی onCatch دلیلی را دریافت می‌کند که Promise اصلی با آن reject شده است. این همیشه یک Promise «زنجیره‌ای» جدید برمی‌گرداند.

بازگرداندن یک value از catch باعث resolve شدن Promise «زنجیره‌ای» می‌شود. پرتاب یک reason در catch باعث reject شدن Promise «زنجیره‌ای» می‌شود.

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return 'done';
}

function recover(reason) {
  console.error(reason.toUpperCase());
  return 42;
}

const promise2 = promise1.catch(recover).then(log);

در حدود ۱/۲ موارد، این پس از حدود ۱۰۰۰ میلی‌ثانیه "Success!" را لاگ می‌کند. در ۱/۲ دیگر موارد، این بلافاصله 42 را لاگ می‌کند.

  • اگر promise1 resolve شود، catch نادیده گرفته می‌شود و به then می‌رسد و مقدار را لاگ می‌کند.
    • حالت و مقدار promise1 برابر resolved و "Success!" خواهد بود.
    • حالت و مقدار promise2 برابر resolved و "done" خواهد بود؛
  • اگر promise1 reject شود، catch اجرا می‌شود، که مقداری را برمی‌گرداند و بنابراین زنجیره اکنون resolved است و به then می‌رسد و مقدار را لاگ می‌کند.
    • حالت و مقدار promise1 برابر rejected و "Nope!" خواهد بود.
    • حالت و مقدار promise2 برابر resolved و "done" خواهد بود؛

finally

گاهی می‌خواهید پس از مشخص شدن سرانجام یک Promise، کدی را اجرا کنید، بی‌آنکه مهم باشد آن Promise resolve می‌شود یا reject. با promise.finally(onSettled)، تابع بازگشتی onSettled هیچ مقداری دریافت نمی‌کند. این همیشه یک Promise «زنجیره‌ای» جدید برمی‌گرداند.

بازگرداندن یک value از finally وضعیت و مقدار Promise اصلی را کپی می‌کند و value را نادیده می‌گیرد. پرتاب یک reason در finally باعث reject شدن Promise «زنجیره‌ای» می‌شود و هر وضعیت و مقدار یا دلیلی را از Promise اصلی بازنویسی می‌کند.

مثال

چند مورد از این متدها در کنار هم:

const myPromise = new Promise(function (resolve, reject) {
  const sampleData = [2, 4, 6, 8];
  const randomNumber = Math.round(Math.random() * 5);

  if (sampleData[randomNumber]) {
    resolve(sampleData[randomNumber]);
  } else {
    reject('Sampling did not result in a sample');
  }
});

const finalPromise = myPromise
  .then(function (sampled) {
    // If the random number was 0, 1, 2, or 3, this will be
    // reached and the number 2, 4, 6, or 8 will be logged.
    console.log(`Sampled data: ${sampled}`);
    return 'yay';
  })
  .catch(function (reason) {
    // If the random number was 4 or 5, this will be reached and
    // reason will be "An error occurred". The entire chain will
    // then reject with an Error with the reason as message.
    throw new Error(reason);
  })
  .finally(function () {
    // This will always log after either the sampled data is
    // logged or the error is raised.
    console.log('Promise completed');
  });
  • در مواردی که randomNumber برابر 0-3 است:
    • myPromise با مقدار 2, 4, 6, or 8 resolve می‌شود
    • finalPromise با مقدار 'yay' resolve می‌شود
    • دو لاگ ثبت می‌شود:
      • Sampled data: ...
      • Promise completed
  • در مواردی که randomNumber برابر 4-5 است:
    • myPromise با دلیل 'Sampling did not result in a sample' reject می‌شود
    • finalPromise با دلیل Error('Sampling did not result in a sample') reject می‌شود
    • یک لاگ ثبت می‌شود:
      • Promise completed
      • در برخی محیط‌ها این یک لاگ "uncaught rejected promise: Error('Sampling did not result in a sample')" تولید می‌کند

همان‌طور که در بالا نشان داده شد، reject با یک رشته کار می‌کند و یک Promise می‌تواند با یک Error هم reject شود.

Note

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

دستورالعمل‌ها

در این تمرین، یک TranslationService ارائه می‌دهید که خدمات ترجمه‌ی پایه را به اعضای رایگان و ترجمه‌ی پیشرفته را همراه با تضمین کیفیت به اعضای ویژه می‌دهد.

API

شما یک API ترجمه‌ی فضایی پیدا کرده‌اید که هر request ترجمه را در زمانی معقول انجام می‌دهد. می‌خواهید از این فرصت استفاده کنید. مترجم‌های فضایی بسیار دمدمی‌مزاج‌اند و از افزونگی بیزارند، بنابراین ماهواره‌های ذخیره‌گاه API را هم فراهم کرده‌اند که می‌توانید ترجمه‌های پیشین را بدون مزاحمت برای آن‌ها fetch کنید.

واکشی یک ترجمه

api.fetch(text) ترجمه‌ی text را از ذخیره‌گاه API واکشی می‌کند و یک promise برمی‌گرداند که دو مقدار ارائه می‌دهد:

  • translation: خود ترجمه
  • quality: کیفیت که به‌صورت یک عدد بیان می‌شود

اگر ترجمه‌ای در ذخیره‌گاه API پیدا نشود، API یک خطای NotAvailable پرتاب می‌کند. ترجمه‌ها را می‌توان با متد api.request اضافه کرد. اگر text قابل ترجمه نباشد، API یک خطای Untranslatable پرتاب می‌کند.

api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })

درخواست یک ترجمه

برخی ترجمه‌ها قطعاً وجود دارند، اما هنوز به ذخیره‌گاه API افزوده نشده‌اند. این همان تفاوت میان NotAvailable (در ذخیره‌گاه نیست، اما می‌توان درخواستش کرد) و Untranslatable (قابل ترجمه نیست) است.

api.request(text, callback) درخواست می‌کند که ترجمه‌ی text انجام شود و به ذخیره‌گاه API افزوده گردد. پس از تکمیل، تابع callback فراخوانی می‌شود.

  • در صورت موفقیت، به callback مقدار undefined داده می‌شود: این یعنی ترجمه موفق بوده و با متد api.fetch قابل دسترسی است.
  • در صورت شکست، به callback یک error داده می‌شود: این یعنی مشکلی پیش آمده است. API فضایی ناپایدار است، یعنی اغلب شکست می‌خورد. اگر چنین شد، اشکالی ندارد که دوباره api.request کنید.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ هشدار! ⚠

Caution

API جادویش را با تله‌پورت کردن مترجم‌های مختلف انجام می‌دهد، هر وقت که یک request برسد. این کار بسیار پرهزینه است، بنابراین وقتی ترجمه‌ای موجود است نباید فراخوانی شود. متأسفانه همه دفترچه‌ی راهنما را نمی‌خوانند، بنابراین سامانه‌ای هست که افراد متخلف را بیرون می‌اندازد.

اگر api.request برای textی فراخوانی شود که موجود است، API برای این فراخوانی یک AbusiveClientError پرتاب می‌کند، و برای هر فراخوانی پس از آن. مطمئن شوید که اگر چیزی قبلاً ترجمه شده، هرگز درخواست ترجمه نکنید.

1. واکشی یک ترجمه، بدون توجه به کیفیت

سرویس رایگان فقط ترجمه‌هایی را ارائه می‌دهد که در حال حاضر در ذخیره‌گاه API هستند.

متدی به نام free(text) پیاده‌سازی کنید که به اعضای رایگان ترجمه‌هایی را ارائه می‌دهد که از قبل در ذخیره‌گاه API وجود دارند. کیفیت را نادیده بگیرید و هر خطایی که API پرتاب می‌کند را منتقل کنید.

  • اگر ترجمه قابل بازیابی باشد، آن را برمی‌گرداند، بدون توجه به کیفیتش
  • هر خطای API ترجمه را منتقل می‌کند
  • از متد api.fetch استفاده می‌کند (api.fetch یک promise برمی‌گرداند)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")

2. واکشی دسته‌ای از ترجمه‌ها، همه یا هیچ

متدی به نام batch([text, text, ...]) برای اعضای رایگان پیاده‌سازی کنید که آرایه‌ای از متن‌ها را با سرویس رایگان ترجمه می‌کند و همه‌ی ترجمه‌ها یا یک خطای واحد را برمی‌گرداند.

  • اگر همه‌ی ترجمه‌ها موجود باشند، با همه‌ی آن‌ها (به همان ترتیب) resolve می‌شود
  • با نخستین خطایی که رخ دهد reject می‌شود
  • اگر هیچ متنی داده نشود، با یک خطای BatchIsEmpty reject می‌شود
service.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]

service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")

service.batch([]);
// => Promise<...> rejects BatchIsEmpty()

3. درخواست یک ترجمه، با حداکثر ۲ بار تلاش مجدد

متدی برای کاربران ویژه به نام request(text) پیاده‌سازی کنید که درخواست می‌کند ترجمه‌ای به ذخیره‌گاه API افزوده شود. درخواست باید در صورت بروز خطا به‌طور خودکار دوباره تلاش کند. برای یک درخواست یکسان، نباید بیشتر از ۳ بار فراخوانی انجام دهد (مترجم‌های فضایی را عصبانی نکنید!!!).

  • اگر api.request خطایی برنگرداند، با undefined resolve کنید
  • اگر api.request خطایی برگرداند، حداکثر دو بار دوباره تلاش کنید
  • اگر تلاش‌های مجدد تمام شد، با آخرین خطای دریافت‌شده reject کنید
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. واکشی یک ترجمه، بررسی کیفیت، یا درخواست آن

متدی برای کاربران ویژه به نام premium(text, quality) پیاده‌سازی کنید که یک ترجمه را واکشی می‌کند. اگر ترجمه‌ای NotAvailable باشد، ترجمه را درخواست کنید و پس از افزوده شدنش به ذخیره‌گاه API واکشی‌اش کنید. متد باید ترجمه را فقط در صورتی برگرداند که آستانه‌ی مشخصی از quality را برآورده کند.

  • اگر api.fetch resolve شود، پیش از resolve کردن کیفیت را بررسی کنید
  • اگر api.fetch reject شود، به‌جایش ترجمه را درخواست کنید
  • اگر api.request reject شود، خطا را منتقل کنید
service.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."

service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()

service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"

نکته.

Note

ترجمه‌ی درست 'arlogh Qoylu'pu'? عبارت است از How many times has it been heard?.

ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
JavaScript Exercism

آماده‌اید سرویس ترجمه را شروع کنید؟

در Exercism ثبت‌نام کنید تا JavaScript را همراه با 37 مفهوم159 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.