diff --git a/web/src/i18n/ar.ts b/web/src/i18n/ar.ts new file mode 100644 index 00000000000..0d961d9b49e --- /dev/null +++ b/web/src/i18n/ar.ts @@ -0,0 +1,701 @@ +import { defineLocale } from "./define-locale"; + +export const ar = defineLocale({ + common: { + save: "حفظ", + saving: "جاري الحفظ...", + cancel: "إلغاء", + close: "إغلاق", + confirm: "تأكيد", + delete: "حذف", + refresh: "تحديث", + retry: "إعادة المحاولة", + search: "بحث...", + loading: "جاري التحميل...", + create: "إنشاء", + creating: "جاري الإنشاء...", + set: "تعيين", + replace: "استبدال", + clear: "مسح", + live: "مباشر", + off: "مغلق", + enabled: "مفعّل", + disabled: "معطّل", + active: "نشط", + inactive: "غير نشط", + unknown: "غير معروف", + untitled: "بدون عنوان", + none: "لا يوجد", + form: "نموذج", + noResults: "لا توجد نتائج", + of: "من", + page: "صفحة", + msgs: "رسائل", + tools: "أدوات", + match: "مطابقة", + other: "آخر", + configured: "مكوَّن", + removed: "مزال", + failedToToggle: "فشل التبديل", + failedToRemove: "فشل الحذف", + failedToReveal: "فشل الإظهار", + collapse: "طي", + expand: "توسيع", + general: "عام", + messaging: "مراسلة", + pluginLoadFailed: + "تعذر تحميل سكربت هذا المكوِّن الإضافي. تفقَّد علامة الشبكة (dashboard-plugins/…) ومسار ملحقات الخادم.", + pluginNotRegistered: + "لم يستدعِ سكربت المكوِّن الإضافي register()، أو حدث خطأ في السكربت. افتح وحدة تحكم المتصفح للتفاصيل.", + }, + + app: { + brand: "Hermes Agent", + brandShort: "HA", + closeNavigation: "إغلاق التنقل", + closeModelTools: "إغلاق النموذج والأدوات", + footer: { + org: "Nous Research", + }, + activeSessionsLabel: "الجلسات النشطة:", + gatewayStatusLabel: "حالة البوابة:", + gatewayStrip: { + failed: "فشل البدء", + off: "مغلق", + running: "يعمل", + starting: "قيد البدء", + stopped: "متوقف", + }, + nav: { + analytics: "التحليلات", + chat: "المحادثة", + config: "الإعدادات", + cron: "المهام المجدولة", + documentation: "التوثيق", + keys: "المفاتيح", + logs: "السجلات", + models: "النماذج", + profiles: "الملفات الشخصية", + plugins: "المكوِّنات الإضافية", + sessions: "الجلسات", + skills: "المهارات", + }, + modelToolsSheetSubtitle: "& أدوات", + modelToolsSheetTitle: "النموذج", + navigation: "التنقل", + openDocumentation: "فتح التوثيق في تبويب جديد", + openNavigation: "فتح التنقل", + pluginNavSection: "المكوِّنات الإضافية", + sessionsActiveCount: "{count} نشطة", + statusOverview: "نظرة عامة على الحالة", + system: "النظام", + webUi: "واجهة الويب", + }, + + status: { + actionFailed: "فشلت الإجراء", + actionFinished: "انتهى", + actions: "إجراءات", + agent: "العامل", + activeSessions: "الجلسات النشطة", + connected: "متصل", + connectedPlatforms: "المنصات المتصلة", + disconnected: "غير متصل", + error: "خطأ", + failed: "فشل", + gateway: "البوابة", + gatewayFailedToStart: "فشل بدء البوابة", + lastUpdate: "آخر تحديث", + noneRunning: "لا شيء", + notRunning: "لا يعمل", + pid: "PID", + platformDisconnected: "غير متصل", + platformError: "خطأ", + recentSessions: "الجلسات الأخيرة", + restartGateway: "إعادة تشغيل البوابة", + restartingGateway: "إعادة تشغيل البوابة…", + running: "يعمل", + runningRemote: "يعمل (عن بعد)", + startFailed: "فشل البدء", + starting: "قيد البدء", + startedInBackground: "بدء في الخلفية — تحقق من السجلات للتقدم", + stopped: "متوقف", + updateHermes: "تحديث Hermes", + updatingHermes: "جاري تحديث Hermes…", + waitingForOutput: "في انتظار الناتج…", + }, + + sessions: { + title: "الجلسات", + searchPlaceholder: "بحث في محتوى الرسائل...", + noSessions: "لا توجد جلسات حتى الآن", + noMatch: "لا توجد جلسات تطابق بحثك", + startConversation: "ابدأ محادثة لتظهر هنا", + noMessages: "لا توجد رسائل", + untitledSession: "جلسة بدون عنوان", + deleteSession: "حذف الجلسة", + confirmDeleteTitle: "حذف الجلسة؟", + confirmDeleteMessage: + "سيزيل هذا نهائيًا المحادثة وجميع رسائلها. لا يمكن التراجع عن هذا الإجراء.", + sessionDeleted: "تم حذف الجلسة", + failedToDelete: "فشل حذف الجلسة", + resumeInChat: "استئناف في المحادثة", + previousPage: "الصفحة السابقة", + nextPage: "الصفحة التالية", + roles: { + user: "المستخدم", + assistant: "المساعد", + system: "النظام", + tool: "أداة", + }, + }, + + analytics: { + period: "الفترة:", + totalTokens: "إجمالي الرموز", + totalSessions: "إجمالي الجلسات", + apiCalls: "استدعاءات API", + dailyTokenUsage: "استخدام الرموز اليومي", + dailyBreakdown: "التفصيل اليومي", + perModelBreakdown: "التفصيل حسب النموذج", + topSkills: "أعلى المهارات استخدامًا", + skill: "مهارة", + loads: "تم تحميل العامل", + edits: "تم إدارة العامل", + lastUsed: "آخر استخدام", + input: "إدخال", + output: "إخراج", + total: "الإجمالي", + noUsageData: "لا توجد بيانات استخدام لهذه الفترة", + startSession: "ابدأ جلسة لرؤية التحليلات هنا", + date: "التاريخ", + model: "النموذج", + tokens: "رموز", + perDayAvg: "في اليوم (متوسط)", + acrossModels: "عبر {count} نموذج", + inOut: "{input} إدخال / {output} إخراج", + }, + + models: { + modelsUsed: "النماذج المستخدمة", + estimatedCost: "التكلفة التقريبية", + tokens: "رموز", + sessions: "جلسات", + avgPerSession: "متوسط/جلسة", + apiCalls: "استدعاءات API", + toolCalls: "استدعاءات الأدوات", + noModelsData: "لا توجد بيانات نموذج لهذه الفترة", + startSession: "ابدأ جلسة لرؤية بيانات النماذج هنا", + }, + + logs: { + title: "السجلات", + autoRefresh: "تحديث تلقائي", + file: "ملف", + level: "المستوى", + component: "المكوِّن", + lines: "الأسطر", + noLogLines: "لم يُعثر على أسطر سجل", + }, + + cron: { + confirmDeleteMessage: + "يزيل هذا المهمة من الجدولة. لا يمكن التراجع عن الإجراء.", + confirmDeleteTitle: "حذف المهمة المجدولة؟", + newJob: "مهمة cron جديدة", + nameOptional: "الاسم (اختياري)", + namePlaceholder: "مثال: ملخص يومي", + prompt: "المطالبة", + promptPlaceholder: "ماذا يجب أن يفعل العامل في كل تشغيل؟", + schedule: "الجدولة (تعبير cron)", + schedulePlaceholder: "0 9 * * *", + deliverTo: "التسليم إلى", + scheduledJobs: "المهام المجدولة", + noJobs: "لم يتم تكوين أي مهام cron. أنشئ واحدة أعلاه.", + last: "آخر", + next: "التالي", + pause: "إيقاف مؤقت", + resume: "استئناف", + triggerNow: "تشغيل الآن", + delivery: { + local: "محلي", + telegram: "Telegram", + discord: "Discord", + slack: "Slack", + email: "البريد الإلكتروني", + }, + }, + + profiles: { + newProfile: "ملف شخصي جديد", + name: "الاسم", + namePlaceholder: "مثال: coder, writer, إلخ.", + nameRequired: "الاسم مطلوب", + nameRule: + "حروف صغيرة، أرقام، _ و - فقط؛ يجب البدء بحرف أو رقم؛ حتى 64 حرفًا.", + invalidName: "اسم ملف شخصي غير صالح", + allProfiles: "الملفات الشخصية", + noProfiles: "لم يُعثر على ملفات شخصية.", + defaultBadge: "افتراضي", + hasEnv: "env", + model: "النموذج", + skills: "المهارات", + rename: "إعادة تسمية", + editSoul: "تعديل SOUL.md", + soulSection: "SOUL.md (شخصية / موجه النظام)", + soulPlaceholder: "# كيف يجب أن يتصرف هذا العامل…", + saveSoul: "حفظ SOUL", + soulSaved: "تم حفظ SOUL.md", + openInTerminal: "نسخ أمر CLI", + commandCopied: "تم النسخ إلى الحافظة", + copyFailed: "تعذر النسخ", + confirmDeleteTitle: "حذف الملف الشخصي؟", + confirmDeleteMessage: + "هذا يحذف نهائيًا الملف الشخصي '{name}' — الإعدادات، المفاتيح، الذكريات، الجلسات، المهارات، مهام cron. لا يمكن التراجع عن الإجراء.", + created: "تم الإنشاء", + deleted: "تم الحذف", + renamed: "تمت إعادة التسمية", + }, + + pluginsPage: { + contextEngineLabel: "محرك السياق", + dashboardSlots: "فتحات لوحة التحكم", + disableRuntime: "تعطيل", + enableAfterInstall: "تفعيل بعد التثبيت", + enableRuntime: "تفعيل", + forceReinstall: "إعادة تثبيت إجباري (حذف المجلد الموجود أولاً)", + headline: + "اكتشف وثبِّت وفعِّل وحدِّث مكوِّنات Hermes الإضافية (مطابقة `hermes plugins`).", + identifierLabel: "رابط Git أو owner/repo", + inactive: "غير نشط", + installBtn: "تثبيت من Git", + installHeading: "تثبيت من GitHub / رابط Git", + installHint: "استخدم اختصار owner/repo أو رابط استنساخ كامل يبدأ بـ https:// أو git@.", + memoryProviderLabel: "مزوِّد الذاكرة", + missingEnvWarn: "عيّن هذه المتغيرات في المفاتيح قبل أن يتمكن المكوِّن الإضافي من العمل:", + noDashboardTab: "لا يوجد تبويب لوحة تحكم", + openTab: "فتح", + orphanHeading: "امتدادات لوحة التحكم فقط (لا يوجد تطابق plugin.yaml للعامل)", + pluginListHeading: "المكوِّنات الإضافية المثبتة", + providerDefaults: "مدمج / افتراضي", + providersHeading: "مكوِّنات مزوِّدات وقت التشغيل", + providersHint: + "يكتب memory.provider (فارغ = مدمج) و context.engine إلى config.yaml. يسري في الجلسة التالية.", + refreshDashboard: "إعادة فحص امتدادات لوحة التحكم", + removeConfirm: "إزالة هذا المكوِّن الإضافي من ~/.hermes/plugins/؟", + removeHint: "يمكن إزالة المكوِّنات المثبتة من المستخدم تحت ~/.hermes/plugins فقط.", + rescanHeading: "سجل مكوِّنات SPA", + rescanHint: "أعد الفحص بعد إضافة ملفات على القرص لتمكن الشريط الجانبي من التقاط المخطوطات الجديدة.", + runtimeHeading: "وقت تشغيل البوابة (مكوِّنات YAML)", + saveProviders: "حفظ إعدادات مزوِّد الذاكرة", + savedProviders: "تم حفظ إعدادات مزوِّد الذاكرة.", + sourceBadge: "المصدر", + authRequired: "مصادقة مطلوبة", + authRequiredHint: "شغِّل هذا الأمر للمصادقة:", + updateGit: "سحب من Git", + versionBadge: "الإصدار", + showInSidebar: "إظهار في الشريط الجانبي", + hideFromSidebar: "إخفاء من الشريط الجانبي", + }, + + skills: { + title: "المهارات", + searchPlaceholder: "بحث في المهارات ومجموعات الأدوات...", + enabledOf: "{enabled}/{total} مفعلة", + all: "الكل", + categories: "الفئات", + filters: "الفلاتر", + noSkills: "لم يُعثر على مهارات. يتم تحميل المهارات من ~/.hermes/skills/", + noSkillsMatch: "لا توجد مهارات تطابق بحثك أو الفلاتر.", + skillCount: "{count} مهارة{s}", + resultCount: "{count} نتيجة{s}", + noDescription: "لا يوجد وصف متاح.", + toolsets: "مجموعات الأدوات", + toolsetLabel: "مجموعة أدوات {name}", + noToolsetsMatch: "لا توجد مجموعات أدوات تطابق البحث.", + setupNeeded: "يلزم الإعداد", + disabledForCli: "معطَّل لواجهة CLI", + more: "+{count} المزيد", + }, + + config: { + configPath: "~/.hermes/config.yaml", + filters: "الفلاتر", + sections: "الأقسام", + exportConfig: "تصدير الإعدادات كـ JSON", + importConfig: "استيراد الإعدادات من JSON", + resetDefaults: "إعادة التعيين للافتراضيات", + resetScopeTooltip: "إعادة تعيين {scope} للافتراضيات", + confirmResetScope: + "إعادة تعيين جميع إعدادات {scope} للافتراضيات؟ هذا يحدِّث النموذج فقط — لا تُكتب التغييرات في config.yaml حتى تضغط على حفظ.", + resetScopeToast: "تمت إعادة تعيين {scope} للافتراضيات — راجع واحفظ للتثبيت", + rawYaml: "إعدادات YAML الأولية", + searchResults: "نتائج البحث", + fields: "حقل{s}", + noFieldsMatch: 'لا توجد حقول تطابق "{query}"', + configSaved: "تم حفظ الإعدادات", + yamlConfigSaved: "تم حفظ إعدادات YAML", + failedToSave: "فشل الحفظ", + failedToSaveYaml: "فشل حفظ YAML", + failedToLoadRaw: "فشل تحميل الإعدادات الأولية", + configImported: "تم استيراد الإعدادات — راجع واحفظ", + invalidJson: "ملف JSON غير صالح", + categories: { + general: "عام", + agent: "العامل", + terminal: "الطرفية", + display: "العرض", + delegation: "التفويض", + memory: "الذاكرة", + compression: "الضغط", + security: "الأمان", + browser: "المتصفح", + voice: "الصوت", + tts: "تحويل النص إلى كلام", + stt: "تحويل الكلام إلى نص", + logging: "التسجيل", + discord: "Discord", + auxiliary: "إضافي", + }, + }, + + env: { + changesNote: "يتم حفظ التغييرات على القرص فورًا. تلتقط الجلسات النشطة المفاتيح الجديدة تلقائيًا.", + confirmClearMessage: + "سيتم إزالة القيمة المخزنة لهذا المتغير من ملف .env. لا يمكن التراجع عن هذا من الواجهة.", + confirmClearTitle: "مسح هذا المفتاح؟", + description: "إدارة مفاتيح API والأسرار المخزَّنة في", + hideAdvanced: "إخفاء المتقدمة", + showAdvanced: "إظهار المتقدمة", + llmProviders: "مزوِّدو LLM", + providersConfigured: "{configured} من {total} مزوِّدين مكوَّنين", + getKey: "الحصول على المفتاح", + notConfigured: "{count} غير مكوَّن", + notSet: "غير معيّن", + keysCount: "{count} مفتاح{s}", + enterValue: "أدخل القيمة...", + replaceCurrentValue: "استبدال القيمة الحالية ({preview})", + showValue: "إظهار القيمة الحقيقية", + hideValue: "إخفاء القيمة", + }, + + oauth: { + title: "عمليات تسجيل دخول المزوِّدين (OAuth)", + providerLogins: "عمليات تسجيل دخول المزوِّدين (OAuth)", + description: + "{connected} من {total} مزوِّدي OAuth متصلون. تتدفق عمليات تسجيل الدخول حاليًا عبر CLI؛ انسخ الأمر وألصقه في طرفية للإعداد.", + connected: "متصل", + expired: "منتهي الصلاحية", + notConnected: "غير متصل. شغِّل {command} في طرفية.", + runInTerminal: "في طرفية.", + noProviders: "لم يُكتشف أي مزوِّد يدعم OAuth.", + login: "تسجيل الدخول", + disconnect: "قطع الاتصال", + managedExternally: "مدار خارجيًا", + copied: "تم النسخ ✓", + cli: "CLI", + copyCliCommand: "نسخ أمر CLI (لخارجي / احتياطي)", + connect: "اتصال", + sessionExpires: "تنتهي صلاحية الجلسة خلال {time}", + initiatingLogin: "بدء تدفق تسجيل الدخول…", + exchangingCode: "تبديل الرمز برموز الوصول…", + connectedClosing: "متصل! إغلاق…", + loginFailed: "فشل تسجيل الدخول.", + sessionExpired: "انتهت صلاحية الجلسة. انقر إعادة المحاولة لبدء تسجيل دخول جديد.", + reOpenAuth: "إعادة فتح صفحة المصادقة", + reOpenVerification: "إعادة فتح صفحة التحقق", + submitCode: "تقديم الرمز", + pasteCode: "ألصق رمز التفويض (مع لاحقة #state مقبولة)", + waitingAuth: "في انتظار تفويضك في المتصفح…", + enterCodePrompt: "فتح تبويب جديد. أدخل هذا الرمز عند الطلب:", + pkceStep1: "فتح تبويب جديد إلى claude.ai. سجِّل الدخول وانقر تفويض.", + pkceStep2: "انسخ رمز التفويض المعروض بعد التفويض.", + pkceStep3: "ألصقه أدناه وقدِّم.", + flowLabels: { + pkce: "تسجيل دخول المتصفح (PKCE)", + device_code: "رمز الجهاز", + external: "CLI خارجي", + }, + expiresIn: "تنتهي خلال {time}", + }, + + language: { + switchTo: "التبديل إلى الإنجليزية", + }, + + theme: { + title: "السمة", + switchTheme: "تبديل السمة", + }, + + achievements: { + hero: { + kicker: "Agentic Gamerscore", + title: "إنجازات Hermes", + subtitle: + "شارات Hermes قابلة للجمع مكتسبة من سجل الجلسات الفعلي. الإنجازات غير المكتملة المعروفة تُعرض كـ Discovered؛ تبقى الإنجازات السرية مخفية حتى يظهر السلوك المطابق لأول مرة.", + scan_subtitle: + "فحص سجل جلسات Hermes. يمكن أن يستغرق الفحص الأول 5–10 ثوانٍ على السجلات الكبيرة.", + }, + actions: { + rescan: "إعادة الفحص", + }, + stats: { + unlocked: "مفتوحة", + unlocked_hint: "شارات مكتسبة", + discovered: "مكتشفة", + discovered_hint: "معروفة، لم تُكتسب بعد", + secrets: "أسرار", + secrets_hint: "مخفية حتى أول إشارة", + highest_tier: "أعلى مستوى", + highest_tier_hint: "نحاس → فضة → ذهب ← ماس → أوليمبي", + latest: "الأحدث", + latest_hint_empty: "شغِّل Hermes أكثر", + none_yet: "لا توجد بعد", + }, + state: { + unlocked: "مفتوح", + discovered: "مكتشف", + secret: "سري", + }, + tier: { + target: "الهدف {tier}", + hidden: "مخفي", + complete: "مكتمل", + objective: "الهدف", + }, + progress: { + hidden: "مخفي", + }, + scan: { + building_headline: "إنشاء ملف الإنجاز…", + building_detail: + "قراءة الجلسات، استدعاءات الأدوات، بيانات تعريف النموذج، وحالة الفتح.", + starting_headline: "بدء فحص الإنجازات…", + progress_detail: + "تم فحص {scanned} من {total} جلسة · {pct}%. تنفتح الشارات مع تدفق المزيد من السجل.", + idle_detail: + "قراءة الجلسات، استدعاءات الأدوات، بيانات تعريف النموذج، وحالة الفتح. تظهر الشارات هنا عند فتحها.", + }, + guide: { + tiers_header: "المستويات", + secret_header: "الإنجازات السرية", + secret_body: + "تخفى الأسرار محددها الدقيق. بمجرد أن ترى Hermes إشارة ذات صلة، تصبح البطاقة مكتشفة وتعرض متطلباتها.", + scan_status_header: "حالة الفحص", + scan_status_body: + "تُفحص Hermes السجل المحلي مرة واحدة، ثم تظهر البطاقات تلقائيًا. لا يوجد توقف إذا استغرق هذا بضع ثوانٍ.", + what_scanned_header: "ما يتم فحصه", + what_scanned_body: + "الجلسات، استدعاءات الأدوات، بيانات تعريف النموذج، الأخطاء، الإنجازات، وحالة الفتح المحلية.", + }, + card: { + share_title: "مشاركة هذا الإنجاز", + share_label: "مشاركة {name}", + share_text: "مشاركة", + how_to_reveal: "كيفية الإظهار", + what_counts: "ما يُحتسب", + evidence_label: "دليل", + evidence_session_fallback: "جلسة", + no_evidence: "لا دليل بعد", + }, + latest: { + header: "آخر الفتوحات", + }, + empty: { + no_secrets_header: "لا توجد أسرار مخفية متبقية في هذا الفحص.", + no_secrets_body: + "تلميح: تبدأ الأسرار عادة من أنماط الفشل غير العادية أو أنماط المستخدم المتقدم — تعارضات المنافذ، حواجز الأذونات، متغيرات بيئة مفقودة، أخطاء YAML، تصادمات Docker، استخدام الإرجاع أو التبقي، ضربات التخزين المؤقت، أو إصلاحات صغيرة بعد الكثير من الأخطاء الحمراء." + }, + filters: { + all_categories: "الكل", + visibility_all: "الكل", + visibility_unlocked: "مفتوح", + visibility_discovered: "مكتشف", + visibility_secret: "سري", + }, + share: { + dialog_label: "مشاركة الإنجاز", + header: "مشاركة: {name}", + close: "إغلاق", + rendering: "جاري العرض…", + card_alt: "بطاقة مشاركة {name}", + error_generic: "حدث خطأ ما.", + x_title: "يفتح X مع منشومعد مسبقًا", + x_button: "مشاركة على X", + copy_title: "نسخ الصورة للصقها في منشورك", + copy_button: "نسخ الصورة", + copied: "تم النسخ ✓", + download_button: "تنزيل PNG", + hint: + "المشاركة على X تفتح منشورًا معدَّلاً مسبقًا في تبويب جديد. انقر نسخ الصورة أولاً إذا أردت شارة الإنجاز 1200×630 مرفقة — يسمح X باللصق مباشرة في مؤلف التغريد. تنزيل PNG يحفظ الملف للاستخدام anywhere.", + clipboard_unsupported: + "نسخ صورة الحافظة غير مدعوم في هذا المتصفح — استخدم التنزيل بدلاً من ذلك.", + tweet_text: "فتحت للتو {tier_part}\"{name}\" في Hermes Agent ☤" + }, + }, + + kanban: { + loading: "جاري تحميل لوحة Kanban…", + loadFailed: "فشل تحميل لوحة Kanban: ", + loadFailedHint: + "يُنشئ الواجهة الخلفية kanban.db تلقائيًا عند أول قراءة. إذا استمرت المشكلة، تفقَّد سجلات لوحة التحكم.", + board: "لوحة", + newBoard: "+ لوحة جديدة", + newBoardTitle: "لوحة جديدة", + newBoardDescription: + "تتيح اللوحات فصل تدفقات العمل غير المرتبطة — واحدة لكل مشروع أو مستودع أو مجال.", + slug: "المعرِّف", + slugHint: "— أحرف صغيرة، واصلات، مثال atm10-server", + displayName: "الاسم المعروض", + displayNameHint: "(اختياري)", + description: "الوصف", + descriptionHint: "(اختياري)", + icon: "أيقونة", + iconHint: "(حرف واحد أو رمز تعبيري)", + switchAfterCreate: "التبديل إلى هذه اللوحة بعد إنشائها", + cancel: "إلغاء", + creating: "جاري الإنشاء…", + createBoard: "إنشاء لوحة", + search: "بحث", + filterCards: "تصفية البطاقات…", + tenant: "المستأجر", + allTenants: "كل المستأجرين", + assignee: "المكلَّف", + allProfiles: "كل الملفات الشخصية", + showArchived: "إظهار المؤرشفة", + lanesByProfile: "مسارات حسب الملف الشخصي", + nudgeDispatcher: "دفع المُجلِّب", + refresh: "تحديث", + selected: "محدد", + complete: "إكمال", + archive: "أرشفة", + apply: "تطبيق", + clear: "مسح", + createTask: "إنشاء مهمة في هذا العمود", + noTasks: "— لا توجد مهام —", + unassigned: "غير مكلَّف", + needsAssignee: "يحتاج مكلِّف", + needsAssigneeHint: + "المتطلبات مُلباة، لكن المُجلِّب يتخطى هذه المهمة حتى تعيّن ملفًا شخصيًا.", + untitled: "(بدون عنوان)", + loadingDetail: "جاري التحميل…", + addComment: "إضافة تعليق… (Enter للتقديم)", + comment: "تعليق", + status: "الحالة", + workspace: "مساحة العمل", + skills: "المهارات", + createdBy: "أنشئ بواسطة", + result: "النتيجة", + comments: "التعليقات", + events: "الأحداث", + runHistory: "سجل التنفيذ", + workerLog: "سجل العامل", + loadingLog: "جاري تحميل السجل…", + noWorkerLog: + "— لا يوجد سجل عامل بعد (لم تُنشَ المهمة أو تم تدوير السجل) —", + noDescription: "— لا يوجد وصف —", + noComments: "— لا توجد تعليقات —", + edit: "تعديل", + save: "حفظ", + dependencies: "الاعتماديات", + parents: "الآباء:", + children: "الأبناء:", + none: "لا شيء", + addParent: "— إضافة أب —", + addChild: "— إضافة ابن —", + removeDependency: "إزالة الاعتماد", + block: "حظر", + unblock: "إلغاء الحظر", + notifyHomeChannels: "إشعار القنوات الرئيسية", + diagnostics: "التشخيصات", + hide: "إخفاء", + show: "إظهار", + attention: "انتباه", + tasksNeedAttention: "مهام تحتاج انتباه", + taskNeedsAttention: "مهمة واحدة تحتاج انتباه", + diagnostic: "تشخيصي", + open: "فتح", + close: "إغلاق (Esc)", + reassignTo: "إعادة تعيين إلى:", + copied: "تم النسخ", + copyCommand: "نسخ الأمر إلى الحافظة", + reclaim: "استعادة", + reassign: "إعادة تعيين", + renderingError: "اصطدم تبويب Kanban بخطأ عرض", + reloadView: "إعادة تحميل العرض", + wsAuthFailed: + "فشلت مصادقة WebSocket — أعد تحميل الصفحة لتحديث رمز الجلسة.", + markDone: "تحديد {n} مهمة كمكتملة؟", + markArchived: "أرشفة {n} مهمة؟", + warning: "تحذير", + phantomIds: "معرفات وهمية:", + active: "نشط", + ended: "منتهٍ", + noProfile: "(لا يوجد ملف شخصي)", + showAllAttempts: "إظهار كل المحاولات", + sendingUpdates: "إرسال التحديثات إلى", + sendNotifications: "إرسال إشعارات مكتمل / محظور / متروك لـ", + archiveBoardConfirm: + "أرشفة لوحة '{name}'؟ ستُنقَل إلى boards/_archived/ لتتمكن من استرجاعها لاحقًا. المهام على هذه اللوحة لن تظهر بعد الآن في أي مكان في الواجهة.", + archiveBoardTitle: "أرشفة هذه اللوحة", + boardSwitcherHint: "تتيح اللوحات فصل تدفقات عمل غير مرتبطة", + taskCreatedWarning: "تم إنشاء المهمة، لكن: ", + moveFailed: "فشل النقل: ", + bulkFailed: "مجموعة: ", + completionBlockedHallucination: "⚠ الحظر — معرّفات بطاقات وهمية", + suspectedHallucinatedReferences: "⚠ النص يشير إلى معرّفات بطاقات وهمية", + pickProfileFirst: "اختر ملفًا شخصيًا أولاً.", + unblockedMessage: "تم إلغاء حظر {id}. المهمة جاهزة للدورة التالية.", + unblockFailed: "فشل إلغاء الحظر: ", + reclaimedMessage: "تم استعادة {id}. المهمة عادت إلى جاهزة.", + reclaimFailed: "فشل الاستعادة: ", + reassignedMessage: "تم إعادة تعيين {id} إلى {profile}.", + reassignFailed: "فشل إعادة التعيين: ", + selectForBulk: "تحديد لإجراءات جماعية", + clickToEdit: "انقر للتعديل", + clickToEditAssignee: "انقر لتعديل المكلَّف", + emptyAssignee: "(فارغ = إزالة التكليف)", + columnLabels: { + triage: "فرز", + todo: "مهام", + ready: "جاهز", + running: "قيد التنفيذ", + blocked: "محظور", + done: "منجز", + archived: "مؤرشف", + }, + columnHelp: { + triage: "أفكار أولية — سيصِغ المُحدِّد المواصفات", + todo: "في انتظار الاعتماديات أو غير مكلَّف", + ready: "الاعتماديات مُلباة؛ عيّن ملفًا شخصيًا للتشغيل", + running: "مطالب بعامل — قيد التنفيذ", + blocked: "طلب العامل مدخلات من الإنسان", + done: "مكتمل", + archived: "مؤرشف", + }, + confirmDone: + "تحديد هذه المهمة كمكتملة؟ سيتم تحرير مطالبة العامل ويصبح الأبناء المعتمدون جاهزين.", + confirmArchive: + "أرشفة هذه المهمة؟ ستختفي من عرض اللوحة الافتراضي.", + confirmBlocked: + "تحديد هذه المهمة كمحظورة؟ سيتم تحرير مطالبة العامل.", + completionSummary: + "ملخص الإنجاز لـ {label}. يُخزَّن كنتيجة للمهمة.", + completionSummaryRequired: + "ملخص الإنجاز مطلوب قبل تحديد المهمة كمكتملة.", + triagePlaceholder: "فكرةrough — سيقوم الذكاء الاصطناعي بصياغة المواصفات…", + taskTitlePlaceholder: "عنوان المهمة الجديد…", + specifier: "محدِّد المواصفات", + assigneePlaceholder: "المكلَّف", + priority: "الأولوية", + skillsPlaceholder: + "مهارات (اختياري، مفصولة بفواصل): translation, github-code-review", + noParent: "— لا يوجد أب —", + workspacePathDir: "مسار مساحة العمل (مطلوب، مثال ~/projects/my-app)", + workspacePathOptional: + "مسار مساحة العمل (اختياري، مستنتج من المكلَّف إذا كان فارغًا)", + logTruncated: "(عرض آخر 100 كيلوبايت — السجل الكامل في ", + logAt: ")", + }, +}); diff --git a/web/src/i18n/context.tsx b/web/src/i18n/context.tsx index aa7a219b924..e25b0cc4b50 100644 --- a/web/src/i18n/context.tsx +++ b/web/src/i18n/context.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react"; import type { Locale, Translations } from "./types"; import { en } from "./en"; import { zh } from "./zh"; @@ -16,6 +16,7 @@ import { ga } from "./ga"; import { pt } from "./pt"; import { ru } from "./ru"; import { hu } from "./hu"; +import { ar } from "./ar"; const TRANSLATIONS: Record = { en, @@ -34,8 +35,13 @@ const TRANSLATIONS: Record = { pt, ru, hu, + ar, }; +// Locales whose script flows right-to-left. Consumed by the provider to set the +// document direction so Tailwind's logical utilities (ms-/me-, ps-/pe-) flip. +const RTL_LOCALES = new Set(["ar"]); + // Display metadata for the language picker — endonym (native name) so users // recognize their language even if they don't speak the current UI language. // Exposed as a constant so the LanguageSwitcher and any future settings page @@ -62,6 +68,7 @@ export const LOCALE_META: Record = { pt: { name: "Português" }, ru: { name: "Русский" }, hu: { name: "Magyar" }, + ar: { name: "العربية" }, }; const SUPPORTED_LOCALES = Object.keys(TRANSLATIONS) as Locale[]; @@ -105,6 +112,12 @@ export function I18nProvider({ children }: { children: ReactNode }) { } }, []); + useEffect(() => { + if (typeof document === "undefined") return; + document.documentElement.lang = locale; + document.documentElement.dir = RTL_LOCALES.has(locale) ? "rtl" : "ltr"; + }, [locale]); + const value: I18nContextValue = { locale, setLocale, diff --git a/web/src/i18n/define-locale.ts b/web/src/i18n/define-locale.ts new file mode 100644 index 00000000000..401a0cccb56 --- /dev/null +++ b/web/src/i18n/define-locale.ts @@ -0,0 +1,46 @@ +import { en } from "./en"; +import type { Translations } from "./types"; + +// Partial-locale helper: a translation file supplies only the strings it has +// translated and every missing key falls back to English, while unknown keys +// still fail the type-check. Mirrors the desktop app's `defineLocale` so a new +// locale (e.g. Arabic) can land without hand-porting every future English key. + +type TranslationOverride = T extends (...args: never[]) => string + ? T + : T extends readonly unknown[] + ? T + : T extends string + ? string + : T extends object + ? { [K in keyof T]?: TranslationOverride } + : T; + +export type TranslationOverrides = TranslationOverride; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function mergeTranslations(base: T, overrides: TranslationOverride | undefined): T { + if (!isRecord(base) || !isRecord(overrides)) { + return (overrides ?? base) as T; + } + + const result: Record = { ...base }; + + for (const [key, value] of Object.entries(overrides)) { + if (value === undefined) { + continue; + } + + const baseValue = result[key]; + result[key] = isRecord(baseValue) && isRecord(value) ? mergeTranslations(baseValue, value) : value; + } + + return result as T; +} + +export function defineLocale(overrides: TranslationOverrides): Translations { + return mergeTranslations(en, overrides); +} diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 52fd86acf6a..1991a20f51d 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -14,7 +14,8 @@ export type Locale = | "ga" | "pt" | "ru" - | "hu"; + | "hu" + | "ar"; export interface Translations { // ── Common ── diff --git a/web/src/index.css b/web/src/index.css index 30b601e6c57..e4b5e2777f9 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -245,3 +245,11 @@ code { font-size: 0.875rem; } 2px 2px; } +/* RTL support — Arabic and any future right-to-left locale. The i18n provider + sets `dir` on ; Tailwind v4's logical spacing utilities (ms-/me-, + ps-/pe-) and logical properties then flip automatically. Scoped so the + default LTR layout is untouched. */ +html[dir="rtl"] { + direction: rtl; +} +