خدمة الترجمة

خدمة الترجمة

تمرين تعلّمي

مقدمة

يمثّل كائن Promise الإتمام النهائي (أو الفشل) لعملية غير متزامنة وقيمتها الناتجة.

Note

هذا موضوع صعب على كثيرين، خصوصًا إن كنت تعرف البرمجة بلغة متزامنة بالكامل. إذا شعرت أن الأمر يفوقك، أو أردت معرفة المزيد عن التزامن والتوازي، فـشاهد (عبر go.dev) أو شاهد مباشرة عبر vimeo واقرأ الشرائح للمحاضرة الرائعة "Concurrency is not parallelism".

دورة حياة الوعد

للوعد Promise ثلاث حالات:

  1. معلّق
  2. منجَز
  3. مرفوض

عند إنشائه، يكون الوعد معلّقًا. وفي وقت لاحق قد يُنجَز أو يُرفَض. وبمجرد أن يُنجَز الوعد أو يُرفَض مرة واحدة، لا يمكن أن يُنجَز أو يُرفَض مرة أخرى، ولا يمكن أن تتغيّر حالته.

وبعبارة أخرى:

  1. عندما يكون معلّقًا، يمكن للوعد:
    • أن ينتقل إلى أيٍّ من الحالتين المنجَزة أو المرفوضة.
  2. عندما يكون منجَزًا، يجب على الوعد:
    • ألا ينتقل إلى أي حالة أخرى.
    • أن يكون له قيمة لا تتغيّر.
  3. عندما يكون مرفوضًا، يجب على الوعد:
    • ألا ينتقل إلى أي حالة أخرى.
    • أن يكون له سبب لا يتغيّر.

إنجاز الوعد

يمكن إنجاز الوعد بطرق متعددة:

// 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 أو وعد آخر. وعادةً تريد الإنجاز بقيمة ليست خطأ.

رفض الوعد

يمكن رفض الوعد بطرق متعددة:

// 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. وعادةً تريد الرفض بسبب خطأ.

ربط الوعد

يمكن متابعة الوعد بإجراء لاحق بمجرد أن يُنجَز أو يُرفَض.

then

كل وعد قابل للربط بـ then. وهذا يعني وجود دالة then متاحة تُنفَّذ بمجرد إنجاز الوعد الأصلي. عند استخدام promise.then(onResolved)، تستقبل دالة رد النداء onResolved القيمة التي أُنجز بها الوعد الأصلي. وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".

إرجاع value من then يُنجز الوعد "المترابط". ورميّ reason في then يرفض الوعد "المترابط".

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!" بعد نحو 1000 مللي ثانية. وستكون حالة promise1 وقيمته resolved و"Success!". وستكون حالة promise2 وقيمته resolved وtrue.

وهناك وسيط ثانٍ متاح يُنفَّذ عندما يُرفَض الوعد الأصلي. عند استخدام promise.then(onResolved, onRejected)، تستقبل دالة رد النداء onResolved القيمة التي أُنجز بها الوعد الأصلي، أو تستقبل دالة رد النداء onRejected سبب رفض الوعد.

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);
  • في نحو 1/2 من الحالات، سيسجّل هذا "Success!" بعد نحو 1000 مللي ثانية.
    • وستكون حالة promise1 وقيمته resolved و"Success!".
    • وستكون حالة promise2 وقيمته resolved وtrue.
  • وفي نحو 1/2 من الحالات، سيسجّل هذا "NOPE!" فورًا.
    • وستكون حالة promise1 وقيمته rejected وNope!.
    • وستكون حالة promise2 وقيمته resolved وfalse.

من المهم أن تفهم أنه بسبب قواعد دورة الحياة، عندما rejects، فإن resolve الذي يأتي بعد نحو 1000 مللي ثانية يُتجاهَل بصمت، لأن الحالة الداخلية لا يمكن أن تتغيّر بمجرد أن يُرفَض الوعد أو يُنجَز. ومن المهم أن تفهم أن إرجاع قيمة من وعد يُنجزه، ورميّ قيمة يرفضه. عندما يُنجَز promise1 ويكون هناك onResolved مترابط: then(onResolved)، فإن ذلك التابع وعد جديد يمكن أن يُنجَز أو يُرفَض. وعندما يُرفَض promise1 لكن يكون هناك onRejected مترابط: then(, onRejected)، فإن ذلك التابع وعد جديد يمكن أن يُنجَز أو يُرفَض.

catch

أحيانًا تريد التقاط الأخطاء والمتابعة فقط عندما rejects الوعد الأصلي. عند استخدام promise.catch(onCatch)، تستقبل دالة رد النداء onCatch سبب رفض الوعد الأصلي. وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".

إرجاع value من catch يُنجز الوعد "المترابط". ورميّ reason في catch يرفض الوعد "المترابط".

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

في نحو 1/2 من الحالات، سيسجّل هذا "Success!" بعد نحو 1000 مللي ثانية. وفي النصف الآخر من الحالات، سيسجّل هذا 42 فورًا.

  • إذا أُنجز promise1، يُتخطّى catch ويصل التنفيذ إلى then ويسجّل القيمة.
    • وستكون حالة promise1 وقيمته resolved و"Success!".
    • وستكون حالة promise2 وقيمته resolved و"done";
  • وإذا رُفض promise1، يُنفَّذ catch، الذي يُرجع قيمة، وبذلك تصبح السلسلة الآن resolved، ويصل التنفيذ إلى then ويسجّل القيمة.
    • وستكون حالة promise1 وقيمته rejected و"Nope!".
    • وستكون حالة promise2 وقيمته resolved و"done";

finally

أحيانًا تريد تنفيذ كود بعد أن يستقر الوعد، بغض النظر عما إذا كان يُنجَز أو يُرفَض. عند استخدام promise.finally(onSettled)، لا تستقبل دالة رد النداء onSettled شيئًا. وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".

إرجاع value من finally ينسخ الحالة والقيمة من الوعد الأصلي مع تجاهل value. ورميّ reason في finally يرفض الوعد "المترابط"، ويلغي أي حالة وقيمة أو سبب من الوعد الأصلي.

مثال

عدة طرق معًا:

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
    • وسيُنجَز finalPromise بالقيمة 'yay'
    • وسيكون هناك سجلّان:
      • Sampled data: ...
      • Promise completed
  • وفي الحالات التي يكون فيها randomNumber هو 4-5:
    • سيُرفَض myPromise بالسبب 'Sampling did not result in a sample'
    • وسيُرفَض finalPromise بالسبب Error('Sampling did not result in a sample')
    • وسيكون هناك سجل واحد:
      • Promise completed
      • وفي بعض البيئات سينتج عن ذلك سجل "uncaught rejected promise: Error('Sampling did not result in a sample')"

كما هو موضح أعلاه، يعمل reject مع سلسلة نصية، ويمكن للوعد أيضًا أن يُرفَض بكائن Error.

Note

إذا كان ربط الوعود أو الاستخدام العام غير واضح، فإن الدرس التعليمي على 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.

  • عند النجاح يُمرَّر undefined إلى callback: وهذا يعني أن الترجمة نجحت ويمكن الوصول إليها باستخدام الطريقة api.fetch.
  • وعند الفشل يُمرَّر error إلى callback: وهذا يعني أن شيئًا ما سار على نحو خاطئ. إن 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 لنص متاح، فإن 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, ...]) للأعضاء المجانيين تترجم مصفوفة من النصوص باستخدام الخدمة المجانية، وتُرجع كل الترجمات أو خطأً واحدًا.

  • تُحلّ بكل الترجمات (بالترتيب نفسه) إذا كانت كلها متاحة
  • تُرفض بأول خطأ تصادفه
  • وتُرفض بخطأ BatchIsEmpty إذا لم يُعطَ أي نص
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. وينبغي أن يعيد الطلب المحاولة تلقائيًا إذا حدث فشل. وينبغي ألا يجري أكثر من 3 استدعاءات للطلب نفسه (لا تغضب مترجمي الفضاء!!!).

  • إذا لم يُرجع api.request خطأً، فحلّ بـundefined
  • وإذا أرجع api.request خطأً، فأعد المحاولة مرتين على الأكثر
  • وإذا نفدت محاولاتك، فارفض بآخر خطأ تلقيته
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. جلب ترجمة وفحص الجودة أو طلبها

نفّذ طريقة للمستخدمين المميزين premium(text, quality) لجلب ترجمة. إذا كانت الترجمة NotAvailable، فاطلب الترجمة واجلبها بعد إضافتها إلى تخزين API. وينبغي ألا تُرجع الطريقة الترجمة إلا إذا بلغت حدًا معينًا من quality.

  • إذا حُلّ api.fetch، فافحص الجودة قبل الحل
  • وإذا رُفض api.fetch، فـ_اطلب_ الترجمة بدلًا من ذلك
  • وإذا رُفض api.request، فمرّر الخطأ
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 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.