From d1da12f639dbdcdb7b5d04408527ec2b317b1091 Mon Sep 17 00:00:00 2001 From: Julius Jurgelenas Date: Mon, 28 Sep 2026 08:52:20 +0300 Subject: [PATCH] Add Arabic language + rtl support --- .../workflows/translations-pull-request.yml | 2 +- package.json | 16 +- src/i18n/index.ts | 6 + src/i18n/locales.json | 4 + src/i18n/locales/ar/messages.json | 365 ++++++++++++++++++ src/ui/App.tsx | 33 +- src/ui/theme/index.ts | 25 +- src/ui/views/SettingsView/index.tsx | 6 +- yarn.lock | 22 ++ 9 files changed, 460 insertions(+), 19 deletions(-) create mode 100644 src/i18n/locales/ar/messages.json diff --git a/.github/workflows/translations-pull-request.yml b/.github/workflows/translations-pull-request.yml index bd7875b2..75d67c08 100644 --- a/.github/workflows/translations-pull-request.yml +++ b/.github/workflows/translations-pull-request.yml @@ -26,7 +26,7 @@ jobs: upload_translations: false download_translations: true - download_translations_args: '--language=it --language=es-ES --language=ko --language=zh-CN --language=ru --language=el --language=fr --language=uk --language=id --language=de --language=da --language=pt-PT --language=pl --language=ka' + download_translations_args: '--language=it --language=es-ES --language=ko --language=zh-CN --language=ru --language=el --language=fr --language=uk --language=id --language=de --language=da --language=pt-PT --language=pl --language=ka --language=ar' localization_branch_name: update-translations-crowdin push_translations: true commit_message: 'Update translations' diff --git a/package.json b/package.json index f449da11..16c9f0d4 100644 --- a/package.json +++ b/package.json @@ -192,12 +192,14 @@ "resolutions": {}, "devDependencies": { "@electron/rebuild": "^4.0.0", + "@eslint/js": "^9.0.0", "@graphql-codegen/cli": "^6.0.0", "@graphql-codegen/introspection": "^5.0.0", + "@graphql-codegen/typed-document-node": "^5.0.0", "@graphql-codegen/typescript": "^5.0.0", "@graphql-codegen/typescript-operations": "^5.0.0", - "@graphql-codegen/typed-document-node": "^5.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", + "@stylistic/eslint-plugin": "^4.0.0", "@teamsupercell/typings-for-css-modules-loader": "^2.5.2", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", @@ -213,13 +215,11 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/rimraf": "^4.0.5", + "@types/stylis": "^4.2.7", "@types/uuid": "^9.0.7", "@types/webpack-bundle-analyzer": "^4.6.3", "@types/webpack-env": "^1.18.4", "@types/webpack-node-externals": "^3.0.4", - "@eslint/js": "^9.0.0", - "@stylistic/eslint-plugin": "^4.0.0", - "typescript-eslint": "^8.0.0", "chalk": "^4.1.0", "concurrently": "^9.0.0", "core-js": "^3.35.0", @@ -233,8 +233,8 @@ "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0", - "globals": "^16.0.0", "file-loader": "^6.0.0", + "globals": "^16.0.0", "html-webpack-plugin": "^5.6.0", "husky": "^9.0.0", "identity-obj-proxy": "^3.0.0", @@ -252,6 +252,7 @@ "tsconfig-paths-webpack-plugin": "^4.1.0", "tsx": "^4.21.0", "typescript": "^5.6.2", + "typescript-eslint": "^8.0.0", "url-loader": "^4.1.0", "webpack": "^5.73.0", "webpack-bundle-analyzer": "^5.0.0", @@ -264,9 +265,11 @@ "@apollo/client": "^4.0.0", "@apollo/server": "^5.0.0", "@as-integrations/express5": "^1.0.0", + "@emotion/cache": "^11.14.0", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.8", + "@graphql-yoga/subscription": "^5.0.0", "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", "@octokit/rest": "^22.0.0", @@ -282,7 +285,6 @@ "get-port": "^5.1.1", "graphql": "^16.7.1", "graphql-scalars": "^1.23.0", - "@graphql-yoga/subscription": "^5.0.0", "graphql-tag": "^2.12.6", "graphql-ws": "^6.0.6", "i18next": "^26.0.0", @@ -304,6 +306,8 @@ "semver": "^7.3.8", "simple-git": "^3.28.0", "source-map-support": "^0.5.21", + "stylis": "^4.4.0", + "stylis-plugin-rtl": "^2.1.1", "ts-node": "^10.9.2", "type-graphql": "2.0.0-rc.3", "typedi": "^0.10.0", diff --git a/src/i18n/index.ts b/src/i18n/index.ts index ad67b86f..6bfb1172 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -8,6 +8,12 @@ const loadPath = `${ searchParams.get('base_url') ?? 'http://localhost:3500/' }/locales/{{lng}}/messages.json`; +// Keep the document direction and language in sync (e.g. RTL for Arabic) +i18n.on('languageChanged', (lng) => { + document.documentElement.dir = i18n.dir(lng); + document.documentElement.lang = lng; +}); + i18n .use(Backend) .use(LanguageDetector) diff --git a/src/i18n/locales.json b/src/i18n/locales.json index 513219b9..110b8ff5 100644 --- a/src/i18n/locales.json +++ b/src/i18n/locales.json @@ -62,5 +62,9 @@ { "value": "ka", "label": "ქართული (Georgian)" + }, + { + "value": "ar", + "label": "العربية (Arabic)" } ] diff --git a/src/i18n/locales/ar/messages.json b/src/i18n/locales/ar/messages.json new file mode 100644 index 00000000..5241eb26 --- /dev/null +++ b/src/i18n/locales/ar/messages.json @@ -0,0 +1,365 @@ +{ + "BuildNotificationsList": { + "VerifyingBuildSystem": "جارٍ التحقق من نظام البناء", + "DownloadingFirmware": "تنزيل البرنامَج الثابت", + "BuildingUserDefinesTXT": "جارٍ إنشاء مِلَفّ user_defines.txt", + "CompilingFirmware": "جارٍ تجميع البرنامَج الثابت\n", + "FlashingDevice": "جارٍ تحديث الجهاز" + }, + "FlashingStepper": { + "BuildOnlyDoneHint": "البرنامج الثابت جاهز. ارفعه عبر واجهة الويب الخاصة بالجهاز عبر Wi-Fi.", + "StockBlDoneHint": "انسخ ملف firmware.elrs إلى بطاقة SD الخاصة بجهاز التحكم، ثم اختر flash ext. ELRS من جهاز التحكم.", + "ShowLog": "إظهار السجل الكامل", + "Step": { + "VERIFYING_BUILD_SYSTEM": "جارٍ التحقق من بيئة البناء", + "DOWNLOADING_FIRMWARE": "تنزيل مصدر البرنامج الثابت", + "BUILDING_FIRMWARE": "جارٍ تنزيل البرنامَج الثابت\n", + "INITIATING_PASSTHROUGH": "جارٍ بدء وضع Passthrough", + "INITIATING_PASSTHROUGH_completed": "اكتمل وضع Passthrough", + "FLASHING_FIRMWARE": "تثبيت البرنامج الثابت على الجهاز" + }, + "Status": { + "InstallingDependencies": "تثبيت أدوات البناء", + "CompilingFirmware": "جاري التجميع", + "PackagingFirmware": "جارٍ تجهيز ملف البرنامج الثابت", + "DetectingDevice": "جارٍ اكتشاف الجهاز", + "ConnectingToDevice": "جارٍ الاتصال بالجهاز", + "ErasingFlash": "جار مسح ذاكرة الفلاش", + "WritingFirmware": "جارٍ كتابة البرنامج الثابت", + "VerifyingFirmware": "جارٍ التحقق من البرنامج الثابت", + "RestartingDevice": "إعادة تشغيل الجهاز", + "UploadingFirmware": "تحميل البرنامج الثابت عبر Wi-Fi", + "TargetMismatch": "تم اختيار هدف خاطئ" + }, + "Failed": { + "InstallingDependencies": "فشل تثبيت أدوات البناء", + "CompilingFirmware": "فشل التجميع", + "PackagingFirmware": "فشل تجهيز ملف البرنامج الثابت", + "DetectingDevice": "تعذر الكشف عن الجهاز", + "ConnectingToDevice": "تعذر الاتصال بالجهاز", + "ErasingFlash": "فشل مسح ذاكرة الفلاش", + "WritingFirmware": "فشل في كتابة البرنامج الثابت", + "VerifyingFirmware": "فشل التحقق من البرنامج الثابت", + "RestartingDevice": "فشل في إعادة تشغيل الجهاز", + "UploadingFirmware": "فشل تحميل البرنامج الثابت عبر Wi-Fi", + "TargetMismatch": "تم اختيار هدف خاطئ", + "Generic": "حدث خطأ ما" + } + }, + "BuildResponse": { + "Error": "خطأ", + "GitDependencyError": "خطأ في تبعية Git", + "PythonDependencyError": "خطأ في تبعيات Python", + "PlatformioDependencyError": "خطأ في تبعيات PlatformIO", + "BuildError": "خطأ في البناء", + "FlashError": "خطأ في التحديث", + "TargetMismatch": "الهدف الذي تحاول تحديثه لا يتطابق مع الهدف الحالي للجهاز. إذا كنت متأكدًا من المتابعة، اضغط «Force Flash» أدناه.", + "Success": "تم بنجاح!", + "ErrorDetails": "حدث خطأ. راجع السجل أعلاه لمعرفة رسالة الخطأ بدقة. إذا لم تكن قد فعلت ذلك بعد، فانتقل إلى Expresslrs.org واقرأ دليل التحديث الخاص بجهازك، بالإضافة إلى دليل استكشاف الأخطاء وإصلاحها. إذا استمرت المشكلة بعد مراجعة الوثائق، فيرجى نسخ سجلات البناء أعلاه إلى أحد مواقع مشاركة النصوص، ثم نشرها في قناة #help-and-support على Discord الخاص بـ ExpressLRS مع رابط للسجلات وأي معلومات أخرى ذات صلة، مثل نوع جهازك، وطريقة التحديث التي كنت تستخدمها، والخطوات التي اتخذتها بالفعل لمحاولة حل المشكلة." + }, + "DeviceOptionsForm": { + "StandardMode": "الوضع القياسي", + "ManualMode": "الوضع اليدوي", + "CopyFromStandardMode": "نسخ من الوضع القياسي", + "LongerBuildTimeWarning": "عند استخدام الوضع اليدوي للإعدادات المخصصة، يتم تعطيل التخزين المؤقت للملفات الثنائية السحابية، مما يؤدي إلى زيادة وقت البناء بشكل ملحوظ.", + "RegulatoryDomainsISM": "النطاق التنظيمي 2.4 جيجاهرتز", + "RegulatoryDomains900": "النطاق التنظيمي 900 ميجاهرتز", + "RegulatoryDomains433": "النطاقات التنظيمية لنطاق 433 ميجاهرتز", + "BindingPhraseSetup": "إعداد العبارة الملزمة", + "CompatibilityOptions": "خيارات التوافق", + "PerformanceOptions": "خيارات الأداء", + "ExtraData": "بيانات إضافية", + "Network": "الشبكة", + "OtherOptions": "خيارات أخرى" + }, + "DeviceTargetForm": { + "UnverifiedHardwareTitle": "جهاز لم يتم التحقق منه", + "UnverifiedHardwareContent": "لم تُقدِّم الشركة المصنِّعة لهذا الجهاز نماذج للمطورين من أجل التقييم والتحقق. تواصل مع الشركة للحصول على الدعم، أو تابع على مسؤوليتك الخاصة. قد لا تعمل جميع الميزات.", + "DeviceCategory": "تصنيف الجهاز", + "Device": "الجهاز:" + }, + "FirmwareVersionForm": { + "OfficialReleases": "الإصدارات الرسمية", + "GitBranch": "فرع Git", + "GitCommit": "Git commit", + "Local": "محلي", + "GitPullRequest": "طلب سحب Git", + "ShowPreRreleases": "إظهار الإصدارات المسبقة", + "Releases": "الإصدارات", + "ReleaseNotes": "ملاحظات الإصدار", + "NewerVersionAvailable": "هناك إصدار أحدث من البرنامج الثابت المتاح", + "UpgradeToV3": "لم يعد هناك دعم لـ ExpressLRS 2.X. الرجاء الترقية إلى ExpressLRS 3.X.", + "DangerZoneTitle": "منطقة الخطر", + "DangerZoneContent": "استخدم هذه المصادر فقط إذا كنت تعرف ما تقوم به أو تلقيت تعليمات من مطوري المشاريع", + "GitBranches": "فرع Git", + "GitCommitHash": "تجزئة الGit", + "LocalPath": "المسار المحلي", + "AttentionTitle": "تنبيه", + "AttentionContent": "أنت تحاول تحديث إصدار مخصّص وقديم من ExpressLRS تابع لـ BetaFPV. أجهزة BetaFPV مدعومة بالكامل في الإصدارات الرسمية الحديثة من ExpressLRS. نوصي باستخدام البرنامج الثابت الرسمي للحصول على أفضل تجربة مع ExpressLRS.", + "ChooseFolder": "اختيار مجلد", + "GitPullRequests": "طلب سحب Git" + }, + "FlashingMethodDescription": { + "BetaflightPassthroughDescription": "تتيح لك هذه الطريقة بحديث المستقبِل أثناء اتصاله بوحدة التحكم بالطيران، وذلك باستخدام ميزة Passthrough في وحدة التحكم بالطيران.", + "PlugInFCToComputer": "قم بتوصيل FC إلى جهاز الكمبيوتر الخاص بك، ولكن لا تتصل بالـ betaflight .", + "SelectDesiredDevice": "حدد خيارات جهازك والجهاز التسلسلي لتحكم الطيران الخاص بك أدناه.", + "RunBuild&Flash": "تشغيل البناء و التحديث.", + "BetaflightPassthroughWiki": "تحقق من صفحتنا على الويكي للحصول على أحدث تعريف.", + "DFU": "تتيح لك هذه الطريقة تحديث المستقبِل أو المرسِل عبر وضع ترقية البرنامج الثابت للجهاز (DFU).", + "GeneralWiki": "تحقق من صفحة ويكي لجهازك الخاص للحصول على مزيد من المعلومات.", + "STLink": "هذه الطريقة تسمح لك بتحديث جهاز الاستقبال أو الإرسال باستخدام مبرمج STLink متصل بالجهاز.", + "StockBL": "تتيح لك هذه الطريقة تحديث المستقبِل أو المرسِل عبر وضع ترقية البرنامج الثابت للجهاز (DFU).", + "UART": "تتيح لك هذه الطريقة تشغيل جهاز الاستقبال أو الإرسال الخاص بك عن طريق منفذ USB الخاص به أو ربط جهاز FTDI.", + "WIFI": "هذه الطريقة تنشئ ملف برنامج ثابت يمكنك تحميله إلى جهاز الاستقبال أو جهاز الإرسال الخاص بك عن طريق الاتصال بشبكة Wifi.", + "EdgeTxPassthrough": "تتيح لك هذه الطريقة بحديث المستقبِل أثناء اتصاله بوحدة التحكم بالطيران، وذلك باستخدام ميزة Passthrough في وحدة التحكم بالطيران.", + "Passthrough": "تتيح لك هذه الطريقة بحديث المستقبِل أثناء اتصاله بوحدة التحكم بالطيران، وذلك باستخدام ميزة Passthrough في وحدة التحكم بالطيران.", + "Zip": "هذه الطريقة تنشئ أرشيفا لجميع الملفات المطلوبة لجهازك." + }, + "FlashingMethodOptions": { + "FlashingMethod": "طريقة التحديث" + }, + "Header": { + "ExpressLRSConfigurator": "اعدادات ExpressLRS™", + "DownloadNewestRelease": "انقر لتنزيل أحدث إصدار", + "UpdateAvailable": "التحديث متاح", + "Documentation": "إرشادات الإستخدام", + "Discord": "ديسكورد", + "FacebookGroup": "مجموعة Facebook", + "Github": "موقع Github", + "OpenCollective": "OpenCollective" + }, + "SerialConnectionForm": { + "SerialDevice": "الجهاز التسلسلي", + "BaudRate": "معدل الباود", + "Connect": "الاتصال " + }, + "SerialDeviceSelect": { + "ManualSelectionTitle": "اختيار الجهاز التسلسلي اليدوي", + "ManualSelectionTitleTooltip": "استخدام اختيار المنفذ التسلسلي اليدوي إذا فشل التحديد التلقائي في تحديد منفذ صحيح. ولا حاجة في الغالبية العظمى من الحالات إلى استخدام هذا الخيار." + }, + "Sidebar": { + "Home": "الصفحة الرئيسية", + "Configurator": "الاعدادات", + "Backpack": "Backpack", + "Logs": "السجلات", + "SerialMonitor": "مراقب المنفذ التسلسلي\n", + "Settings": "الاعدادات", + "Support": "الدعم", + "SwitchToLightTheme": "التبديل إلى الوضع الفاتح", + "SwitchToDarkTheme": "التبديل إلى الوضع الداكن" + }, + "FlashingMethod": { + "BetaflightPassthrough": "Betaflight Passthrough", + "DFU": "DFU (ترقية البرنامج الثابت للجهاز)", + "EdgeTxPassthrough": "EdgeTX Passthrough", + "Passthrough": "Passthrough", + "STLink": "STLink", + "Stock_BL": " الاصلي Bootloader", + "UART": "UART (اتصال تسلسلي)", + "WIFI": "نظام تشغيل الواي-فاي", + "Zip": "ضغط" + }, + "UserDefineKey": { + "AUTO_WIFI_ON_INTERVAL": "تأخير بدء تشغيل Wi-Fi (بالثواني)", + "BINDING_PHRASE": "العبارة الملزمة", + "DEVICE_NAME": "اسم الجهاز", + "DISABLE_ALL_BEEPS": "كتم كل التنبيهات", + "DISABLE_STARTUP_BEEP": "تعطيل تنبيه بدء التشغيل", + "HOME_WIFI_PASSWORD": "كلمة مرور Wi-Fi:", + "HOME_WIFI_SSID": "اسم شبكة Wi-Fi (SSID)", + "JUST_BEEP_ONCE": "تنبيه فردي فقط", + "LOCK_ON_FIRST_CONNECTION": "قفل بعد أول اتصال", + "MY_STARTUP_MELODY": "نغمة بدء التشغيل الخاصة بي", + "RCVR_INVERT_TX": "عكس إشارة منفذ TX للمستقبِل", + "RCVR_UART_BAUD": "معدل الباود للمستقبِل", + "REGULATORY_DOMAIN_AU_433": "433 ميجاهرتز استراليا", + "REGULATORY_DOMAIN_AU_915": "915 ميجاهرتز استراليا", + "REGULATORY_DOMAIN_EU_433": "433 ميجاهرتز أوروبا ", + "REGULATORY_DOMAIN_EU_868": "868 ميجاهرتز أوروبا ", + "REGULATORY_DOMAIN_FCC_915": "915 ميجاهرتز (FCC)", + "REGULATORY_DOMAIN_IN_866": "868 ميجاهرتز الهند", + "REGULATORY_DOMAIN_EU_CE_2400": "2.4 جيجاهرتز LBT (الاستماع قبل الإرسال)", + "REGULATORY_DOMAIN_ISM_2400": "2.4 جيجاهرتز ISM (قياسي)", + "RX_AS_TX": "استخدام المستقبِل كوحدة إرسال (TX)", + "TLM_REPORT_INTERVAL_MS": "الفاصل الزمني لإرسال بيانات التليمترِي (مللي ثانية)", + "UART_INVERTED": "عكس UART", + "UNLOCK_HIGHER_POWER": "فتح إخراج الطاقة الأعلى", + "USE_R9MM_R9MINI_SBUS": "تفعيل SBUS لوحدات R9MM/R9Mini" + }, + "UserDefineDescription": { + "RegulatoryDomain915": "راجع خطط وت regulations ترددات LoRaWAN لاختيار النطاق التنظيمي المناسب لموقعك.", + "Wiki": "تحقق من صفحتنا على الويكي للحصول على أحدث تعريف.", + "RegulatoryDomainISM2400": "نطاقات الراديو ISM هي أجزاء من الطيف الترددي مخصّصة دوليًا. يمكنك استخدام هذا النطاق التنظيمي بأمان في أي دولة.\n", + "RegulatoryDomainEUCE2400": "بالنسبة لإصدارات البرنامج الثابت الأقدم من v3.x، يحد النطاق التنظيمي EU CE بتردد 2.4 جيجاهرتز القدرة القصوى إلى 10mW، ولا يتطلب ذلك استخدام LBT (الاستماع قبل الإرسال). أما في إصدارات البرنامج الثابت الأحدث من v3.x، فيحد النطاق التنظيمي EU CE بتردد 2.4 جيجاهرتز القدرة القصوى إلى 100mW، مع تفعيل LBT (الاستماع قبل الإرسال).\n", + "BindingPhrase": "تقوم عبارة الربط (Binding Phrase) بربط أجهزة الاستقبال بأجهزة الإرسال دون الحاجة إلى إجراء الربط يدويًا. أجهزة الاستقبال التي يتم تحديثها من دون عبارة ربط ستحتاج إلى استخدام طريقة الربط التقليدية. يجب أن تكون عبارة الربط نفسها مستخدمة في كلٍّ من TX وRX حتى يتم الاتصال. اختر عبارة يسهل تذكّرها، ويُفضّل أن تتكوّن من أحرف وأرقام باستخدام الأبجدية اللاتينية فقط. هذه العبارة ليست كلمة مرور ولا توفر أي حماية أمنية، وإنما تساعد على تقليل احتمالية تداخل إشارات RF مع طيارين آخرين.\n", + "AutoWifiOnInterval": "

إذا لم يتم اكتشاف اتصال RC خلال الوقت المحدد بعد بدء التشغيل، فستقوم الوحدة تلقائيًا بتفعيل نقطة اتصال Wi-Fi الخاصة بها. يتيح لك ذلك الاتصال بشبكة Wi-Fi الخاصة بالوحدة والدخول إلى 10.0.0.1 لإجراء تحديثات البرنامج الثابت.

يتم تحديد مهلة التفعيل التلقائي بوحدة الثواني. القيمة الافتراضية: 60 ثانية.

", + "TLMReportIntervalMs": "

اجعل وحدة TX ترسل بيانات التليمترِي إلى جهاز التحكم على فاصل زمني ثابت. يساعد ذلك في تقليل تحذيرات 「Telemetry Lost」 عند استخدام نسبة تليمترِي مرتفعة أو معدلات منخفضة مثل 50Hz.

القيمة الافتراضية هي 240LU. إذا أردت تغييرها، يجب إضافة LU بعد قيمة المللي ثانية. على سبيل المثال، لتحديد معدل تحديث تليمترِي قدره 100 ms، أدخل القيمة بهذا الشكل: 100LU.

عادةً يُفضّل الإبقاء على 240LU لأجهزة التحكم التي تعمل بنظام EdgeTX/OpenTX، واستخدام 100LU للأجهزة التي تعمل بنظام ErskyTx.

", + "LockOnFirstConnection": "قفل وضع RF - في الوضع الافتراضي، يتنقّل RX بين أوضاع RF المتاحة مع انتظار 5 ثوانٍ بين كل وضع وآخر، بدءًا من أعلى وضع إلى أدناه، حتى يعثر على الوضع الذي يرسل عليه TX. يتيح ذلك لـ RX التنقّل بين الأوضاع، ولكن بمجرد إنشاء الاتصال، سيتوقف RX عن التنقّل بين أوضاع RF حتى تتم إعادة تشغيل الطاقة.\n", + "JustBeepOnce": "يحدد هذا الخيار ما إذا كان TX سيصدر صفيرًا مرة واحدة فقط بدلًا من تشغيل نغمة بدء التشغيل.\n", + "DisableStartupBeep": "يحدد هذا الخيار ما إذا كان TX سيبقى صامتًا عند بدء التشغيل.", + "DisableAllBeeps": "تعطيل جميع التنبيهات.", + "MyStartupMelody": "

استخدم هذا الخيار لتحديد نغمة بدء التشغيل الخاصة بك باستخدام صيغة BlHeli32. يجب تحديد كلٍ من music string وbpm، بينما يُعد semitone offset اختياريًا ويمكن استخدامه لرفع النغمة بالكامل أو خفضها بعدد أنصاف النغمات المحدد. تتوفر أمثلة على نغمات BlHeli32 في قناة Rox Wolfs على YouTube، وقد تحتاج إلى بعض التجربة للوصول إلى النتيجة المطلوبة.

لإنشاء نغمتك الخاصة، يُعد كلٌ من Sheet Music 101 وBLHeli Piano من المصادر المفيدة.

يدعم هذا الخيار الآن أيضًا النغمات بتنسيق RTTTL. ويُعد EscTunes.com مصدرًا ممتازًا لاكتشاف نغمات جديدة.

\n", + "UnlockHigherPower": "

وردت بعض التقارير عن عدم استقرار وحدات R9M عند استخدام قدرة أعلى من 250mW مع نظام التبريد الأصلي. ويرجع ذلك جزئيًا إلى أن ELRS يستخدم دورة تشغيل أعلى للإرسال مقارنة بالبرنامج الثابت الأصلي. افتراضيًا، تكون القدرة محدودة إلى 250mW. يمكنك فتح القدرة حتى 1000mW من خلال تفعيل الخيار التالي. استخدم هذا الخيار على مسؤوليتك الخاصة إذا لم تُجرِ أي تعديلات على نظام التبريد.

قمنا بنشر R9M Fan Mod Cover، وهو غطاء خلفي مخصص مطبوع بتقنية ثلاثية الأبعاد، يحتوي على مساحة لتركيب مروحة وتبريد إضافي للسماح باستخدام القدرة القصوى (1–2W حسب نوع التعديل).

\n", + "UARTInverted": "يتيح هذا الخيار التوافق مع أجهزة التحكم التي تُخرج إشارة CRSF معكوسة، مثل FrSky QX7 وTBS Tango 2 وRadioMaster TX16S. يُفضّل إبقاء هذا الخيار مفعّلًا في معظم الحالات. إذا كان جهاز التحكم لديك هو T8SG V2 أو كنت تستخدم برنامج Deviation، فقم بتعطيل هذا الإعداد.", + "RCVRUARTBaud": "معدل الباود لـ UART في المستقبِل. القيمة الافتراضية هي 420000. اضبط هذا الخيار على 400000 ليعمل المستقبِل مع وحدات التحكم بالطيران Kiss v1.\n", + "RCVRInvertTX": "إذا كنت تستخدم وحدة تحكم طيران تحتوي فقط على منفذ RXI / SBUS (RX معكوس)، ففعّل هذا الخيار لعكس خرج CRSF من المستقبِل حتى تتمكن من استخدام هذا المنفذ. هذا الخيار لا يحوّل الإشارة إلى SBUS؛ بل تبقى الإشارة CRSF معكوسة، لذلك يجب أن يظل CRSF هو بروتوكول المستقبِل المحدد في برنامج وحدة التحكم بالطيران. هذا الخيار مخصص فقط لأجهزة الاستقبال المعتمدة على ESP.", + "UseR9MMR9MiniSBUS": "

هذا الخيار لا يفعّل بروتوكول SBUS.

يفيد هذا الخيار مع وحدات التحكم بالطيران F4 التي لا توفر خيار UART غير معكوس. يعمل الاتصال في اتجاه واحد فقط، لذلك ستفقد إرسال بيانات التليمترِي إلى جهاز التحكم، كما ستفقد إمكانية التفليش عبر Passthrough. سيتم إخراج الإشارة من منفذ S.BUS الافتراضي في R9MM/R9Mini. وقد تحتاج أيضًا إلى ضبط serialrx_inverted = ON داخل Betaflight لضمان التوافق.

", + "HomeWifiSSID": "تعيين اسم شبكة Wi-Fi الرئيسية (SSID). سيسمح هذا الجهاز بتشغيل Wi-Fi للاتصال بالشبكة الرئيسية تلقائياً.", + "HomeWifiPassword": "تعيين اسم شبكة Wi-Fi الرئيسية (SSID). سيسمح هذا الجهاز بتشغيل Wi-Fi للاتصال بالشبكة الرئيسية تلقائياً.", + "RxAsTx": "قم بتثبيت جهاز الاستقبال الخاص بك مع هذا الخيار مفعّل واستخدمه كوحدة TX." + }, + "UserDefinesAdvisor": { + "DisableUARTInvertedWarning": "تعطيل UART_INVERTED غير شائع. الرجاء التأكد من أن المرسل الخاص بك يدعم ذلك." + }, + "UserDefinesList": { + "BindingPhraseHistory": "", + "BindingPhraseHistoryRemove": "احذف من السجل", + "CustomBindingPhrase": "عبارة ربط مخصصة", + "MyStartupMelody": "نغمة بدء التشغيل الخاصة بي", + "Value": "قيمة" + }, + "WifiDeviceList": { + "Name": "الاسم", + "Target": "الهدف", + "Version": "الإصدار", + "Type": "النوع", + "DNS": "نظـام أسماء النطـاقات", + "IP": "عنوان IP", + "Select": "اختر" + }, + "WifiDeviceNotification": { + "NewDevice": "جهاز جديد", + "Select": "اختر" + }, + "WifiDeviceSelect": { + "DefaultWifiIp": "الافتراضي (10.0.0.1)", + "WifiDevices": "أجهزة WiFi", + "Manual": "دليل المستخدم", + "WifiDeviceSelection": "اختيار جهاز WiFi", + "IPAddress": "عنوان بروتوكول IP" + }, + "Updater": { + "FoundUpdates": "تم العثور على تحديثات", + "UpdateNow": "تم العثور على تحديثات، هل تريد التحديث الآن؟", + "Sure": "بالتأكيد", + "Later": "لاحقاً", + "UpdateDownloading": "جارٍ تنزيل التحديث", + "NotifiedWhenReadyToInstall": "يتم تحميل التحديث، سيتم إعلامك عندما تكون جاهزة للتثبيت.", + "Restart": "إعادة تشغيل", + "ApplicationUpdate": "تحديثات التطبيق", + "Update": "تحديث", + "ApplyUpdates": "تم تنزيل إصدار جديد. أعد تشغيل التطبيق لتطبيق التحديثات." + }, + "ConfiguratorView": { + "DownloadLUAScript": "تحميل البرنامج النصي LUA", + "FirmwareVersion": "إصدار البرنامج الثابت", + "Target": "الهدف", + "Notice": "تنويه", + "SelectFirmwareVersionFirst": "الرجاء تحديد إصدار البرنامج الثابت أولاً.", + "DeviceOptions": "خيارات الجهاز", + "ResetDevice": "إعادة تعيين خيارات الجهاز إلى الإعدادات الافتراضية الموصى بها على هدف هذا الجهاز. باستثناء عبارة الربط المخصصة.", + "Reset": "إعادة الضبط", + "Actions": "الإجراءات", + "FlashingOptions": "خيارات التحديث", + "EraseBeforeFlash": "مسح قبل التحديث", + "ForceFlash": "تحديث إجباري", + "Build": "بناء", + "Flash": "تحديث", + "NetworkDevices": "أجهزة الشبكة", + "DeviceSelectError": "خطأ في تحديد الجهاز", + "DeviceCouldNotBeAutomaticallySelected": "لا يمكن اختيار الجهاز المستهدف تلقائياً، ويجب أن يتم ذلك يدوياً.", + "Close": "إغلاق", + "Logs": "السجلات", + "CopyLogToClipboard": "نسخ السجل إلى الحافظة", + "SaveLogToFile": "حفظ السجل في مِلَفّ", + "BuildsTakeMinutesFirstTime": "في بعض الأحيان يستغرق البناء بضع دقائق على الأقل. إنه أمر طبيعي، خاصة لأول مرة.", + "Result": "النتيجة", + "Warning": "تحذير", + "WaitForLEDBeforeDisconnectingPower": "", + "UpdateLuaScript": "تحديث برنامج Lua Script", + "UpdateLuaScriptOnRadio": "تأكد من تحديث LUA SCRIPT على الراديو الخاص بك", + "BuildNotice": "اشعار البناء", + "FirmwareOpenedInFileExplorer": "تم فتح الملف الثنائي للبرنامج الثابت في مستكشف الملفات", + "Back": "عودة", + "FlashAnother": "تحديث آخر", + "Retry": "أعد المحاولة" + }, + "LogsView": { + "Logs": "السجلات", + "OpenLogsFolder": "فتح مجلد السجلات" + }, + "SerialMonitorView": { + "SerialMonitor": "مراقب المنفذ التسلسلي\n", + "Disconnect": "قطع الاتصال" + }, + "SettingsView": { + "Settings": "الاعدادات", + "LanguageSelector": "اختيار اللغة", + "CurrentLanguage": "اللغة الحالية", + "ExpertMode": "وضع الخبير", + "ApplicationOptions": "خيارات التطبيق", + "ExpertModeDescription": "تمكين وضع الخبير لفتح المميزات المتقدمة بما في ذلك مصادر البرامج الثابتة الإضافية. القدرة على بناء برنامج ثابت مع المعلمات المحددة يدويًا للمستخدم، والوصول إلى شاشة الرصد التسلسلية للحصول على تشخيصات مفصلة.", + "ThemeSelector": "مظهر", + "ThemeSystem": "النظام (وفق تفضيلات نظام التشغيل)", + "ThemeLight": "فاتح", + "ThemeDark": "داكن" + }, + "SupportView": { + "Support": "الدعم", + "JoinTheCommunity": "بحاجة إلى مساعدة؟ حائر ؟ انضم إلى المجتمع!", + "ExpressLRSDocumentation": "وثائق ExpressLRS", + "DiscordChat": "دردشة ديسكورد", + "FacebookGroup": "مجموعة الفيسبوك", + "Troubleshooting": "استكشاف الأخطاء وإصلاحها", + "LegalDisclaimer": "إخلاء المسؤولية القانونية", + "UseAtYourOwnRisk": "وقد يتطلب استخدام هذا النوع من الأجهزة وتشغيله ترخيص، وقد تحظر بعض البلدان استخدامه. والأمر متروك تماما للمستخدم النهائي لضمان الامتثال للأنظمة المحلية. هذا برنامج/معدات تجريبية ولا يوجد ضمان للاستقرار أو الموثوقية. استخدم على الفور RISK.", + "CorruptedFirmwareFiles": "ملفات البرامج الثابتة التالفة", + "YouCanManuallyClearAllFiles": "إذا أغلقت Configurator أثناء تنزيل Git لملفات البرنامج الثابت، فقد تتلف حالة مستودع Git. يمكنك مسح جميع الملفات يدويًا ثم بدء عملية البناء من جديد.", + "ClearFirmwareFiles": "مسح ملفات البرامج الثابتة", + "CorruptedFirmwareFilesDeleted": "تم حذف ملفات البرامج الثابتة الفاسدة وتم تنظيف مستودع git. حاول البناء مرة أخرى.", + "CorruptedPlatformioDependencies": "تبعيات PlatformIO تالفة", + "YouCanManuallyClearAllDependencies": "إذا أغلقت Configurator أثناء تنزيل Git لملفات البرنامج الثابت، فقد تتلف حالة مستودع Git. يمكنك مسح جميع الملفات يدويًا ثم بدء عملية البناء من جديد.", + "ClearPlatformioDependencies": "تبعيات PlatformIO تالفة", + "CorruptedPlatformioDependenciesDeleted": "تم حذف ملفات البرامج الثابتة الفاسدة وتم تنظيف مستودع git. حاول البناء مرة أخرى." + }, + "HomeView": { + "Flash": { + "Title": "تثبيت البرنامج الثابت", + "ExpressLRS": "البرنامج الثابت ExpressLRS", + "ExpressLRSDescription": "قم بتشغيل البرنامج الثابت الرئيسي ExExpressLRS إلى جهاز الإرسال أو جهاز الاستقبال.", + "Backpack": "Backpack", + "BackpackDescription": "قم بتحديث برنامج Backpack الثابت إلى وحدات TX أو مستقبلات الفيديو أو متتبعات الهوائيات أو مؤقتات السباقات." + }, + "Resources": { + "Title": "الموارد و البدء", + "GettingStarted": "الحصول على دليل البداية", + "GettingStartedDescription": "جديد إلى ExpressLRS؟ قراءة دليل البداية السريعة خطوة بخطوة.", + "ProductFinder": "الباحث عن المنتج", + "ProductFinderDescription": "العثور على الأجهزة المدعومة وتحديد هوية المرسل أو المستلم الخاص بك.", + "Documentation": "إرشادات الإستخدام", + "DocumentationDescription": "تصفح جميع وثائق ExpressLRS في expresslrs.org.", + "LuaScripts": "Lua Scripts", + "LuaScriptsDescription": "قم بتنزيل البرامج النصية لـ ExpressLRS Lua لـ EdgeTX و OpenTX." + }, + "Community": { + "Title": "إنضم إلى المجتمع", + "Subtitle": "الآلاف من الطيارين والمطورين موجودين هنا.", + "Discord": "ديسكورد", + "DiscordDescription": "انضم إلى المحادثة. احصل على المساعدة، شارك البناءات، واتصل بالآلاف من الطيارين والمطورين.", + "JoinDiscord": "انضم إلى ديسكورد", + "Facebook": "فيسبوك", + "FacebookDescription": "انضم إلى مجموعة الفيس بوك الخاصة بنا - واحدة من أكثر المجتمعات نشاطا في FPV مع الآلاف من الأعضاء.", + "JoinGroup": "الانضمام إلى المجموعة" + }, + "Support": { + "Title": "دعم المشروع", + "Subtitle": "سواء كنت تكتب الكود أو تحسن التوثيق أو أبلغ عن الأخطاء أو تدعم المشروع - كل المساهمة مهمة.", + "GitHub": "GitHub", + "GitHubDescription": "المساعدة في بناء مستقبل الـ RC. المدونة، التوثيق والاختبار - كل المساهمة مهمة.", + "Contribute": "المساهمة", + "OpenCollective": "افتح المجموعة", + "OpenCollectiveDescription": "دعم تطوير ExpressLRS بمساهمة لمرة واحدة أو متكررة.", + "SupportProject": "الدعم" + } + } +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 1215f743..c3f479c4 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,9 +2,14 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're import { Routes, Route, HashRouter, Navigate } from 'react-router'; import CssBaseline from '@mui/material/CssBaseline'; import '@fontsource/roboto'; -import { ThemeProvider, StyledEngineProvider } from '@mui/material'; +import { ThemeProvider } from '@mui/material'; +import { CacheProvider } from '@emotion/react'; +import createCache from '@emotion/cache'; +import { prefixer } from 'stylis'; +import rtlPlugin from 'stylis-plugin-rtl'; +import { useTranslation } from 'react-i18next'; import { ApolloProvider } from '@apollo/client/react'; -import { createAppTheme } from './theme'; +import { createAppTheme, ThemeDirection } from './theme'; import client from './gql'; import ConfiguratorView from './views/ConfiguratorView'; import LogsView from './views/LogsView'; @@ -23,13 +28,31 @@ import useBuildProgressNotifications from './hooks/useBuildProgressNotifications import useBuildLogs from './hooks/useBuildLogs'; import useResolvedThemeMode from './hooks/useResolvedThemeMode'; +// Emotion caches per text direction. The RTL cache flips left/right styles +// (margins, paddings, positions) for right-to-left languages such as Arabic. +// `prepend: true` replaces StyledEngineProvider's injectFirst. +const emotionCaches: Record> = { + ltr: createCache({ key: 'mui', prepend: true }), + rtl: createCache({ + key: 'muirtl', + prepend: true, + stylisPlugins: [prefixer, rtlPlugin], + }), +}; + interface ThemeWrapperProps { children: React.ReactNode; } const ThemeWrapper: FunctionComponent = ({ children }) => { const resolvedMode = useResolvedThemeMode(); - const theme = useMemo(() => createAppTheme(resolvedMode), [resolvedMode]); + const { i18n } = useTranslation(); + const language = i18n.resolvedLanguage ?? i18n.language; + const direction: ThemeDirection = i18n.dir(language); + const theme = useMemo( + () => createAppTheme(resolvedMode, direction, language), + [resolvedMode, direction, language], + ); // Sync the body/html background with the current theme. // The flash-prevention script in index.ejs sets an inline background-color @@ -42,12 +65,12 @@ const ThemeWrapper: FunctionComponent = ({ children }) => { }, [theme]); return ( - + {children} - + ); }; diff --git a/src/ui/theme/index.ts b/src/ui/theme/index.ts index c50217f7..5cf8d79b 100644 --- a/src/ui/theme/index.ts +++ b/src/ui/theme/index.ts @@ -1,14 +1,27 @@ import { createTheme, Theme } from '@mui/material'; +import { arSD, Localization } from '@mui/material/locale'; import { darkPalette, lightPalette } from './palette'; import getComponentOverrides from './overrides'; export type ResolvedThemeMode = 'dark' | 'light'; -export function createAppTheme(mode: ResolvedThemeMode): Theme { +export type ThemeDirection = 'ltr' | 'rtl'; + +// MUI built-in component strings (Alert close label, Autocomplete "No options", etc.) +const muiLocales: Record = { + ar: arSD, +}; + +export function createAppTheme( + mode: ResolvedThemeMode, + direction: ThemeDirection = 'ltr', + language = 'en', +): Theme { const palette = mode === 'dark' ? darkPalette : lightPalette; // First pass: build theme with palette const baseTheme = createTheme({ + direction, shape: { borderRadius: 0, }, @@ -16,9 +29,13 @@ export function createAppTheme(mode: ResolvedThemeMode): Theme { }); // Second pass: build theme with component overrides that reference resolved tokens - return createTheme(baseTheme, { - components: getComponentOverrides(baseTheme), - }); + return createTheme( + baseTheme, + { + components: getComponentOverrides(baseTheme), + }, + muiLocales[language] ?? {}, + ); } export default createAppTheme('dark'); diff --git a/src/ui/views/SettingsView/index.tsx b/src/ui/views/SettingsView/index.tsx index d61a09fb..96903fe0 100644 --- a/src/ui/views/SettingsView/index.tsx +++ b/src/ui/views/SettingsView/index.tsx @@ -76,7 +76,7 @@ const SettingsView: FunctionComponent = () => { icon={} title={t('SettingsView.LanguageSelector')} /> - + { icon={} title={t('SettingsView.ThemeSelector')} /> - + { icon={} title={t('SettingsView.ApplicationOptions')} /> - +