mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-25 17:18:11 +00:00
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:
commit
d51bd5fdc6
14 changed files with 3811 additions and 7 deletions
|
|
@ -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
2561
apps/desktop/src/i18n/ar.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' } }),
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
418
locales/ar.yaml
Normal 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
701
web/src/i18n/ar.ts
Normal 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. يمكن أن يستغرق الفحص الأول 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: ")",
|
||||
},
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
46
web/src/i18n/define-locale.ts
Normal file
46
web/src/i18n/define-locale.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -14,7 +14,8 @@ export type Locale =
|
|||
| "ga"
|
||||
| "pt"
|
||||
| "ru"
|
||||
| "hu";
|
||||
| "hu"
|
||||
| "ar";
|
||||
|
||||
export interface Translations {
|
||||
// ── Common ──
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue