Skip to content

pitfalls and solutions

SalehGNUTUX edited this page Jul 24, 2026 · 3 revisions

سجلُّ الحالات والحلول — مرجعُ المزالق

دليلٌ عمليٌّ لكلِّ مشكلةٍ عالجناها (وما يُشبهها مستقبلاً). كلُّ حالة: العَرَض ← الجذر ← الحلّ ← الموضع. رتّبناها بالمجال. عند مواجهة سلوكٍ غريب، ابحثْ هنا أولاً.


🔊 الصوت والنطق

ح‑1 · الشدّةُ تُسمَع كالسكون (أوّل، محرّم، عدّ)

  • العَرَض: كلماتٌ بشدّةٍ يَنطقها العصبيُّ/الآليُّ ناقصةً («أَوْل» بدل «أوّل»).
  • الجذر: ترتيبُ التشكيل «حرف+حركة+شدّة» (وما يُنتجه NFC)؛ المحرّكُ يتوقّع «حرف+شدّة+حركة».
  • الحلّ: forSynthesis/reorderMarks في src/arabic-normalize.js، تُطبَّق على نصّ التركيب فقط. تفصيلٌ كاملٌ في audio-pronunciation.md.

ح‑2 · لفظُ الجلالة «الله» مشوّهٌ مع حركة الإعراب (lhi/lho)

  • العَرَض: اللهِ → «lhi»، اللهُ → «lho».
  • الجذر: قاعدةُ espeak الخاصّةُ لـ«الله» تعملُ للكلمة المجرّدة وحدها؛ حركةُ الإعراب تكسرُها.
  • الحلّ: jalalahBare يستبدلها بإملاءٍ صوتيٍّ بمدٍّ صريح «اَلّاه + الحركة» (ولـ«لله/بالله/اللهمّ» نظائرُها).

ح‑3 · المقطعُ لا يُنطَق رغم وجود التسجيل (مطابقة حرفيّة)

  • العَرَض: «وّ» تُسمَع في الاستوديو لكنّها لا تُنطَق داخل كلمة.
  • الجذر: المطابقةُ حرفيّةٌ 100%؛ اختلافُ ترتيبِ التشكيل/مسافةٍ بين النصّين.
  • الحلّ: وحِّدِ الترتيبَ على الطرفين (reorderMarks في segment())؛ بعد أيِّ محتوًى منطوق: npm run gen:audio.

ح‑4 · أوّلُ تشغيلٍ لمقطعٍ يرتدُّ للنطق الآليّ

  • الجذر: التشغيلُ قبل جهوزيّة المقطع.
  • الحلّ: playUrlAsync ينتظر canplay؛ primeVoices قبل أوّل تشغيلٍ في قارئ القصص.

ح‑7 · نطقُ نصٍّ يُعرَض مجرّداً (مرجعٌ منطوقٌ مشكول)

  • العَرَض: كلماتٌ/أحرفٌ تُعرَض مجرّدةً عمداً (كلماتُ لعبة الدمج content/words.js، أسماءُ الأحرف «ألف») فيَنطقها المحرّكُ بالحدس خطأً (ألف→«allaf»، ذرة→«dharra»).
  • الجذر: لا يصحُّ تشكيلُ نصّ العرض (يكسرُ اللعبةَ/الشكل)، والمحرّكُ يَحدِس المجرّد.
  • الحلّ: src/spoken-ref.js — خريطةُ «عرض مجرّد ← مرجعٌ منطوقٌ مشكول» يستعملها المولّدان (gen-audio/gen-piper) عبر spokenRef(t) ثمّ forSynthesis، والمفتاحُ يبقى المجرَّدَ فلا يتأثّر البحث. أسماءُ الأعداد مشكولةٌ أصلاً (numbers-ar.js)؛ عبارات الآليّ robo-phrases.js مشكولةٌ في المصدر مباشرةً.
  • مزلق: تشكيلُ نصٍّ كان مفتاحاً (كعبارات NOTICES) يغيّر المفتاحَ فتُولَّد مقاطعُ جديدةٌ وتَيتَمُ القديمةُ ⇒ نظِّفِ المقاطعَ غيرَ المُشار إليها في tts-manifest.json من public/tts/.

ح‑5 · أسماءُ الأعداد 0–100 لا تُنطَق

  • الجذر: تشكيلُ numbers-ar.js/numerals.js لا يطابق letters.js حرفيّاً (المطابقة حرفيّة).
  • الحلّ: وحِّدِ التشكيلَ (صِفر، واحِد، ثَلاثة…).

ح‑6 · الانتقالُ التلقائيّ يَعلَق على جهازٍ بلا أصواتٍ عربيّة

  • الجذر: speechSynthesis لا يُطلق onend إن لم تَجهز الأصوات.
  • الحلّ: مهلةُ أمانٍ في speak.js → ttsPath تُطلق onend.

📦 PWA · البناء · النشر

ب‑1 · كودٌ/مقاطعُ قديمةٌ بعد إعادة البناء

  • الجذر: عاملُ الخدمة (Service Worker) يُخبّئ كلَّ شيء.
  • الحلّ: إنتاجاً skipWaiting+clientsClaim+cleanupOutdatedCaches؛ تطويراً حارسٌ في sound-prefs.js يُلغي SW القديم ويمسح الكاش عند import.meta.env.DEV. عند الاستعصاء: أعِد التحميل مرّتين أو ألغِ تسجيلَ SW.

ب‑2 · صفحةٌ جديدةٌ لا تظهر في البناء

  • الجذر: كلُّ *.html نقطةُ دخولٍ مستقلّة.
  • الحلّ: أضِفها إلى vite.config.js → rollupOptions.input وإلّا لم تُبنَ.

ب‑3 · /app/* تُرجِع 404 رغم نجاح النشر

  • الجذر: نوعُ بناء GitHub Pages legacy لا workflow.
  • الحلّ: gh api --method PUT .../pages -f build_type=workflow ثمّ أعِد تشغيل المسار.

ب‑4 · تلاوةُ الحصري (~79م) لا يجوز تضمينُها في precache

  • الحلّ: globIgnores لها + runtimeCaching (CacheFirst، quran-husary) فتعمل دون إنترنت بعد أوّل استماع. النصُّ مُضمَّنٌ فيعمل دائماً.

ب‑5 · vite: Permission denied على ext4

  • الحلّ: chmod +x node_modules/.bin/*.

ح‑12 · نصٌّ منطوقٌ لا مقطعَ له ⇐ يُنطَقُ بصوتِ النظام (أو يصمت)

  • العَرَض: عبارةٌ جديدةٌ في لعبةٍ أو ردٍّ للآليِّ تُنطَقُ بصوتٍ رديءٍ (أو تصمتُ على لينكس).
  • الجذر: مطابقةُ المقاطعِ حرفيّةٌ بالنصِّ الكامل؛ فأيُّ نصٍّ لم يمرَّ على gen:piper لا مقطعَ له. ويشملُ ذلك النصوصَ المركَّبةَ وقتَ التشغيل (`${a} ${b}`) — استعمِلْ roboSayChain([a,b]) فيُنطَقَ كلُّ جزءٍ بمقطعِه.
  • الحلّ: أضِفِ العبارةَ إلى مصدرٍ يقرؤُه المولّد (GAME_INTROS/NOTICES/…) مشكولةً بالكامل ثمّ npm run gen:piper. التوليدُ تفاضليٌّ (يتخطّى الموجود) فيستغرقُ ثوانيَ لا 25 دقيقة؛ وPIPER_FORCE=1 يُعيدُ الكلَّ عندَ تغييرِ النموذج.
  • مزلقٌ مرافق: ما يستوردُه المولّدُ يجبُ أن يخلوَ من DOM — استيرادُ وحدةِ واجهةٍ (كـorient-widget) يجرُّ voices-bundled.json فيفشلُ في Node. ضعِ النصوصَ في content/.
  • نطقٌ خاطئٌ رغمَ التشكيل؟ استعمِلْ SYNTH_FIX في gen-piper.mjs: يُغيّرُ نصَّ التركيبِ فقط والمفتاحُ يبقى، فلا يتغيّرُ العرضُ ولا المطابقة (مثالٌ: «الوَقْت» كان يُسمَعُ «الوَخْت»).

📦 التحزيمُ الأصليّ (Linux · Android)

النظامُ الكاملُ موثّقٌ في packaging.md. المنسّق: scripts/build-packages.sh (npm run pkg:linux|pkg:apk|pkg:all). يَلُفُّ مخرجَ npm run build نفسَه: Linux بـelectron-builder، Android بـCapacitor.

ز‑1 · DEB يفشل: Please specify project homepage

  • الحلّ: أضِفْ homepageauthor) في package.json؛ يتطلّبهما electron-builder لوسمِ DEB/RPM.

ز‑2 · RPM يفشل عبر electron-builder (rpmbuild failed)

  • الجذر: fpm المرفقُ (1.9.3) قديمٌ لا يوافقُ rpmbuild الحديث (4.20+) — يُولّد spec مرفوضًا. (AppImage/DEB لا يستعملان rpmbuild فينجحان.)
  • الحلّ: ابْنِ DEB ثمّ حوّلْه إلى RPM بـfakeroot alien --to-rpm (rpm_from_deb في السكربت). يتطلّب alien+fakeroot.

ز‑3 · APK يفشل: Could not resolve project :capacitor-android

  • الجذر: npm install --no-save @capacitor/assets حذفَ حزمَ Capacitor المثبّتةَ سابقًا بـ--no-save (غيرُ مذكورةٍ في package.json = «زائدة») فاختفى node_modules/@capacitor/android.
  • الحلّ: ضَعْ @capacitor/core/cli/android (@^6، صغيرةٌ بلا تَنّات) في package.json؛ وassets --no-save بعدها لا يَحذِفُها. حافِظْ على توافقِ إصداراتها (نفسُ الـmajor). أبقِ الثقيلَ (electron/electron-builder/@capacitor/assets+sharp) خارجَ package.json (عند الطلب) ليبقى نشرُ الويب خفيفًا، ونظّفِ package-lock.json بـnpm install --package-lock-only.

ز‑4 · بناءٌ فاشلٌ يُبلّغُ «نجاحًا» بنسخةٍ قديمة

  • الحلّ: build_apk يَحذِفُ app-debug.apk قبل gradlew فلا يُنسَخَ APK قديمٌ عند الفشل.

ز‑5 · أيقونةُ أندرويد وشاشةُ البدء

  • المصدرُ assets/ (icon-foreground/background + splash/-dark). npx @capacitor/assets generate --android (داخل build_apk بعد cap sync) يكتبُ كلَّ المقاسات في res/mipmap-* (أيقونةٌ بلا نصّ) وres/drawable-* (شاشةٌ بالعنوان). android/ مُولَّدٌ (gitignored) فالأيقونةُ تُعاد كتابتُها من assets/ في كلّ بناء.

🧩 البنية · تكرارُ المنطق

ب‑1 · إصلاحٌ في الوحدةِ المركزيّةِ لا يصلُ صفحةً تحملُ نسختَها المحلّيّة (v1.6)

  • العَرَض: تُصلِحُ سلوكًا في src/versus.js ويعملُ في صفحةٍ (lang.html) ولا يعملُ في أخرى (play.html) — والمستخدمُ يُبلِّغُ أنّ العلّةَ باقية.
  • الجذر: play.html يحملُ نسخًا محلّيّةً من meCard/vsMakePlayers/vsSetup (ولعبةُ الذاكرةِ نسخةً ثالثةً منفصلة) فلا يستوردُ المركزيَّ أصلًا. بينما lang.html يستوردُه فيَرِثُ كلَّ إصلاحٍ مجّانًا.
  • الحلّ: قبلَ إعلانِ إصلاحٍ «شامل»، ابحثْ عن نسخٍ محلّيّةٍ للدالّةِ نفسِها (grep -n "function <name>") — وأصلِحْها أو (الأفضل) اجعلْها تستوردُ المركزيّ. التكرارُ هو مصدرُ هذا النوعِ من الأخطاء.
  • أثرٌ مشابهٌ عولِج: منطقُ «مفتاحِ الطرفَين» في سجلِّ التنافسِ كان مكرَّرًا؛ صُدِّرَ vsPairKey مصدرًا واحدًا يستعملُه التجميعُ والحذفُ معًا (اختلافُهما كان سيَحذفُ سجلًّا غيرَ المقصود).

ب‑2 · نداءُ دالّةٍ غيرِ موجودةٍ يُجهِضُ حلقةَ المؤقّتِ صامتًا (v1.6)

  • العَرَض: «اللعبُ ضدّ الوقت» ينتهي وقتُه فلا يحدثُ شيء: لا حركةَ تُلعَبُ ولا ينتقلُ الدورُ ولا يتفاعلُ الآليّ. بلا رسالةِ خطأٍ ظاهرة.
  • الجذر: sfx.wrong() — وsrc/sfx.js يُصدّرُ success/heal/nope/tap فقط. الاستثناءُ يقعُ داخلَ requestAnimationFrame فيُجهِضُ بقيّةَ الشيفرةِ بهدوء. بقيَ الخللُ في الذاكرةِ ودائرةٍ وعلامةَ منذ v1.5.4 دونَ اكتشاف (6 مواضعَ إجمالًا).
  • الحلّ: تحقّقْ من وجودِ الدالّةِ قبلَ استعمالِها. فحصٌ سريعٌ يكشفُ الكلَّ: node -e '…' قارِنْ sfx\.([a-z]+)\( في كلِّ الملفّاتِ بمُصدَّراتِ src/sfx.js.
  • قاعدةٌ عامّة: أيُّ استثناءٍ داخلَ rAF/مؤقّتٍ يبتلعُ بقيّةَ المنطق — لا تفترضْ أنّ «لا شيءَ يحدثُ» يعني منطقًا خاطئًا؛ افحصِ الطرفيّةَ (console) أوّلًا.

ب‑3 · صفحةٌ تظهرُ فارغةً — استدعاءُ إقلاعٍ قبلَ تعريفاتِ let/const (TDZ) (v1.7)

  • العَرَض: quran-full.html تُفتَحُ فارغةً تمامًا (لا أحزاب، لا رسالةَ بوّابة).
  • الجذر: كان الإقلاعُ (renderHizbs()) يُستدعى في وسطِ السكربت، فيَستدعي stopAll() التي تقرأُ let mode/seq المُعرَّفتَينِ لاحقًا في الملفّ. let/const مرفوعانِ لكنْ غيرُ مُهيَّأَينِ حتّى سطرِ تعريفِهما (منطقةُ الموتِ الزمنيّة TDZ) ⇒ ReferenceError: Cannot access 'mode' before initialization يُجهِضُ الوحدةَ كلَّها صامتًا فتبقى #view فارغة.
  • الحلّ: لُفَّ الإقلاعَ في دالّةٍ (boot()) تُستدعى في نهايةِ السكربت بعدَ كلِّ التعريفات — كما يفعلُ quran.html (سطرُ renderList() الأخير). القاعدة: أيُّ استدعاءٍ على المستوى الأعلى يَلمِسُ متغيّراتٍ يجب أن يأتيَ بعدَ تعريفِها، لا قبلَها. انظر #6-الأداءُ-الصامتُ-أسوأُ-من-العطلِ-الصريح (الاستثناءُ يبتلعُ المنطقَ صامتًا).

🗄️ التخزين · الحسابات

خ‑1 · بياناتُ التطوير «تختفي»

  • الجذر: التخزين (localStorage/IndexedDB) مرتبطٌ بالأصل/المنفذ؛ تغيّرُ منفذِ Vite يُخفيها.
  • الحلّ: server/preview.port=5174 + strictPort مثبَّتان في vite.config.js.

خ‑2 · تقدّمُ حسابٍ يطغى على آخر

  • الجذر: المفتاحُ tilmithi_progress_v1__<id> يُحسَب مرّةً عند الاستيراد.
  • الحلّ: لقراءة حسابٍ آخر استعمل getStatsFor(id)/loadProgressFor(id)؛ وداخل المفتاح الواحد استعمل نمطَ حمّل‑عدّل‑احفظ دائماً.

خ‑3 · لوحةُ الأهل تُكشَف منها حلقةُ «علّم الآليّ»

  • الجذر: الـoverlay لا يحجب الخلفيّة.
  • الحلّ: parentOnly يُعتِم الـoverlay؛ غيرُ الوالد يُطلَب كلمةَ المرور كلَّ مرّة (لا جلسةَ مؤقّتة)؛ الإغلاقُ يعيد للحساب الذي فُتحت منه.

🎨 الواجهة · السمة · التجاوب

و‑1 · عناصرُ فاتحةٌ في الوضع الداكن

  • الجذر: ألوانٌ مثبّتةٌ يدويّاً (inline أو #fff) لا تتبع المتغيّرات.
  • الحلّ: استعمل var(--card)/var(--bg)/var(--ink) (تقنيةُ «لا تغيير في الفاتح، يدكَن في الداكن»)، أو انتقاءات html[data-theme="dark"] بـ!important للـinline. مطبَّقٌ على القرآن (ورقٌ داكن) والاستوديو ومسجّل التمرّن وعناصر النماذج.

و‑2 · وميضُ السمة عند التحميل

  • الحلّ: سكربتٌ كلاسيكيٌّ صغيرٌ في <head> يضبط data-theme قبل الرسم + نمطٌ حرجٌ مضمَّن؛ السمةُ المشتركةُ في src/dark.css تُستورَد عبر theme.js.

و‑3 · أزرارٌ علويّةٌ متنافرةُ الأحجام تلتفُّ على الهاتف

  • الحلّ: src/topbar.css المشترَك (عبر theme.js): .topbtn بلا التفافِ نصّ + .iconbtn مربّعٌ موحّد + .topbtns يلتفُّ بأناقة + تصغيرٌ @media(max-width:430px).

و‑4 · classList.add("") يرمي استثناءً

  • الحلّ: احرسْ ضدّ القيمة الفارغة (كان سببَ تعطّل أزرار القراءة).

و‑5 · نصُّ <button> أسودُ خفيٌّ في الوضع الداكن

  • العَرَض: أزرارٌ مُنسَّقةٌ بخلفيّةٍ var(--card) يظهرُ نصُّها أسودَ (غيرَ مرئيٍّ) في الداكن (خياراتُ الساعة/الاتجاهات، بطاقاتُ «الزمان والمكان»، زرُّ «ابدأ الاختبار»، بطاقاتُ الصلاة).
  • الجذر: عنصرُ <button> لا يرثُ color افتراضًا (يأخذُ canvastext≈أسود)، بخلافِ <div>/<a> التي ترثُ لونَ الـbody. فبطاقةٌ بصنفٍ واحدٍ تظهرُ سليمةً كـdiv وخفيّةً كـbutton.
  • الحلّ: أضِفْ color:var(--ink) صراحةً لكلِّ صنفِ زرٍّ مُنسَّق (v1.5.2: clk-opt/or-opt/or-tile/or-per/mcard/pr-card). القاعدة: أيُّ <button> بخلفيّةِ بطاقةٍ يجب أن يضبطَ color صراحةً.

و‑6 · تخصيصُ لونِ الآليِّ عبرَ متغيّرِ CSS عالميّ + حقنُ الملحق (v1.7)

  • النمط: لونُ الآليِّ المخصَّصُ يُطبَّقُ بـdocument.documentElement.style.setProperty("--robo-body", …) (على الجذر) لا على كلِّ نسخةٍ على حدة، فيَسري تلقائيًّا على كلِّ آليٍّ في الصفحة (المرافقُ العائم + بطاقةُ الدخولِ + ظهرُ بطاقاتِ الذاكرة) بقاعدةٍ واحدة .rc-body{fill:var(--robo-body,#B8C0CC)}.
  • المزلق: قاعدةُ .rc-body تُحقَنُ ضمنَ STYLE المرافقِ عند تركيبِه فقط (robo.mount()). فقبلَ التركيب (شاشةُ الدخول) يبقى اللونُ الافتراضيَّ (fill المضمَّن) — مقبولٌ (ارتدادٌ سليم). ولإظهارِ اللونِ فورًا استُدعيَ applyRoboColor() مبكرًا في وحدةِ الصفحة.
  • الملحقاتُ (accessory): تُحقَنُ نصًّا في <g class="rc-acc"> داخلَ الـSVG عند التركيبِ وبعدَ كلِّ حفظ (robo.refresh()refreshLook()). رسومُها بإحداثيّاتِ viewBox 0 0 80 80. المُعرّفاتُ اللاتينيّةُ تبقى accessory بالكود، والعرضُ العربيُّ «ملحقات» لا «إكسسوارات».

و‑7 · التنقّلُ الصوتيّ للصغار — نمطُ النقرتَين (v1.7)

  • المشكلة: بطاقاتُ الفهرسِ <a href> تنتقلُ فورًا، فنطقُ اسمِ البطاقةِ (لغيرِ القارئ) يُقطَعُ بالانتقال.
  • الحلّ: حين يُفعَّلُ التوگل (isVoiceNavOn، مُطفأٌ افتراضيًّا): النقرةُ الأولى e.preventDefault() + نطقُ الاسمِ + صنفُ .vnav-armed (إبرازٌ + «👆 المِسْ ثانيةً»)، والنقرةُ الثانيةُ على البطاقةِ نفسِها (خلالَ 5ث) تَدَعُ الرابطَ يعمل. مُطفأٌ ⇒ تنقّلٌ بنقرةٍ واحدةٍ للأكبر. النمطُ على .navcard[href] فقط (البطاقاتُ التي تفتحُ نوافذَ في الصفحةِ لا تُقطَع).

و‑8 · وضعُ الاستماعِ الليليّ يَسري كلَّ الصفحاتِ عبرَ أثرِ الاستيراد (v1.7)

  • المطلوب: فلترٌ كهرمانيٌّ دافئٌ (html.night-mode) للاستماعِ قبلَ النوم، فعّالٌ في القرآنِ والقصصِ لا الفهرسَ وحدَه.
  • الحلّ: التوگلُ في الفهرسِ فقط، لكنّ applyNightMode() يُستدعى كأثرٍ جانبيٍّ عند استيرادِ sound-prefs (وحدةٌ يستوردُها كلُّ صفحة)، فيُطبَّقُ الصنفُ على <html> أينما فُتِح. آمنٌ في Node (كلُّه في try/catch، والوحدةُ للمتصفّحِ لا يستوردُها أيُّ أداةِ توليد).

🖼️ الأيقونات (SVG)

أ‑1 · توحيدُ أيقونات بطاقات الأقسام

  • الحلّ: src/icons.js (مولَّدٌ بـnpm run gen:icons من Font Awesome Free solid) يُصدّر ICONS (اسم→SVG) وEMOJI_ICON (إيموجي→اسم) وiconHtml(emoji) وinjectIcons(root). التلوينُ الحيويُّ لكلّ بطاقةٍ في src/nav-icons.css (--ik + currentColor). الإيموجي تبقى حلًّا احتياطيّاً.
  • الرخصة: Font Awesome Free — الأيقونات CC BY 4.0 (متوافقة مع GPL‑3.0)؛ انظر CREDITS.md.

أ‑2 · iconHtml(emoji) يأخذُ إيموجي لا مفتاحًا

  • العَرَض: iconHtml("clock") يطبعُ النصَّ «clock» بدل الأيقونة.
  • الجذر: iconHtml(emoji) يبحثُ في EMOJI_ICON[emoji]؛ المفتاحُ النصّيُّ لا يُطابَق فتُرجَعُ السلسلةُ كما هي.
  • الحلّ: للمفاتيحِ الدلاليّة استعمِلْ ICONS[key] مباشرةً (كما في clock.html: const icon=k=>ICONS[k]||"")، أو data-icon="key" + injectIcons(). مزلقٌ آخر: إضافةُ أيقونةٍ يدويًّا إلى icons.js (كـclock/backup/trophy…) تُمحى إن أُعيد npm run gen:icons دون إضافةِ اسمِها إلى MAP في tools/gen-fa-icons.mjs.

أ‑3 · إيموجي خامّةٌ تُعرَضُ بدلَ الأيقونةِ المعتمدة (v1.6)

  • العَرَض: بطاقاتُ الأقسامِ والألعابِ والأوسمةِ تُظهِرُ إيموجي النظامِ (تختلفُ بين أندرويدَ وسطحِ المكتبِ وقد تكونُ «تُفو»).
  • الجذر: رمزٌ لا تعيينَ له في EMOJI_ICON، أو حقلٌ يُعرَضُ مباشرةً دونَ iconHtml (كاقتطاعِ الرمزِ من نصِّ العنوان بـslice).
  • الحلّ: npm run check:icons — أداةٌ دائمةٌ تفحصُ data-icon وiconHtml وحقولَ ic: في كلِّ الصفحاتِ والوحدات وتفشلُ إن وُجِدَ رمزٌ غيرُ معتمَد. الرموزُ الجديدةُ تُضافُ إلى tools/gen-fa-icons.mjs (MAP+EMOJI) ثمّ npm run gen:iconsلا تُحرَّرْ src/icons.js فهو مولَّدٌ ويُدهَس. (كشفت 24 رمزًا؛ 77←92 أيقونة.)

أ‑4 · أيقونةٌ SVG تتمدّدُ فتبتلعُ البطاقة (v1.6)

  • العَرَض: أيقونةُ بطاقةٍ جديدةٍ تظهرُ ضخمةً بلا لون، بينما بطاقاتٌ أخرى سليمة.
  • الجذر: src/nav-icons.css يُقيّدُ ارتفاعَ الـSVG ويُلوّنُه لأصنافٍ بعينِها (.gcard/.lcard)؛ صنفُ البطاقةِ الجديدُ غيرُ مذكورٍ فيبقى الـSVG بلا قيد. وfont-size لا يؤثّرُ في <svg> بخلافِ الإيموجي النصّيّة.
  • الحلّ: أضِفِ الصنفَ الجديدَ إلى قواعدِ nav-icons.css (الحجمُ + اللونُ + لوحةُ الألوان) — لا ترقيعًا محلّيًّا في الصفحة.

📚 المحتوى والتوليد

م‑1 · المصدرُ الموحّد للنصوص المنطوقة

  • src/vocab.js يجمع كلَّ النصوص القصيرة (يقرؤه الاستوديو والمولّدات). أضِفِ المحتوى مرّةً فيتزامن. عبارات الآليّ في src/robo-phrases.js (REACTIONS + NOTICES) — أشِرْ إليها بالاسم لا بالنصّ المكرّر.
  • بعد أيِّ نصٍّ منطوقٍ جديد: npm run gen:audio (+ gen:piper إن لزم).

م‑3 · دمجُ صوتٍ بشريٍّ من ZIP (gen:voices) — تراكُمٌ لا استبدال

  • مزلق حرجٌ (فقدانُ تسجيلات): تصديرُ الاستوديو يَشمل تسجيلاتِ الجهاز فقط لا المُدمَجَ. فلو استبدلنا المجموعةَ بالتصدير ضاع المُدمَجُ القديمُ غيرُ الموجود على هذا الجهاز (حدَث: 632→337). الحلّ: gen-voices.mjs يُراكِم الآن: mergedFiles = {...prev.files, ...new} (يُبقي كلَّ القديم ويُضيف/يُحدّث الجديد). الحذفُ المقصودُ يدويّاً لا عبر تصديرٍ جزئيّ.
  • الاستردادُ عند الفقد: المقاطعُ القديمةُ محفوظةٌ في git (إن لم تُرفَع) ⇒ git show HEAD:.../voices-bundled.json لمدخلاتها + git checkout HEAD -- <مساراتها> لملفّاتها، ثمّ دمجُها (اتّحاد).
  • مزلق الترميز: unzip العاديّ يُفسِد اسمَ المجلّد العربيّ ⇒ استخرجْ بـpython مع استعادةِ UTF-8: name.encode('cp437').decode('utf-8').

م‑5 · لا تُغيّرْ مفاتيحَ المحتوى للتشكيل (يكسرُ مطابقةَ الصوت البشريّ)

  • العَرَض: بعد تشكيل أسماء الأعداد في المصدر، لم تَعُدْ تسجيلاتُ «صالح» (المُسجَّلةُ على المفاتيح القديمة) تُنطَق ⇒ ارتدادٌ للآليّ.
  • القاعدة: الصوتُ البشريُّ يُطابِقُ المفتاحَ حرفيّاً (مشكولاً أو لا)؛ فالمفتاحُ (نصُّ المحتوى) ثابتٌ كما يُعرَض/يُسجَّل. التشكيلُ للنطق الآليّ/العصبيّ يكون في التركيب فقط عبر spoken-ref.js (مفتاح→مرجعٌ مشكول) وforSynthesis — دون مساسِ المفتاح. (الأعدادُ مثالٌ: المفتاحُ «أربعة»، المرجعُ «أَرْبَعَة».)

م‑4 · تشكيلُ النصوص المعروضة المنطوقة

  • نصوصُ القصص/الدروس المعروضةُ المنطوقةُ يجب أن تكون مشكولةً بالكامل (قصّةُ الأرقام numerals.js كانت مجرّدةً جزئيّاً فأُسيءَ نطقُها). شكِّلِ المصدرَ (يفيدُ العرضَ والنطقَ معًا)، أو وفِّرْ مرجعًا مشكولًا في spoken-ref.js. انظر #ح‑7.

م‑2 · القصصُ المترجمة (اللغات)

  • ترجمةُ content/stories.js بنفس id/cover/art في content/lang-en/fr.js — أيُّ قصّةٍ جديدةٍ تُضاف نظيرتُها بنفس المفاتيح.

م‑6 · التقويمُ الهجريُّ الحسابيُّ تقريبيّ — للتهنئةِ لا للعبادة (v1.7)

  • السياق: بانرُ المناسبات (src/occasions.js) يُحوّلُ الميلاديَّ إلى هجريٍّ بخوارزميّةِ التقويم الجدوليّ (الكويتيّة، عبرَ رقمِ اليومِ اليوليانيّ) — محلّيٌّ بالكامل بلا إنترنتٍ ولا مكتبة.
  • المزلق: التقويمُ الجدوليُّ قد يختلفُ يومًا أو يومَين عن الرؤيةِ الشرعيّة/أمِّ القرى. دُقِّقَ على رمضان ١٤٤٦ والعيدَينِ ورأسِ السنةِ فكان مطابقًا، لكنّ الفوارقَ الحدّيّةَ واردة.
  • القاعدة: يُستعمَلُ للتهنئةِ والتذكيرِ العامِّ لا للعبادة (لا يُبنى عليه بدءُ صيامٍ ولا عيد). ملاحظةٌ صريحةٌ في رأسِ الملفّ. new Date() مسموحٌ في كودِ التطبيقِ (لا في سكربتاتِ Workflow).

م‑7 · home.html يستعملُ الأرقامَ الغربيّةَ مباشرةً بلا دالّةِ AR() (v1.7)

  • المزلق: صفحاتٌ أخرى قد تملكُ مُنسِّقَ أرقامٍ، أمّا home.html فلا دالّةَ AR() فيه — استعمالُها يرمي ReferenceError يُجهِضُ سكربتَ الوحدةِ صامتًا. اطبعِ الأرقامَ مباشرةً (${n}) موافقةً لسياسةِ الأرقامِ الغربيّةِ في الواجهة.

راجعْ ../CLAUDE.md للنظرة المعماريّة المكثّفة، وPLAN.md لسجلّ العمل المؤرَّخ.

Clone this wiki locally