Merge pull request #70870 from NousResearch/bb/ar-rtl-locale

feat(i18n): Arabic (ar) locale with RTL support — desktop, dashboard, agent
This commit is contained in:
brooklyn! 2026-07-24 12:20:10 -05:00 committed by GitHub
commit d51bd5fdc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 3811 additions and 7 deletions

View file

@ -25,7 +25,8 @@ Language resolution order:
3. ``display.language`` from config.yaml
4. ``"en"`` (baseline)
Supported languages: en, zh, ja, de, es, fr, tr, uk. Unknown values fall back to en.
Supported languages: en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga,
pt, ru, hu, ar. Unknown values fall back to en.
"""
from __future__ import annotations
@ -41,7 +42,7 @@ logger = logging.getLogger(__name__)
SUPPORTED_LANGUAGES: tuple[str, ...] = (
"en", "zh", "zh-hant", "ja", "de", "es", "fr", "tr", "uk",
"af", "ko", "it", "ga", "pt", "ru", "hu",
"af", "ko", "it", "ga", "pt", "ru", "hu", "ar",
)
DEFAULT_LANGUAGE = "en"
@ -78,6 +79,9 @@ _LANGUAGE_ALIASES: dict[str, str] = {
"russian": "ru", "русский": "ru", "ru-ru": "ru",
# Hungarian
"hungarian": "hu", "magyar": "hu", "hu-hu": "hu",
# Arabic — bare "arabic"/endonym plus the common regional BCP-47 tags.
"arabic": "ar", "العربية": "ar",
"ar-sa": "ar", "ar-eg": "ar", "ar-ae": "ar", "ar-ma": "ar", "ar-dz": "ar",
}
_catalog_cache: dict[str, dict[str, str]] = {}

2561
apps/desktop/src/i18n/ar.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
import { ar } from './ar'
import { en } from './en'
import { ja } from './ja'
import type { Locale, Translations } from './types'
@ -8,5 +9,6 @@ export const TRANSLATIONS: Record<Locale, Translations> = {
en,
zh,
'zh-hant': zhHant,
ja
ja,
ar
}

View file

@ -209,6 +209,24 @@ describe('I18nProvider', () => {
expect(screen.getByTestId('locale').textContent).toBe('ja')
})
it('applies RTL direction for Arabic and restores LTR on switch back', async () => {
render(
<I18nProvider configClient={null} initialLocale="ar">
<LanguageProbe target="en" />
</I18nProvider>
)
expect(screen.getByTestId('locale').textContent).toBe('ar')
expect(document.documentElement.dir).toBe('rtl')
expect(document.documentElement.lang).toBe('ar')
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
await waitFor(() => expect(screen.getByTestId('locale').textContent).toBe('en'))
expect(document.documentElement.dir).toBe('ltr')
expect(document.documentElement.lang).toBe('en')
})
it('rolls back the visible locale when saving fails', async () => {
const configClient: I18nConfigClient = {
getConfig: vi.fn().mockResolvedValue({ display: { language: 'en' } }),

View file

@ -55,6 +55,17 @@ function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
}
const RTL_LOCALES = new Set<Locale>(['ar'])
function applyDocumentLocale(locale: Locale) {
if (typeof document === 'undefined') {
return
}
document.documentElement.lang = locale
document.documentElement.dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'
}
export interface I18nContextValue {
configLoadError: Error | null
isLoadingConfig: boolean
@ -92,6 +103,7 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
useEffect(() => {
localeRef.current = locale
setRuntimeI18nLocale(locale)
applyDocumentLocale(locale)
}, [locale])
useEffect(() => {

View file

@ -15,6 +15,9 @@ describe('desktop i18n languages', () => {
expect(normalizeLocale('zh_HK')).toBe('zh-hant')
expect(normalizeLocale('ja')).toBe('ja')
expect(normalizeLocale('ja-JP')).toBe('ja')
expect(normalizeLocale('ar')).toBe('ar')
expect(normalizeLocale('AR-SA')).toBe('ar')
expect(normalizeLocale(' ar_eg ')).toBe('ar')
})
it('falls back to English for empty or unsupported values', () => {
@ -32,6 +35,7 @@ describe('desktop i18n languages', () => {
expect(isLocale('zh')).toBe(true)
expect(isLocale('zh-hant')).toBe(true)
expect(isLocale('ja')).toBe(true)
expect(isLocale('ar')).toBe(true)
})
it('returns the persisted config value for supported locales', () => {
@ -39,5 +43,6 @@ describe('desktop i18n languages', () => {
expect(localeConfigValue('zh')).toBe('zh')
expect(localeConfigValue('zh-hant')).toBe('zh-hant')
expect(localeConfigValue('ja')).toBe('ja')
expect(localeConfigValue('ar')).toBe('ar')
})
})

View file

@ -28,6 +28,12 @@ export const LOCALE_OPTIONS = [
name: '日本語',
englishName: 'Japanese',
configValue: 'ja'
},
{
id: 'ar',
name: 'العربية',
englishName: 'Arabic',
configValue: 'ar'
}
] as const satisfies readonly { configValue: string; englishName: string; id: Locale; name: string }[]
@ -64,7 +70,16 @@ const LOCALE_ALIASES: Record<string, Locale> = {
zh_hant_hk: 'zh-hant',
ja: 'ja',
'ja-jp': 'ja',
ja_jp: 'ja'
ja_jp: 'ja',
ar: 'ar',
'ar-sa': 'ar',
ar_sa: 'ar',
'ar-ae': 'ar',
ar_ae: 'ar',
'ar-eg': 'ar',
ar_eg: 'ar',
arabic: 'ar',
العربية: 'ar'
}
export function isLocale(value: unknown): value is Locale {

View file

@ -5,7 +5,7 @@
// partial locales should use `defineLocale()` so missing desktop-only strings
// fall back to English while new keys remain type-checked.
export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja'
export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja' | 'ar'
export type ToolTitleKey =
| 'browser_click'

418
locales/ar.yaml Normal file
View file

@ -0,0 +1,418 @@
# Hermes static-message catalog -- Arabic (العربية)
#
# Mirrors locales/en.yaml key-for-key. Only user-facing static prose is
# translated: command names, /slash commands, config keys, paths, URLs, model
# IDs, provider names, emojis and {placeholder} tokens are kept verbatim so
# interpolation and identifiers keep working. tests/agent/test_i18n.py asserts
# key parity and placeholder parity against en.yaml.
approval:
dangerous_header: "⚠️ أمر خطير: {description}"
choose_long: " [o] مرة | [s] الجلسة | [a] دائمًا | [d] رفض"
choose_short: " [o] مرة | [s] الجلسة | [d] رفض"
prompt_long: " الاختيار [o/s/a/D]: "
prompt_short: " الاختيار [o/s/D]: "
choose_smart_deny: " [o] مرة | [d] رفض"
prompt_smart_deny: " الاختيار [o/D]: "
smart_deny_once_inputs: "o,once"
smart_deny_deny_inputs: "d,deny"
timeout: " ⏱ انتهت المهلة - يتم رفض الأمر"
allowed_once: " ✓ مسموح مرة واحدة"
allowed_session: " ✓ مسموح لهذه الجلسة"
allowed_always: " ✓ أُضيف إلى قائمة السماح الدائمة"
denied: " ✗ مرفوض"
cancelled: " ✗ أُلغي"
blocklist_message: "هذا الأمر مدرج في قائمة الحظر غير المشروطة ولا يمكن الموافقة عليه."
gateway:
approval_expired: "⚠️ انتهت صلاحية الموافقة (لم يعد الوكيل ينتظر). اطلب من الوكيل المحاولة مرة أخرى."
draining: "⏳ جارٍ إنهاء {count} وكيل نشط قبل إعادة التشغيل..."
goal_cleared: "✓ تم مسح الهدف."
no_active_goal: "لا يوجد هدف نشط."
config_read_failed: "⚠️ تعذّر قراءة config.yaml: {error}"
config_save_failed: "⚠️ تعذّر حفظ الإعدادات: {error}"
model:
error_prefix: "خطأ: {error}"
switched: "تم التبديل إلى النموذج `{model}`"
provider_label: "المزوّد: {provider}"
context_label: "السياق: {tokens} رمزًا"
max_output_label: "أقصى خرج: {tokens} رمزًا"
cost_label: "التكلفة: {cost}"
capabilities_label: "القدرات: {capabilities}"
prompt_caching_enabled: "التخزين المؤقت للموجِّه: مُفعّل"
warning_prefix: "تحذير: {warning}"
saved_global: "حُفظ في config.yaml (`--global`)"
session_only_hint: "_(هذه الجلسة فقط — أضف `--global` للحفظ الدائم)_"
current_label: "الحالي: `{model}` على {provider}"
current_tag: " (الحالي)"
more_models_suffix: " (+{count} آخر)"
usage_switch_model: "`/model <name>` — تبديل النموذج"
usage_switch_provider: "`/model <name> --provider <slug>` — تبديل المزوّد"
usage_persist: "`/model <name> --global` — حفظ دائم"
agents:
header: "🤖 **الوكلاء والمهام النشطة**"
active_agents: "**الوكلاء النشطون:** {count}"
this_chat: " · هذه المحادثة"
more: "... و{count} آخر"
running_processes: "**العمليات الخلفية الجارية:** {count}"
async_jobs: "**مهام البوابة غير المتزامنة:** {count}"
none: "لا يوجد وكلاء نشطون أو مهام جارية."
state_starting: "قيد البدء"
state_running: "قيد التشغيل"
approve:
no_pending: "لا يوجد أمر معلّق للموافقة عليه."
once_singular: "✅ تمت الموافقة على الأمر. الوكيل يستأنف..."
once_plural: "✅ تمت الموافقة على الأوامر ({count} أوامر). الوكيل يستأنف..."
session_singular: "✅ تمت الموافقة على الأمر (النمط مُوافق عليه لهذه الجلسة). الوكيل يستأنف..."
session_plural: "✅ تمت الموافقة على الأوامر (النمط مُوافق عليه لهذه الجلسة) ({count} أوامر). الوكيل يستأنف..."
always_singular: "✅ تمت الموافقة على الأمر (النمط مُوافق عليه دائمًا). الوكيل يستأنف..."
always_plural: "✅ تمت الموافقة على الأوامر (النمط مُوافق عليه دائمًا) ({count} أوامر). الوكيل يستأنف..."
background:
usage: "الاستخدام: /background <prompt>\nمثال: /background لخّص أهم قصص HN اليوم\n\nيشغّل الموجِّه في جلسة منفصلة. يمكنك متابعة المحادثة — ستظهر النتيجة هنا عند الانتهاء."
started: "🔄 بدأت مهمة خلفية: \"{preview}\"\nمعرّف المهمة: {task_id}\nيمكنك متابعة المحادثة — ستظهر النتائج عند الانتهاء."
branch:
db_unavailable: "قاعدة بيانات الجلسات غير متاحة."
no_conversation: "لا توجد محادثة للتفريع — أرسل رسالة أولًا."
create_failed: "فشل إنشاء الفرع: {error}"
switch_failed: "أُنشئ الفرع لكن تعذّر التبديل إليه."
branched_one: "⑂ تم التفريع إلى **{title}** (نُسخت {count} رسالة)\nالأصل: `{parent}`\nالفرع: `{new}`\nاستخدم `/resume` للعودة إلى الأصل."
branched_many: "⑂ تم التفريع إلى **{title}** (نُسخت {count} رسالة)\nالأصل: `{parent}`\nالفرع: `{new}`\nاستخدم `/resume` للعودة إلى الأصل."
commands:
usage: "الاستخدام: `/commands [page]`"
skill_header: "⚡ **أوامر المهارات**:"
default_desc: "أمر مهارة"
none: "لا توجد أوامر متاحة."
header: "📚 **الأوامر** ({total} إجمالًا، صفحة {page}/{total_pages})"
nav_prev: "`/commands {page}` ← السابق"
nav_next: "التالي → `/commands {page}`"
out_of_range: "_(الصفحة المطلوبة {requested} خارج النطاق، يتم عرض الصفحة {page}.)_"
compress:
not_enough: "لا توجد محادثة كافية للضغط (يلزم 4 رسائل على الأقل)."
no_provider: "لا يوجد مزوّد مُهيّأ -- لا يمكن الضغط."
nothing_to_do: "لا شيء للضغط بعد (النص كله لا يزال سياقًا محميًا)."
aggressive_unsupported: "الخيار --aggressive غير مدعوم؛ استخدم '/compress here [N]' للإبقاء على أحدث التبادلات فقط، أو /undo لإسقاط الأدوار."
focus_line: "التركيز: \"{topic}\""
summary_failed: "⚠️ فشل توليد الملخّص ({error}). أُزيلت {count} رسالة تاريخية واستُبدلت بعنصر نائب؛ لم يعد السياق الأقدم قابلًا للاسترجاع. فكّر في مراجعة إعداد نموذج auxiliary.compression."
aborted: "⚠️ أُلغي الضغط ({error}). لم تُسقط أي رسائل — المحادثة كما هي. شغّل /compress للمحاولة مجددًا، أو /reset لجلسة نظيفة، أو راجع إعداد نموذج auxiliary.compression."
aux_failed: " فشل نموذج الضغط المُهيّأ `{model}` ({error}). تم التعافي باستخدام نموذجك الرئيسي — السياق سليم — لكن قد ترغب في مراجعة `auxiliary.compression.model` في config.yaml."
failed: "فشل الضغط: {error}"
debug:
upload_failed: "✗ فشل رفع تقرير التصحيح: {error}"
header: "**تم رفع تقرير التصحيح:**"
auto_delete: "⏱ ستُحذف اللصقات تلقائيًا خلال 6 ساعات."
full_logs_hint: "لرفع السجلات الكاملة، استخدم `hermes debug share` من واجهة الأوامر."
share_hint: "شارك هذه الروابط مع فريق Hermes للحصول على الدعم."
deny:
stale: "❌ رُفض الأمر (الموافقة كانت قديمة)."
no_pending: "لا يوجد أمر معلّق لرفضه."
denied_singular: "❌ رُفض الأمر."
denied_plural: "❌ رُفضت الأوامر ({count} أوامر)."
denied_reason_singular: "❌ رُفض الأمر. السبب المُبلّغ للوكيل: \"{reason}\""
denied_reason_plural: "❌ رُفضت الأوامر ({count} أوامر). السبب المُبلّغ للوكيل: \"{reason}\""
fast:
not_supported: "⚡ /fast متاح فقط لنماذج OpenAI التي تدعم المعالجة ذات الأولوية."
status: "⚡ المعالجة ذات الأولوية\n\nالوضع الحالي: `{mode}`\n\n_الاستخدام:_ `/fast <normal|fast|status>`"
unknown_arg: "⚠️ وسيط غير معروف: `{arg}`\n\n**الخيارات الصالحة:** normal, fast, status"
saved: "⚡ ✓ المعالجة ذات الأولوية: **{label}** (حُفظت في الإعدادات)\n_(تسري على الرسالة التالية)_"
session_only: "⚡ ✓ المعالجة ذات الأولوية: **{label}** (هذه الجلسة فقط)"
label_fast: "سريع"
label_normal: "عادي"
status_fast: "fast"
status_normal: "normal"
picker_title: "⚡ **المعالجة ذات الأولوية**\n\nالوضع الحالي: `{mode}`\n\nاختر خيارًا:"
choice_fast: "fast — المعالجة ذات الأولوية مُفعّلة"
choice_normal: "normal — المعالجة القياسية"
footer:
status: "📎 تذييل التشغيل: **{state}**\nالحقول: `{fields}`\nالمنصّة: `{platform}`"
usage: "الاستخدام: `/footer [on|off|status]`"
saved: "📎 تذييل التشغيل: **{state}**{example}\n_(حُفظ عالميًا — يسري على الرسالة التالية)_"
example_line: "\nمثال: `{preview}`"
state_on: "مُفعّل"
state_off: "مُعطّل"
goal:
unavailable: "الأهداف غير متاحة في هذه الجلسة."
no_goal_set: "لم يُحدَّد هدف."
paused: "⏸ أُوقف الهدف مؤقتًا: {goal}"
no_resume: "لا يوجد هدف لاستئنافه."
resumed: "▶ استُؤنف الهدف: {goal}\nأرسل أي رسالة للمتابعة، أو انتظر — سأتخذ الخطوة التالية في الدور القادم."
invalid: "هدف غير صالح: {error}"
set: "⊙ حُدِّد الهدف (ميزانية {budget} دور): {goal}\nسأواصل العمل حتى يكتمل الهدف، أو توقفه/تمسحه، أو تنفد الميزانية.\nالتحكم: /goal status · /goal pause · /goal resume · /goal clear"
help:
header: "📖 **أوامر Hermes**\n"
skill_header: "\n⚡ **أوامر المهارات** ({count} نشطة):"
more_use_commands: "\n... و{count} آخر. استخدم `/commands` للقائمة الكاملة المُقسّمة إلى صفحات."
insights:
invalid_days: "قيمة --days غير صالحة: {value}"
error: "خطأ في توليد الرؤى: {error}"
kanban:
error_prefix: "⚠ خطأ kanban: {error}"
subscribed_suffix: "(مُشترك — ستُخطَر عند اكتمال {task_id} أو حجبها)"
truncated_suffix: "… (مقتطع؛ استخدم `hermes kanban …` في الطرفية للخرج الكامل)"
no_output: "(لا يوجد خرج)"
wake:
completed: "اكتملت"
gave_up: "استسلم (نفدت المحاولات)"
crashed: "تعطّل (خرج العامل)؛ سيعيد المُوزّع المحاولة"
timed_out: "انتهت المهلة؛ سيعيد المُوزّع المحاولة"
blocked: "محجوبة؛ تحتاج إلى انتباه"
status_default: "تغيّرت الحالة"
status_joiner: "، "
message: "[kanban] المهمة {task_id} {status}.\nالعنوان: {title}\nالمُكلَّف: @{assignee}\nاللوحة: {board}\n\nراجع النتيجة أو قرّر الخطوة التالية."
personality:
none_configured: "لا توجد شخصيات مُهيّأة في `{path}/config.yaml`"
header: "🎭 **الشخصيات المتاحة**\n"
none_option: "• `none` — (بدون طبقة شخصية)"
item: "• `{name}` — {preview}"
usage: "\nالاستخدام: `/personality <name>`"
save_failed: "⚠️ فشل حفظ تغيير الشخصية: {error}"
cleared: "🎭 مُسحت الشخصية — يتم استخدام سلوك الوكيل الأساسي.\n_(يسري على الرسالة التالية)_"
set_to: "🎭 ضُبطت الشخصية إلى **{name}**\n_(يسري على الرسالة التالية)_"
unknown: "شخصية غير معروفة: `{name}`\n\nالمتاح: {available}"
profile:
header: "👤 **الملف الشخصي:** `{profile}`"
home: "📂 **المجلد الرئيسي:** `{home}`"
reasoning:
level_default: "medium (افتراضي)"
level_disabled: "none (مُعطّل)"
scope_session: "تجاوز الجلسة"
scope_global: "الإعدادات العامة"
status: "🧠 **إعدادات التفكير**\n\n**الجهد:** `{level}`\n**النطاق:** {scope}\n**العرض:** {display}\n\n_الاستخدام:_ `/reasoning <none|minimal|low|medium|high|xhigh|max|ultra|reset|show|hide> [--global]`"
display_on: "مُفعّل ✓"
display_off: "مُعطّل"
display_set_on: "🧠 ✓ عرض التفكير: **مُفعّل**\nسيُعرض تفكير النموذج قبل كل رد على **{platform}**."
display_set_off: "🧠 ✓ عرض التفكير: **مُعطّل** لـ **{platform}**"
reset_global_unsupported: "⚠️ `/reasoning reset --global` غير مدعوم. استخدم `/reasoning <level> --global` لتغيير الافتراضي العام."
reset_done: "🧠 ✓ مُسح تجاوز التفكير للجلسة؛ العودة إلى الإعدادات العامة."
unknown_arg: "⚠️ وسيط غير معروف: `{arg}`\n\n**المستويات الصالحة:** none, minimal, low, medium, high, xhigh, max, ultra\n**العرض:** show, hide\n**الحفظ:** أضف `--global` للحفظ بعد هذه الجلسة"
picker_title: "🧠 **إعدادات التفكير**\n\n**الجهد:** `{level}`\n**النطاق:** {scope}\n**العرض:** {display}\n\nاختر خيارًا:"
choice_none: "none — تعطيل التفكير"
choice_reset: "reset — مسح تجاوز الجلسة"
choice_show: "عرض التفكير في الردود"
choice_hide: "إخفاء التفكير من الردود"
set_global: "🧠 ✓ ضُبط جهد التفكير إلى `{effort}` (حُفظ في الإعدادات)\n_(يسري على الرسالة التالية)_"
set_global_save_failed: "🧠 ✓ ضُبط جهد التفكير إلى `{effort}` (الجلسة فقط — فشل حفظ الإعدادات)\n_(يسري على الرسالة التالية)_"
set_session: "🧠 ✓ ضُبط جهد التفكير إلى `{effort}` (الجلسة فقط — أضف `--global` للحفظ الدائم)\n_(يسري على الرسالة التالية)_"
reload_mcp:
cancelled: "🟡 أُلغي /reload-mcp. أدوات MCP دون تغيير."
always_followup: " ستُشغَّل استدعاءات `/reload-mcp` المستقبلية دون تأكيد. أعِد التفعيل عبر `approvals.mcp_reload_confirm: true` في config.yaml."
confirm_prompt: "⚠️ **تأكيد /reload-mcp**\n\nإعادة تحميل خوادم MCP تعيد بناء مجموعة الأدوات لهذه الجلسة و**تُبطل التخزين المؤقت لموجِّه المزوّد** — ستُعاد الرسالة التالية بكامل رموز الإدخال. على النماذج ذات السياق الطويل أو التفكير العالي قد يكون ذلك مكلفًا.\n\nاختر:\n• **الموافقة مرة** — أعِد التحميل الآن\n• **الموافقة دائمًا** — أعِد التحميل الآن واكتم هذا التنبيه نهائيًا\n• **إلغاء** — اترك أدوات MCP دون تغيير\n\n_بديل نصي: رُدّ بـ `/approve` أو `/always` أو `/cancel`._"
header: "🔄 **أُعيد تحميل خوادم MCP**\n"
reconnected: "♻️ أُعيد الاتصال: {names}"
added: " أُضيف: {names}"
removed: " أُزيل: {names}"
none_connected: "لا توجد خوادم MCP متصلة."
tools_available: "\n🔧 {tools} أداة متاحة من {servers} خادم"
failed: "❌ فشلت إعادة تحميل MCP: {error}"
reload_skills:
header: "🔄 **أُعيد تحميل المهارات**\n"
no_new: "لم تُكتشف مهارات جديدة."
total: "\n📚 {count} مهارة متاحة"
added_header: " **مهارات مُضافة:**"
removed_header: " **مهارات مُزالة:**"
item_with_desc: " - {name}: {desc}"
item_no_desc: " - {name}"
failed: "❌ فشلت إعادة تحميل المهارات: {error}"
reset:
header_default: "✨ أُعيد ضبط الجلسة! بداية جديدة."
header_new: "✨ بدأت جلسة جديدة!"
header_titled: "✨ بدأت جلسة جديدة: {title}"
title_rejected: "\n⚠ رُفض العنوان: {error}"
title_error_untitled: "\n⚠ {error} — بدأت الجلسة دون عنوان."
title_empty_untitled: "\n⚠ العنوان فارغ بعد التنظيف — بدأت الجلسة دون عنوان."
tip: "\n✦ نصيحة: {tip}"
restart:
in_progress: "⏳ إعادة تشغيل البوابة قيد التنفيذ بالفعل..."
restarting: "♻ جارٍ إعادة تشغيل البوابة. إن لم تُخطَر خلال 60 ثانية، أعِد التشغيل من وحدة التحكّم باستخدام `hermes gateway restart`."
resume:
db_unavailable: "قاعدة بيانات الجلسات غير متاحة."
parse_error: "⚠️ تعذّر تحليل وسائط `/resume`: {error}.\nاستخدم علامات اقتباس حول العناوين التي تحتوي مسافات، مثال: `/resume \"Project A Plan\"`."
matrix_no_named_sessions: "لم يُعثر على جلسات مُسمّاة لغرفة Matrix هذه.\nاستخدم `/title My Session` لتسمية جلسة الغرفة الحالية، أو `/resume --all` لسرد كل جلسات Matrix، أو `/resume --cross-room <session name>` لتجاوز حدود الغرفة صراحةً."
matrix_blocked_no_origin: "⚠️ حُجب /resume في Matrix: هذه الجلسة المُسمّاة لا تملك أصل غرفة مُسجّلًا، لذا لن يستأنفها Hermes داخل الغرفة الحالية افتراضيًا. استخدم `/resume --cross-room {name}` إن كنت تريد عمدًا تجاوز حدود الغرفة."
matrix_blocked_other_room: "⚠️ حُجب /resume في Matrix: تلك الجلسة تخصّ غرفة Matrix مختلفة ({room}). استخدم `/resume --cross-room {name}` إن كنت تريد عمدًا استئنافها هنا."
matrix_cross_room_success: "⚠️ استئناف عبر الغرف: استُؤنفت **{title}** داخل غرفة Matrix **{room}**.\nستستخدم الرسائل المستقبلية في هذه الغرفة ذلك النص حتى `/reset` أو `/resume` آخر.{msg_part}"
blocked_not_owner: "⚠️ حُجب /resume: '**{name}**' تخصّ مستخدمًا أو محادثة مختلفة. يمكنك فقط استئناف جلسات هذه المحادثة."
no_named_sessions: "لم يُعثر على جلسات مُسمّاة.\nاستخدم `/title My Session` لتسمية جلستك الحالية، ثم `/resume My Session` للعودة إليها لاحقًا."
list_header: "📋 **الجلسات المُسمّاة**\n"
list_item: "• **{title}**{preview_part}"
list_item_numbered: "{index}. **{title}**{preview_part}"
list_preview_suffix: " — _{preview}_"
list_footer: "\nالاستخدام: `/resume <session name>`"
list_footer_numbered: "\nالاستخدام: `/resume <session name>` أو `/resume <number>` (مثال: `/resume 1` للأحدث)"
list_failed: "تعذّر سرد الجلسات: {error}"
out_of_range: "مؤشر الاستئناف {index} خارج النطاق.\nاستخدم `/resume` دون وسائط لعرض الجلسات المتاحة."
not_found: "لم يُعثر على جلسة تطابق '**{name}**'.\nاستخدم `/resume` دون وسائط لعرض الجلسات المتاحة."
already_on: "📌 أنت بالفعل على الجلسة **{name}**."
switch_failed: "فشل تبديل الجلسة."
resumed_one: "↻ استُؤنفت الجلسة **{title}** ({count} رسالة). استُعيدت المحادثة."
resumed_many: "↻ استُؤنفت الجلسة **{title}** ({count} رسالة). استُعيدت المحادثة."
resumed_no_count: "↻ استُؤنفت الجلسة **{title}**. استُعيدت المحادثة."
retry:
no_previous: "لا توجد رسالة سابقة لإعادة المحاولة."
rollback:
not_enabled: "نقاط التحقّق غير مُفعّلة.\nفعّلها في config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
none_found: "لم يُعثر على نقاط تحقّق لـ {cwd}"
invalid_number: "رقم نقطة تحقّق غير صالح. استخدم 1-{max}."
restored: "✅ استُعيد إلى نقطة التحقّق {hash}: {reason}\nحُفظت لقطة ما قبل التراجع تلقائيًا."
restore_failed: "❌ {error}"
set_home:
save_failed: "فشل حفظ القناة الرئيسية: {error}"
success: "✅ ضُبطت القناة الرئيسية إلى **{name}** (المعرّف: {chat_id}).\nستُسلَّم مهام cron والرسائل عبر المنصّات هنا."
status:
header: "📊 **حالة بوابة Hermes**"
matrix_scope_header: "**نطاق Matrix:**"
matrix_scope_room: " الغرفة: {room}"
matrix_scope_room_id: " معرّف الغرفة: {room_id}"
matrix_scope_thread: " معرّف الخيط: {thread_id}"
matrix_scope_mode: " نطاق الجلسة: {scope}"
matrix_scope_key: " مفتاح الجلسة: {session_key}"
session_id: "**معرّف الجلسة:** `{session_id}`"
title: "**العنوان:** {title}"
created: "**أُنشئت:** {timestamp}"
last_activity: "**آخر نشاط:** {timestamp}"
model: "**النموذج:** `{model}`"
model_provider: "**النموذج:** `{model}` ({provider})"
context: "**السياق:** {used} / {total} ({pct}%)"
context_used: "**السياق:** ~{used} رمزًا"
tokens: "**رموز API التراكمية (تُعاد كل استدعاء):** {tokens}"
agent_running: "**الوكيل قيد التشغيل:** {state}"
state_yes: "نعم ⚡"
state_no: "لا"
queued: "**المتابعات في الطابور:** {count}"
platforms: "**المنصّات المتصلة:** {platforms}"
stop:
stopped_pending: "⚡ تم الإيقاف. لم يكن الوكيل قد بدأ بعد — يمكنك متابعة هذه الجلسة."
stopped: "⚡ تم الإيقاف. يمكنك متابعة هذه الجلسة."
no_active: "لا توجد مهمة نشطة لإيقافها."
title:
db_unavailable: "قاعدة بيانات الجلسات غير متاحة."
warn_prefix: "⚠️ {error}"
empty_after_clean: "⚠️ العنوان فارغ بعد التنظيف. من فضلك استخدم أحرفًا قابلة للطباعة."
set_to: "✏️ ضُبط عنوان الجلسة: **{title}**"
not_found: "لم يُعثر على الجلسة في قاعدة البيانات."
current_with_title: "📌 الجلسة: `{session_id}`\nالعنوان: **{title}**"
current_no_title: "📌 الجلسة: `{session_id}`\nلم يُضبط عنوان. الاستخدام: `/title My Session Name`"
topic:
not_telegram_dm: "الأمر /topic متاح فقط في محادثات Telegram الخاصة."
no_session_db: "قاعدة بيانات الجلسات غير متاحة."
unauthorized: "غير مصرّح لك باستخدام /topic على هذا البوت."
restore_needs_topic: "لاستعادة جلسة، أنشئ أو افتح موضوع Telegram أولًا، ثم أرسل /topic <session-id> داخل ذلك الموضوع. لإنشاء موضوع جديد، افتح All Messages وأرسل أي رسالة هناك."
topics_disabled: "مواضيع Telegram غير مُفعّلة لهذا البوت بعد.\n\nكيفية تفعيلها:\n1. افتح @BotFather.\n2. اختر البوت الخاص بك.\n3. افتح Bot Settings → Threads Settings.\n4. فعّل Threaded Mode وتأكّد من السماح للمستخدمين بإنشاء خيوط جديدة.\n\nثم أرسل /topic مرة أخرى."
topics_user_disallowed: "مواضيع Telegram مُفعّلة، لكن غير مسموح للمستخدمين بإنشاء مواضيع.\n\nافتح @BotFather → اختر البوت → Bot Settings → Threads Settings، ثم أوقف 'Disallow users to create new threads'.\n\nثم أرسل /topic مرة أخرى."
enable_failed: "فشل تفعيل وضع مواضيع Telegram: {error}"
bound_status: "هذا الموضوع مرتبط بـ:\nالجلسة: {label}\nالمعرّف: {session_id}\n\nاستخدم /new لاستبدال هذا الموضوع بجلسة جديدة.\nللعمل المتوازي، افتح All Messages وأرسل رسالة هناك لإنشاء موضوع آخر."
thread_ready: "مواضيع Telegram متعددة الجلسات مُفعّلة.\n\nسيُستخدم هذا الموضوع كجلسة Hermes مستقلة. استخدم /new لاستبدال جلسة هذا الموضوع الحالية. للعمل المتوازي، افتح All Messages وأرسل رسالة هناك لإنشاء موضوع آخر."
untitled_session: "جلسة دون عنوان"
undo:
nothing: "لا شيء للتراجع عنه."
removed: "↩️ تم التراجع عن {turns} دور ({count} رسالة).\nنُسخ احتياطيًا إلى: \"{preview}\"\nانسخ/عدّل النص أعلاه وأرسله لإعادة التوجيه من هنا."
invalid_count: "عدد غير صالح \"{arg}\" — استخدم /undo أو /undo N."
update:
platform_not_messaging: "✗ /update متاح فقط من منصّات المراسلة. شغّل `hermes update` من الطرفية."
not_git_repo: "✗ ليس مستودع git — لا يمكن التحديث."
hermes_cmd_not_found: "✗ تعذّر تحديد موقع أمر `hermes`. Hermes يعمل، لكن أمر التحديث لم يجد الملف التنفيذي على PATH أو عبر مُفسّر Python الحالي. جرّب تشغيل `hermes update` يدويًا في طرفيتك."
start_failed: "✗ فشل بدء التحديث: {error}"
starting: "⚕ جارٍ بدء تحديث Hermes… سأبثّ التقدّم هنا."
usage:
rate_limits: "⏱️ **حدود المعدّل:** {state}"
header_session: "📊 **استخدام رموز الجلسة**"
label_model: "النموذج: `{model}`"
label_input_tokens: "رموز الإدخال: {count}"
label_cache_read: "رموز قراءة التخزين المؤقت: {count}"
label_cache_write: "رموز كتابة التخزين المؤقت: {count}"
label_output_tokens: "رموز الإخراج: {count}"
label_total: "الإجمالي: {count}"
label_api_calls: "استدعاءات API: {count}"
label_cost: "التكلفة: {prefix}${amount}"
label_cost_included: "التكلفة: مشمولة"
label_context: "السياق: {used} / {total} ({pct}%)"
label_compressions: "عمليات الضغط: {count}"
breakdown_header: "🧩 **تفصيل السياق** _(تقديري)_"
breakdown_line: "• {label}: ~{count} ({pct}%)"
breakdown_cat_system_prompt: "موجّه النظام"
breakdown_cat_tool_definitions: "تعريفات الأدوات"
breakdown_cat_rules: "القواعد"
breakdown_cat_skills: "المهارات"
breakdown_cat_mcp: "MCP"
breakdown_cat_subagent_definitions: "تعريفات الوكلاء الفرعيين"
breakdown_cat_memory: "الذاكرة"
breakdown_cat_conversation: "المحادثة"
header_session_info: "📊 **معلومات الجلسة**"
label_messages: "الرسائل: {count}"
label_estimated_context: "السياق المُقدّر: ~{count} رمزًا"
detailed_after_first: "_(الاستخدام المُفصّل متاح بعد أول رد من الوكيل)_"
no_data: "لا تتوفّر بيانات استخدام لهذه الجلسة."
unknown_subcommand: "أمر فرعي غير معروف لـ /usage: `{args}`. جرّب `/usage` أو `/usage reset [--force]`."
reset_wrong_provider: "إعادة تعيين الاستخدام المُدّخر متاحة فقط على مزوّد openai-codex. بدّل عبر `/model` أولًا."
credits:
not_logged_in: "لم تُسجّل الدخول إلى Nous Portal. سجّل الدخول لرؤية رصيد أرصدتك وإعادة الشحن."
verbose:
not_enabled: "الأمر `/verbose` غير مُفعّل لمنصّات المراسلة.\n\nفعّله في `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
mode_off: "⚙️ تقدّم الأدوات: **مُعطّل** — لا يُعرض نشاط الأدوات."
mode_new: "⚙️ تقدّم الأدوات: **NEW** — يُعرض عند تغيّر الأداة (طول المعاينة: `display.tool_preview_length`، افتراضي 40)."
mode_all: "⚙️ تقدّم الأدوات: **ALL** — يُعرض كل استدعاء أداة (طول المعاينة: `display.tool_preview_length`، افتراضي 40)."
mode_verbose: "⚙️ تقدّم الأدوات: **VERBOSE** — كل استدعاء أداة مع وسائطه الكاملة."
mode_log: "⚙️ تقدّم الأدوات: **LOG** — صامت في المحادثة؛ تُكتب استدعاءات الأدوات إلى ~/.hermes/logs/tool_calls.log."
saved_suffix: "_(حُفظ لـ **{platform}** — يسري على الرسالة التالية)_"
save_failed: "_(تعذّر الحفظ في الإعدادات: {error})_"
voice:
enabled_voice_only: "وضع الصوت مُفعّل.\nسأردّ بالصوت عندما ترسل رسائل صوتية.\nاستخدم /voice tts للحصول على ردود صوتية لكل الرسائل."
disabled_text: "وضع الصوت مُعطّل. ردود نصّية فقط."
tts_enabled: "TTS التلقائي مُفعّل.\nستتضمّن كل الردود رسالة صوتية."
status_mode: "وضع الصوت: {label}"
status_channel: "قناة الصوت: #{channel}"
status_participants: "المشاركون: {count}"
status_member: " - {name}{status}"
speaking: " (يتحدّث)"
enabled_short: "وضع الصوت مُفعّل."
disabled_short: "وضع الصوت مُعطّل."
label_off: "مُعطّل (نص فقط)"
label_voice_only: "مُفعّل (رد صوتي على الرسائل الصوتية)"
label_all: "TTS (رد صوتي على كل الرسائل)"
help: "{toggle}\n\n**كيف يعمل /voice**\n• `/voice on` — رد صوتي عند إرسال رسالة صوتية\n• `/voice tts` — رد صوتي على *كل* رسالة\n• `/voice off` — العودة إلى ردود نصّية فقط\n• `/voice status` — عرض الوضع الحالي\n• `/voice` (بدون وسيط) — تبديل سريع بين التفعيل والتعطيل{channels}"
help_channels: "\n\n**قنوات الصوت الحيّة (Discord)**\n• انضمّ إلى قناة صوتية أولًا، ثم `/voice channel` — سأنضمّ وأستمع وأنطق ردودي\n• `/voice leave` — قطع الاتصال بالقناة الصوتية"
yolo:
disabled: "⚠️ وضع YOLO **مُعطّل** لهذه الجلسة — ستتطلّب الأوامر الخطيرة موافقة."
enabled: "⚡ وضع YOLO **مُفعّل** لهذه الجلسة — كل الأوامر مُوافق عليها تلقائيًا. استخدمه بحذر."
shared:
session_db_unavailable: "قاعدة بيانات الجلسات غير متاحة."
session_db_unavailable_prefix: "قاعدة بيانات الجلسات غير متاحة"
session_not_found: "لم يُعثر على الجلسة في قاعدة البيانات."
warn_passthrough: "⚠️ {error}"

701
web/src/i18n/ar.ts Normal file
View file

@ -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. يمكن أن يستغرق الفحص الأول 510 ثوانٍ على السجلات الكبيرة.",
},
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: ")",
},
});

View file

@ -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<Locale, Translations> = {
en,
@ -34,8 +35,13 @@ const TRANSLATIONS: Record<Locale, Translations> = {
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<Locale>(["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<Locale, { name: string }> = {
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,

View file

@ -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> = T extends (...args: never[]) => string
? T
: T extends readonly unknown[]
? T
: T extends string
? string
: T extends object
? { [K in keyof T]?: TranslationOverride<T[K]> }
: T;
export type TranslationOverrides = TranslationOverride<Translations>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function mergeTranslations<T>(base: T, overrides: TranslationOverride<T> | undefined): T {
if (!isRecord(base) || !isRecord(overrides)) {
return (overrides ?? base) as T;
}
const result: Record<string, unknown> = { ...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<Translations>(en, overrides);
}

View file

@ -14,7 +14,8 @@ export type Locale =
| "ga"
| "pt"
| "ru"
| "hu";
| "hu"
| "ar";
export interface Translations {
// ── Common ──

View file

@ -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 <html>; 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;
}