mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-12 22:55:45 +08:00
feat: enable Arabic in production UI and add complete Arabic documentation (#13315)
### What problem does this PR solve? This PR adds end-to-end Arabic support in production. It also adds a full Arabic README ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Documentation Update
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
412
README_ar.md
Normal file
412
README_ar.md
Normal file
@@ -0,0 +1,412 @@
|
||||
<div align="center">
|
||||
<a href="https://demo.ragflow.io/">
|
||||
<img src="web/src/assets/logo-with-text.svg" width="520" alt="ragflow logo">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-DBEDFA"></a>
|
||||
<a href="./README_zh.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_tzh.md"><img alt="繁體版中文自述文件" src="https://img.shields.io/badge/繁體中文-DFE0E5"></a>
|
||||
<a href="./README_ja.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-DFE0E5"></a>
|
||||
<a href="./README_ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-DFE0E5"></a>
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/intent/follow?screen_name=infiniflowai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/infiniflow?logo=X&color=%20%23f5f5f5" alt="follow on X(Twitter)">
|
||||
</a>
|
||||
<a href="https://demo.ragflow.io" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
|
||||
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.24.0">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
|
||||
</a>
|
||||
<a href="https://github.com/infiniflow/ragflow/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?labelColor=d4eaf7&color=2e6cc4" alt="license">
|
||||
</a>
|
||||
<a href="https://deepwiki.com/infiniflow/ragflow">
|
||||
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://ragflow.io/docs/dev/">Document</a> |
|
||||
<a href="https://github.com/infiniflow/ragflow/issues/12241">Roadmap</a> |
|
||||
<a href="https://twitter.com/infiniflowai">Twitter</a> |
|
||||
<a href="https://discord.gg/NjYzJD3GM3">Discord</a> |
|
||||
<a href="https://demo.ragflow.io">Demo</a>
|
||||
</h4>
|
||||
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary><b>📕 جدول المحتويات</b></summary>
|
||||
|
||||
- 💡 [ما هو RAGFlow؟](#-what-is-ragflow)
|
||||
- 🎮 [Demo](#-demo)
|
||||
- 📌 [آخر التحديثات](#-latest-updates)
|
||||
- 🌟 [الميزات الرئيسية](#-key-features)
|
||||
- 🔎 [بنية النظام](#-system-architecture)
|
||||
- 🎬 [ابدأ](#-get-started)
|
||||
- 🔧 [التكوينات](#-configurations)
|
||||
- 🔧 [إنشاء صورة Docker](#-build-a-docker-image)
|
||||
- 🔨 [إطلاق الخدمة من المصدر للتطوير](#-launch-service-from-source-for-development)
|
||||
- 📚 [التوثيق](#-documentation)
|
||||
- 📜 [Roadmap](#-roadmap)
|
||||
- 🏄 [المجتمع](#-community)
|
||||
- 🙌 [مساهمة](#-contributing)
|
||||
|
||||
</details>
|
||||
|
||||
## 💡 ما هو RAGFlow؟
|
||||
|
||||
يُعد مشروع [RAGFlow](https://ragflow.io/) محركًا رائدًا ومفتوح المصدر للاسترجاع المعزز بالتوليد (<bdi dir="ltr">RAG</bdi>)، ويجمع أحدث تقنيات <bdi dir="ltr">RAG</bdi> مع قدرات الوكلاء لبناء طبقة سياق متقدمة لنماذج <bdi dir="ltr">LLMs</bdi>. يوفّر سير عمل <bdi dir="ltr">RAG</bdi> مبسّطًا وقابلًا للتكيّف مع المؤسسات بمختلف أحجامها. وبالاعتماد على [محرك سياق موحّد](https://ragflow.io/basics/what-is-agent-context-engine) وقوالب وكلاء جاهزة، يتيح <bdi dir="ltr">RAGFlow</bdi> للمطورين تحويل البيانات المعقّدة إلى أنظمة <bdi dir="ltr">AI</bdi> عالية الدقة وجاهزة للإنتاج بكفاءة وموثوقية.
|
||||
|
||||
## 🎮 Demo
|
||||
|
||||
جرّب النسخة التجريبية على [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/chunking.gif" width="1200"/>
|
||||
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/agentic-dark.gif" width="1200"/>
|
||||
</div>
|
||||
|
||||
## 🔥 آخر التحديثات
|
||||
|
||||
- 2025-12-26 يدعم ميزة "Memory" لوكلاء الذكاء الاصطناعي.
|
||||
- 11-11-2025 يدعم Gemini 3 Pro.
|
||||
- 12-11-2025 يدعم مزامنة البيانات من Confluence، S3، Notion، Discord، Google Drive.
|
||||
- 23-10-2025 يدعم MinerU وDocling كطرق لتحليل المستندات.
|
||||
- 15-10-2025 يدعم العرض الأوركسترالي pipeline.
|
||||
- 08-08-2025 يدعم أحدث موديلات سلسلة OpenAI.
|
||||
- 01-08-2025 يدعم سير العمل الوكيل وMCP.
|
||||
- 23-05-2025 تمت إضافة مكون منفذ كود Python/JavaScript إلى Agent.
|
||||
- 05-05-2025 يدعم الاستعلام بين اللغات.
|
||||
- 19-03-2025 يدعم استخدام نموذج متعدد الوسائط لفهم الصور داخل ملفات PDF أو DOCX.
|
||||
|
||||
## 🎉 تابعونا
|
||||
|
||||
⭐️ قم بتمييز مستودعنا بنجمة لتبقى على اطلاع بالميزات والتحسينات الجديدة والمثيرة! احصل على إشعارات فورية بالجديد
|
||||
الإصدارات! 🌟
|
||||
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/18c9707e-b8aa-4caf-a154-037089c105ba" width="1200"/>
|
||||
</div>
|
||||
|
||||
## 🌟 الميزات الرئيسية
|
||||
|
||||
### 🍭 **"الجودة في الداخل، الجودة في الخارج"**
|
||||
|
||||
- [الفهم العميق للمستندات](./deepdoc/README.md) لاستخراج المعرفة من البيانات غير المنظمة
|
||||
ذات التنسيقات المعقدة.
|
||||
- يجد "إبرة في كومة قش بيانات" من الرموز غير المحدودة حرفيًا.
|
||||
|
||||
### 🍱 **التقطيع القائم على القالب**
|
||||
|
||||
- ذكي وقابل للتفسير.
|
||||
- الكثير من خيارات القالب للاختيار من بينها.
|
||||
|
||||
### 🌱 **استشهادات مؤرضة لتقليل الهلوسة**
|
||||
|
||||
- تصور تقطيع النص للسماح بالتدخل البشري.
|
||||
- عرض سريع للمراجع الرئيسية والاستشهادات التي يمكن تتبعها لدعم الإجابات المبنية على أسس سليمة.
|
||||
|
||||
### 🍔 **التوافق مع مصادر البيانات غير المتجانسة**
|
||||
|
||||
- يدعم Word، والشرائح، وExcel، وtxt، والصور، والنسخ الممسوحة ضوئيًا، والبيانات المنظمة، وصفحات الويب، والمزيد.
|
||||
|
||||
### 🛀 **سير عمل RAG آلي وسهل**
|
||||
|
||||
- تنسيق RAG مبسط يلبي احتياجات الشركات الشخصية والكبيرة على حد سواء.
|
||||
- نماذج LLMs قابلة للتكوين بالإضافة إلى نماذج embedding.
|
||||
- الاستدعاء المتعدد المقترن بإعادة التصنيف المدمجة.
|
||||
- APIs بديهي للتكامل السلس مع الأعمال.
|
||||
|
||||
## 🔎 هندسة النظام
|
||||
|
||||
<div align="center" style="margin-top:20px;margin-bottom:20px;">
|
||||
<img src="https://github.com/user-attachments/assets/31b0dd6f-ca4f-445a-9457-70cb44a381b2" width="1000"/>
|
||||
</div>
|
||||
|
||||
## 🎬 ابدأ
|
||||
|
||||
### 📝 المتطلبات الأساسية
|
||||
|
||||
- CPU >= 4 مراكز
|
||||
- الرام >= 16 جيجا
|
||||
- القرص >= 50 جيجا بايت
|
||||
- Docker >= 24.0.0 & Docker Compose >= v2.26.1
|
||||
- [gVisor](https://gvisor.dev/docs/user_guide/install/): مطلوب فقط إذا كنت تنوي استخدام ميزة منفذ التعليمات البرمجية (وضع الحماية) لـ RAGFlow.
|
||||
|
||||
> [!TIP]
|
||||
> إذا لم تقم بتثبيت Docker على جهازك المحلي (Windows أو Mac أو Linux)، راجع [تثبيت Docker Engine](https://docs.docker.com/engine/install/).
|
||||
|
||||
### 🚀 بدء تشغيل الخادم
|
||||
|
||||
1. تأكد من `vm.max_map_count` >= 262144:
|
||||
|
||||
> للتحقق من قيمة `vm.max_map_count`:
|
||||
>
|
||||
> ```bash
|
||||
> $ sysctl vm.max_map_count
|
||||
> ```
|
||||
>
|
||||
> أعد تعيين `vm.max_map_count` إلى قيمة 262144 على الأقل إذا لم تكن كذلك.
|
||||
>
|
||||
> ```bash
|
||||
> # In this case, we set it to 262144:
|
||||
> $ sudo sysctl -w vm.max_map_count=262144
|
||||
> ```
|
||||
>
|
||||
> سيتم إعادة ضبط هذا التغيير بعد إعادة تشغيل النظام. لضمان بقاء التغيير دائمًا، قم بإضافة أو تحديث
|
||||
> `vm.max_map_count` القيمة في **/etc/sysctl.conf** وفقًا لذلك:
|
||||
>
|
||||
> ```bash
|
||||
> vm.max_map_count=262144
|
||||
> ```
|
||||
>
|
||||
2. استنساخ الريبو:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/infiniflow/ragflow.git
|
||||
```
|
||||
3. ابدأ تشغيل الخادم باستخدام صور Docker المعدة مسبقًا:
|
||||
|
||||
> [!CAUTION]
|
||||
> جميع الصور Docker مصممة لمنصات x86. لا نعرض حاليًا صور Docker لـ ARM64.
|
||||
> إذا كنت تستخدم نظامًا أساسيًا ARM64، فاتبع [هذا الدليل](https://ragflow.io/docs/dev/build_docker_image) لإنشاء صورة Docker متوافقة مع نظامك.
|
||||
|
||||
> يقوم الأمر أدناه بتنزيل إصدار `v0.24.0` من الصورة RAGFlow Docker. راجع الجدول التالي للحصول على أوصاف لإصدارات RAGFlow المختلفة. لتنزيل إصدار RAGFlow مختلف عن `v0.24.0`، قم بتحديث المتغير `RAGFLOW_IMAGE` وفقًا لذلك في **docker/.env** قبل استخدام `docker compose` لبدء تشغيل الخادم.
|
||||
|
||||
```bash
|
||||
$ cd ragflow/docker
|
||||
|
||||
# git checkout v0.24.0
|
||||
# Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases)
|
||||
# This step ensures the **entrypoint.sh** file in the code matches the Docker image version.
|
||||
|
||||
# Use CPU for DeepDoc tasks:
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
|
||||
# To use GPU to accelerate DeepDoc tasks:
|
||||
# sed -i '1i DEVICE=gpu' .env
|
||||
# docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> ملاحظة: قبل `v0.22.0`، قدمنا كلتا الصورتين بنماذج embedding وصورًا رفيعة بدون نماذج embedding. التفاصيل على النحو التالي:
|
||||
|
||||
| RAGFlow علامة الصورة | حجم الصورة (جيجابايت) | هل لديه نماذج embedding؟ | مستقر؟ |
|
||||
|-------------------|-----------------|-----------------------|----------------|
|
||||
| v0.21.1 | ≈9 | ✔️ | إصدار مستقر |
|
||||
| v0.21.1-slim | ≈2 | ❌ | إصدار مستقر |
|
||||
|
||||
> بدءًا من `v0.22.0`، نقوم بشحن الإصدار النحيف فقط ولم نعد نلحق اللاحقة **-slim** بعلامة الصورة.
|
||||
|
||||
4. التحقق من حالة الخادم بعد تشغيل الخادم:
|
||||
|
||||
```bash
|
||||
$ docker logs -f docker-ragflow-cpu-1
|
||||
```
|
||||
|
||||
_النتيجة التالية تؤكد الإطلاق الناجح للنظام:_
|
||||
|
||||
```bash
|
||||
|
||||
____ ___ ______ ______ __
|
||||
/ __ \ / | / ____// ____// /____ _ __
|
||||
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
|
||||
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
|
||||
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
|
||||
|
||||
* Running on all addresses (0.0.0.0)
|
||||
```
|
||||
|
||||
> إذا تخطيت خطوة التأكيد هذه وقمت بتسجيل الدخول مباشرة إلى RAGFlow، فقد يعرض متصفحك تنبيه `network abnormal`
|
||||
> خطأ لأنه في تلك اللحظة، قد لا تتم تهيئة RAGFlow بشكل كامل.
|
||||
>
|
||||
5. في متصفح الويب الخاص بك، أدخل عنوان IP الخاص بالخادم الخاص بك وقم بتسجيل الدخول إلى RAGFlow.
|
||||
|
||||
> باستخدام الإعدادات الافتراضية، ما عليك سوى إدخال `http://IP_OF_YOUR_MACHINE` (**من دون** رقم المنفذ) كإعداد افتراضي
|
||||
> HTTP يمكن حذف منفذ العرض `80` عند استخدام التكوينات الافتراضية.
|
||||
>
|
||||
6. في [service_conf.yaml.template](./docker/service_conf.yaml.template)، حدد المصنع LLM المطلوب في `user_default_llm` وقم بالتحديث
|
||||
الحقل `API_KEY` مع مفتاح API المقابل.
|
||||
|
||||
> راجع [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) لمزيد من المعلومات.
|
||||
>
|
||||
|
||||
_العرض بدأ!_
|
||||
|
||||
## 🔧 التكوينات
|
||||
|
||||
عندما يتعلق الأمر بتكوينات النظام، ستحتاج إلى إدارة الملفات التالية:
|
||||
|
||||
- [.env](./docker/.env): يحتفظ بالإعدادات الأساسية للنظام، مثل `SVR_HTTP_PORT`، `MYSQL_PASSWORD`، و
|
||||
`MINIO_PASSWORD`.
|
||||
- [service_conf.yaml.template](./docker/service_conf.yaml.template): تكوين الخدمات الخلفية. سيتم ملء متغيرات البيئة في هذا الملف تلقائيًا عند بدء تشغيل الحاوية Docker. ستكون أي متغيرات بيئة تم تعيينها داخل حاوية Docker متاحة للاستخدام، مما يسمح لك بتخصيص سلوك الخدمة استنادًا إلى بيئة النشر.
|
||||
- [docker-compose.yml](./docker/docker-compose.yml): يعتمد النظام على [docker-compose.yml](./docker/docker-compose.yml) لبدء التشغيل.
|
||||
|
||||
> يوفر الملف [./docker/README](./docker/README.md) وصفًا تفصيليًا لإعدادات البيئة والخدمة
|
||||
> التكوينات التي يمكن استخدامها كـ `${ENV_VARS}` في ملف [service_conf.yaml.template](./docker/service_conf.yaml.template).
|
||||
|
||||
لتحديث منفذ العرض الافتراضي HTTP (80)، انتقل إلى [docker-compose.yml](./docker/docker-compose.yml) وقم بتغيير `80:80`
|
||||
إلى `<YOUR_SERVING_PORT>:80`.
|
||||
|
||||
تتطلب تحديثات التكوينات المذكورة أعلاه إعادة تشغيل جميع الحاويات لتصبح سارية المفعول:
|
||||
|
||||
> ```bash
|
||||
> $ docker compose -f docker-compose.yml up -d
|
||||
> ```
|
||||
|
||||
### تبديل محرك المستندات من Elasticsearch إلى Infinity
|
||||
|
||||
RAGFlow يستخدم Elasticsearch بشكل افتراضي لتخزين النص الكامل والمتجهات. للتبديل إلى [Infinity](https://github.com/infiniflow/infinity/)، اتبع الخطوات التالية:
|
||||
|
||||
1. إيقاف كافة الحاويات قيد التشغيل:
|
||||
|
||||
```bash
|
||||
$ docker compose -f docker/docker-compose.yml down -v
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> `-v` سوف يحذف docker وحدات تخزين الحاوية، وسيتم مسح البيانات الموجودة.
|
||||
|
||||
2. اضبط `DOC_ENGINE` في **docker/.env** على `infinity`.
|
||||
3. ابدأ الحاويات:
|
||||
|
||||
```bash
|
||||
$ docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> التبديل إلى Infinity على جهاز Linux/arm64 غير مدعوم رسميًا بعد.
|
||||
|
||||
## 🔧 أنشئ صورة Docker
|
||||
|
||||
يبلغ حجم هذه الصورة حوالي 2 غيغابايت وتعتمد على خدمات LLM وembedding الخارجية.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
|
||||
```
|
||||
|
||||
أو إذا كنت خلف وكيل، فيمكنك تمرير وسيطات الوكيل:
|
||||
|
||||
```bash
|
||||
docker build --platform linux/amd64 \
|
||||
--build-arg http_proxy=http://YOUR_PROXY:PORT \
|
||||
--build-arg https_proxy=http://YOUR_PROXY:PORT \
|
||||
-f Dockerfile -t infiniflow/ragflow:nightly .
|
||||
```
|
||||
|
||||
## 🔨 إطلاق الخدمة من المصدر للتطوير
|
||||
|
||||
1. قم بتثبيت `uv` و`pre-commit`، أو قم بتخطي هذه الخطوة إذا كانا مثبتين بالفعل:
|
||||
|
||||
```bash
|
||||
pipx install uv pre-commit
|
||||
```
|
||||
2. استنساخ الكود المصدري وتثبيت تبعيات بايثون:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/infiniflow/ragflow.git
|
||||
cd ragflow/
|
||||
uv sync --python 3.12 # install RAGFlow dependent python modules
|
||||
uv run download_deps.py
|
||||
pre-commit install
|
||||
```
|
||||
3. قم بتشغيل الخدمات التابعة (MinIO وElasticsearch وRedis وMySQL) باستخدام Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose-base.yml up -d
|
||||
```
|
||||
|
||||
أضف السطر التالي إلى `/etc/hosts` لحل كافة المضيفين المحددين في **docker/.env** إلى `127.0.0.1`:
|
||||
|
||||
```
|
||||
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
|
||||
```
|
||||
4. إذا لم تتمكن من الوصول إلى HuggingFace، فقم بتعيين متغير البيئة `HF_ENDPOINT` لاستخدام موقع مرآة:
|
||||
|
||||
```bash
|
||||
export HF_ENDPOINT=https://hf-mirror.com
|
||||
```
|
||||
5. إذا كان نظام التشغيل لديك لا يحتوي على jemalloc، فيرجى تثبيته على النحو التالي:
|
||||
|
||||
```bash
|
||||
# Ubuntu
|
||||
sudo apt-get install libjemalloc-dev
|
||||
# CentOS
|
||||
sudo yum install jemalloc
|
||||
# OpenSUSE
|
||||
sudo zypper install jemalloc
|
||||
# macOS
|
||||
sudo brew install jemalloc
|
||||
```
|
||||
6. إطلاق الخدمة الخلفية:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
export PYTHONPATH=$(pwd)
|
||||
bash docker/launch_backend_service.sh
|
||||
```
|
||||
7. تثبيت تبعيات الواجهة الأمامية:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm install
|
||||
```
|
||||
8. إطلاق خدمة الواجهة الأمامية:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
_النتيجة التالية تؤكد الإطلاق الناجح للنظام:_
|
||||
|
||||

|
||||
9. أوقف خدمة الواجهة الأمامية والخلفية RAGFlow بعد اكتمال التطوير:
|
||||
|
||||
```bash
|
||||
pkill -f "ragflow_server.py|task_executor.py"
|
||||
```
|
||||
|
||||
## 📚 التوثيق
|
||||
|
||||
- [البدء السريع](https://ragflow.io/docs/dev/)
|
||||
- [التكوين](https://ragflow.io/docs/dev/configurations)
|
||||
- [ملاحظات الإصدار](https://ragflow.io/docs/dev/release_notes)
|
||||
- [أدلة المستخدم](https://ragflow.io/docs/dev/category/guides)
|
||||
- [أدلة المطورين](https://ragflow.io/docs/dev/category/developers)
|
||||
- [المراجع](https://ragflow.io/docs/dev/category/references)
|
||||
- [الأسئلة الشائعة](https://ragflow.io/docs/dev/faq)
|
||||
|
||||
## 📜 Roadmap
|
||||
|
||||
راجع [RAGFlow Roadmap 2026](https://github.com/infiniflow/ragflow/issues/12241)
|
||||
|
||||
## 🏄 المجتمع
|
||||
|
||||
- [Discord](https://discord.gg/NjYzJD3GM3)
|
||||
- [Twitter](https://twitter.com/infiniflowai)
|
||||
- [مناقشات جيثب](https://github.com/orgs/infiniflow/discussions)
|
||||
|
||||
## 🙌 المساهمة
|
||||
|
||||
RAGFlow يزدهر من خلال التعاون مفتوح المصدر. وبهذه الروح، فإننا نحتضن المساهمات المتنوعة من المجتمع.
|
||||
إذا كنت ترغب في أن تكون جزءًا، فراجع [إرشادات المساهمة](https://ragflow.io/docs/dev/contributing) أولاً.
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DBEDFA"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DBEDFA"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DBEDFA"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="./README_id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-DFE0E5"></a>
|
||||
<a href="./README_pt_br.md"><img alt="Português(Brasil)" src="https://img.shields.io/badge/Português(Brasil)-DFE0E5"></a>
|
||||
<a href="./README_fr.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-DFE0E5"></a>
|
||||
<a href="./README_ar.md"><img alt="README in Arabic" src="https://img.shields.io/badge/Arabic-DFE0E5"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Toaster as Sonner } from '@/components/ui/sonner';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { configResponsive } from 'ahooks';
|
||||
import { App, ConfigProvider, ConfigProviderProps, theme } from 'antd';
|
||||
import pt_BR from 'antd/lib/locale/pt_BR';
|
||||
import arEG from 'antd/locale/ar_EG';
|
||||
import deDE from 'antd/locale/de_DE';
|
||||
import enUS from 'antd/locale/en_US';
|
||||
import ru_RU from 'antd/locale/ru_RU';
|
||||
@@ -54,6 +55,7 @@ const AntLanguageMap = {
|
||||
vi: vi_VN,
|
||||
'pt-BR': pt_BR,
|
||||
de: deDE,
|
||||
ar: arEG,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@@ -85,6 +87,12 @@ function Root({ children }: React.PropsWithChildren) {
|
||||
const { theme: themeragflow } = useTheme();
|
||||
const getLocale = (lng: string) =>
|
||||
AntLanguageMap[lng as keyof typeof AntLanguageMap] ?? enUS;
|
||||
const updateDocumentLocale = (lng: string) => {
|
||||
document.documentElement.lang = lng;
|
||||
document.documentElement.dir = lng.toLowerCase().startsWith('ar')
|
||||
? 'rtl'
|
||||
: 'ltr';
|
||||
};
|
||||
|
||||
const [locale, setLocal] = useState<Locale>(getLocale(storage.getLanguage()));
|
||||
|
||||
@@ -92,9 +100,10 @@ function Root({ children }: React.PropsWithChildren) {
|
||||
const handleLanguageChanged = (lng: string) => {
|
||||
storage.setLanguage(lng);
|
||||
setLocal(getLocale(lng));
|
||||
document.documentElement.lang = lng;
|
||||
updateDocumentLocale(lng);
|
||||
};
|
||||
|
||||
updateDocumentLocale(storage.getLanguage() || i18n.language || 'en');
|
||||
i18n.on('languageChanged', handleLanguageChanged);
|
||||
|
||||
return () => {
|
||||
@@ -130,7 +139,7 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
|
||||
useEffect(() => {
|
||||
const lng = storage.getLanguage();
|
||||
if (lng) {
|
||||
i18n.changeLanguage(lng);
|
||||
void changeLanguageAsync(lng);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { t } from 'i18next';
|
||||
import { HomeIcon } from '../svg-icon';
|
||||
|
||||
export enum EmptyType {
|
||||
@@ -17,27 +16,27 @@ export enum EmptyCardType {
|
||||
export const EmptyCardData = {
|
||||
[EmptyCardType.Agent]: {
|
||||
icon: <HomeIcon name="agents" width={'24'} />,
|
||||
title: t('empty.agentTitle'),
|
||||
notFound: t('empty.notFoundAgent'),
|
||||
titleKey: 'empty.agentTitle',
|
||||
notFoundKey: 'empty.notFoundAgent',
|
||||
},
|
||||
[EmptyCardType.Dataset]: {
|
||||
icon: <HomeIcon name="datasets" width={'24'} />,
|
||||
title: t('empty.datasetTitle'),
|
||||
notFound: t('empty.notFoundDataset'),
|
||||
titleKey: 'empty.datasetTitle',
|
||||
notFoundKey: 'empty.notFoundDataset',
|
||||
},
|
||||
[EmptyCardType.Chat]: {
|
||||
icon: <HomeIcon name="chats" width={'24'} />,
|
||||
title: t('empty.chatTitle'),
|
||||
notFound: t('empty.notFoundChat'),
|
||||
titleKey: 'empty.chatTitle',
|
||||
notFoundKey: 'empty.notFoundChat',
|
||||
},
|
||||
[EmptyCardType.Search]: {
|
||||
icon: <HomeIcon name="searches" width={'24'} />,
|
||||
title: t('empty.searchTitle'),
|
||||
notFound: t('empty.notFoundSearch'),
|
||||
titleKey: 'empty.searchTitle',
|
||||
notFoundKey: 'empty.notFoundSearch',
|
||||
},
|
||||
[EmptyCardType.Memory]: {
|
||||
icon: <HomeIcon name="memory" width={'24'} />,
|
||||
title: t('empty.memoryTitle'),
|
||||
notFound: t('empty.notFoundMemory'),
|
||||
titleKey: 'empty.memoryTitle',
|
||||
notFoundKey: 'empty.notFoundMemory',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIsDarkTheme } from '../theme-provider';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SvgIcon from '../svg-icon';
|
||||
import { EmptyCardData, EmptyCardType, EmptyType } from './constant';
|
||||
import { EmptyCardProps, EmptyProps } from './interface';
|
||||
@@ -82,8 +83,13 @@ export const EmptyAppCard = (props: {
|
||||
testId?: string;
|
||||
}) => {
|
||||
const { type, showIcon, className, isSearch, children, testId } = props;
|
||||
const { t } = useTranslation();
|
||||
let defaultClass = '';
|
||||
let style = {};
|
||||
const cardData = EmptyCardData[type];
|
||||
const title = t(cardData.titleKey);
|
||||
const notFound = t(cardData.notFoundKey);
|
||||
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
style = { width: '256px' };
|
||||
@@ -100,10 +106,8 @@ export const EmptyAppCard = (props: {
|
||||
return (
|
||||
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
|
||||
<EmptyCard
|
||||
icon={showIcon ? EmptyCardData[type].icon : undefined}
|
||||
title={
|
||||
isSearch ? EmptyCardData[type].notFound : EmptyCardData[type].title
|
||||
}
|
||||
icon={showIcon ? cardData.icon : undefined}
|
||||
title={isSearch ? notFound : title}
|
||||
className={cn('cursor-pointer ', className)}
|
||||
style={style}
|
||||
// description={EmptyCardData[type].description}
|
||||
|
||||
@@ -28,13 +28,13 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
inset && 'ps-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
<ChevronRight className="ms-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
@@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
inset && 'ps-8',
|
||||
justifyBetween && 'flex justify-between',
|
||||
className,
|
||||
)}
|
||||
@@ -101,13 +101,13 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute start-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
@@ -125,12 +125,12 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute start-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
@@ -150,7 +150,7 @@ const DropdownMenuLabel = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
inset && 'ps-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -176,7 +176,7 @@ const DropdownMenuShortcut = ({
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
className={cn('ms-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -80,8 +80,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
paddingLeft: !!prefix && prefixWidth ? `${prefixWidth}px` : '',
|
||||
paddingRight: isPasswordInput
|
||||
paddingInlineStart:
|
||||
!!prefix && prefixWidth ? `${prefixWidth}px` : '',
|
||||
paddingInlineEnd: isPasswordInput
|
||||
? '40px'
|
||||
: !!suffix
|
||||
? `${suffixWidth}px`
|
||||
@@ -109,7 +110,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
{prefix && (
|
||||
<span
|
||||
ref={prefixRef}
|
||||
className="absolute left-0 top-[50%] translate-y-[-50%]"
|
||||
className="absolute start-0 top-[50%] translate-y-[-50%]"
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
@@ -118,8 +119,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
{suffix && (
|
||||
<span
|
||||
ref={suffixRef}
|
||||
className={cn('absolute right-0 top-[50%] translate-y-[-50%]', {
|
||||
'right-14': isPasswordInput,
|
||||
className={cn('absolute end-0 top-[50%] translate-y-[-50%]', {
|
||||
'end-14': isPasswordInput,
|
||||
})}
|
||||
>
|
||||
{suffix}
|
||||
@@ -130,7 +131,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
type="button"
|
||||
className="
|
||||
p-2 text-text-secondary
|
||||
absolute border-0 right-1 top-[50%] translate-y-[-50%]
|
||||
absolute border-0 end-1 top-[50%] translate-y-[-50%]
|
||||
dark:peer-autofill/input:text-text-secondary-inverse
|
||||
dark:peer-autofill/input:hover:text-text-primary-inverse
|
||||
dark:peer-autofill/input:focus-visible:text-text-primary-inverse
|
||||
@@ -165,7 +166,7 @@ const SearchInput = (props: InputProps) => {
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
{...props}
|
||||
prefix={<Search className="ml-2 mr-1 size-[1em]" />}
|
||||
prefix={<Search className="ms-2 me-1 size-[1em]" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -164,7 +164,7 @@ const Modal: ModalType = ({
|
||||
)}
|
||||
>
|
||||
{confirmLoading && (
|
||||
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader className="inline-block me-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{okText ?? t('modal.okText')}
|
||||
</button>
|
||||
@@ -253,7 +253,7 @@ const Modal: ModalType = ({
|
||||
<DialogPrimitive.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex absolute right-5 top-5 h-7 w-7 items-center justify-center text-text-secondary rounded-full hover:text-text-primary focus-visible:outline-none"
|
||||
className="flex absolute end-5 top-5 h-7 w-7 items-center justify-center text-text-secondary rounded-full hover:text-text-primary focus-visible:outline-none"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{closeIcon}
|
||||
|
||||
@@ -129,7 +129,7 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
className={cn('py-1.5 ps-8 pe-2 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -142,13 +142,13 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-2 text-sm outline-none text-text-secondary',
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-2 pe-2 text-sm outline-none text-text-secondary',
|
||||
'focus:bg-border-button focus:text-text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute end-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
|
||||
@@ -50,7 +50,8 @@ const sheetVariants = cva(
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
closeIcon?: boolean;
|
||||
}
|
||||
@@ -72,7 +73,7 @@ const SheetContent = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
{closeIcon ? (
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary ">
|
||||
<SheetPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary ">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const LanguageList = [
|
||||
'Chinese',
|
||||
'Traditional Chinese',
|
||||
'Russian',
|
||||
'Indonesia',
|
||||
'Indonesian',
|
||||
'Spanish',
|
||||
'Vietnamese',
|
||||
'Japanese',
|
||||
@@ -55,12 +55,14 @@ export const LanguageList = [
|
||||
'French',
|
||||
'Italian',
|
||||
'Bulgarian',
|
||||
'Arabic',
|
||||
];
|
||||
export const LanguageMap = {
|
||||
English: 'English',
|
||||
Chinese: '简体中文',
|
||||
'Traditional Chinese': '繁體中文',
|
||||
Russian: 'Русский',
|
||||
Indonesian: 'Bahasa Indonesia',
|
||||
Indonesia: 'Indonesia',
|
||||
Spanish: 'Español',
|
||||
Vietnamese: 'Tiếng việt',
|
||||
@@ -70,6 +72,7 @@ export const LanguageMap = {
|
||||
French: 'Français',
|
||||
Italian: 'Italiano',
|
||||
Bulgarian: 'Български',
|
||||
Arabic: 'العربية',
|
||||
};
|
||||
|
||||
export enum LanguageAbbreviation {
|
||||
@@ -86,6 +89,7 @@ export enum LanguageAbbreviation {
|
||||
Fr = 'fr',
|
||||
It = 'it',
|
||||
Bg = 'bg',
|
||||
Ar = 'ar',
|
||||
}
|
||||
|
||||
export const LanguageAbbreviationMap = {
|
||||
@@ -93,7 +97,7 @@ export const LanguageAbbreviationMap = {
|
||||
[LanguageAbbreviation.Zh]: '简体中文',
|
||||
[LanguageAbbreviation.ZhTraditional]: '繁體中文',
|
||||
[LanguageAbbreviation.Ru]: 'Русский',
|
||||
[LanguageAbbreviation.Id]: 'Indonesia',
|
||||
[LanguageAbbreviation.Id]: 'Bahasa Indonesia',
|
||||
[LanguageAbbreviation.Es]: 'Español',
|
||||
[LanguageAbbreviation.Vi]: 'Tiếng việt',
|
||||
[LanguageAbbreviation.Ja]: '日本語',
|
||||
@@ -102,6 +106,7 @@ export const LanguageAbbreviationMap = {
|
||||
[LanguageAbbreviation.Fr]: 'Français',
|
||||
[LanguageAbbreviation.It]: 'Italiano',
|
||||
[LanguageAbbreviation.Bg]: 'Български',
|
||||
[LanguageAbbreviation.Ar]: 'العربية',
|
||||
};
|
||||
|
||||
export const LanguageTranslationMap = {
|
||||
@@ -110,11 +115,14 @@ export const LanguageTranslationMap = {
|
||||
'Traditional Chinese': 'zh-TRADITIONAL',
|
||||
Russian: 'ru',
|
||||
Indonesian: 'id',
|
||||
Indonesia: 'id',
|
||||
Spanish: 'es',
|
||||
Vietnamese: 'vi',
|
||||
Japanese: 'ja',
|
||||
Korean: 'ko',
|
||||
'Portuguese BR': 'pt-br',
|
||||
'Portuguese BR': 'pt-BR',
|
||||
'pt-br': 'pt-BR',
|
||||
'pt-BR': 'pt-BR',
|
||||
German: 'de',
|
||||
French: 'fr',
|
||||
Italian: 'it',
|
||||
@@ -126,6 +134,7 @@ export const LanguageTranslationMap = {
|
||||
Hindi: 'hi',
|
||||
Ukrainian: 'uk',
|
||||
Bulgarian: 'bg',
|
||||
Arabic: 'ar',
|
||||
};
|
||||
|
||||
export enum FileMimeType {
|
||||
|
||||
@@ -60,7 +60,9 @@ export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
|
||||
LanguageTranslationMap[
|
||||
data.data.language as keyof typeof LanguageTranslationMap
|
||||
];
|
||||
await changeLanguageAsync(targetLng);
|
||||
if (targetLng) {
|
||||
await changeLanguageAsync(targetLng);
|
||||
}
|
||||
}
|
||||
return data?.data ?? {};
|
||||
},
|
||||
@@ -118,42 +120,58 @@ export const useFetchTenantInfo = (
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
const DEFAULT_PARSERS = [
|
||||
{ value: 'naive', label: 'General' },
|
||||
{ value: 'qa', label: 'Q&A' },
|
||||
{ value: 'resume', label: 'Resume' },
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
{ value: 'table', label: 'Table' },
|
||||
{ value: 'paper', label: 'Paper' },
|
||||
{ value: 'book', label: 'Book' },
|
||||
{ value: 'laws', label: 'Laws' },
|
||||
{ value: 'presentation', label: 'Presentation' },
|
||||
{ value: 'picture', label: 'Picture' },
|
||||
{ value: 'one', label: 'One' },
|
||||
{ value: 'audio', label: 'Audio' },
|
||||
{ value: 'email', label: 'Email' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
];
|
||||
|
||||
export const useSelectParserList = (): Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
}> => {
|
||||
const { data: tenantInfo } = useFetchTenantInfo(true);
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const defaultParsers = useMemo(
|
||||
() => [
|
||||
{ value: 'naive', label: t('knowledgeConfiguration.parserLabel.naive') },
|
||||
{ value: 'qa', label: t('knowledgeConfiguration.parserLabel.qa') },
|
||||
{
|
||||
value: 'resume',
|
||||
label: t('knowledgeConfiguration.parserLabel.resume'),
|
||||
},
|
||||
{
|
||||
value: 'manual',
|
||||
label: t('knowledgeConfiguration.parserLabel.manual'),
|
||||
},
|
||||
{ value: 'table', label: t('knowledgeConfiguration.parserLabel.table') },
|
||||
{ value: 'paper', label: t('knowledgeConfiguration.parserLabel.paper') },
|
||||
{ value: 'book', label: t('knowledgeConfiguration.parserLabel.book') },
|
||||
{ value: 'laws', label: t('knowledgeConfiguration.parserLabel.laws') },
|
||||
{
|
||||
value: 'presentation',
|
||||
label: t('knowledgeConfiguration.parserLabel.presentation'),
|
||||
},
|
||||
{
|
||||
value: 'picture',
|
||||
label: t('knowledgeConfiguration.parserLabel.picture'),
|
||||
},
|
||||
{ value: 'one', label: t('knowledgeConfiguration.parserLabel.one') },
|
||||
{ value: 'audio', label: t('knowledgeConfiguration.parserLabel.audio') },
|
||||
{ value: 'email', label: t('knowledgeConfiguration.parserLabel.email') },
|
||||
{ value: 'tag', label: t('knowledgeConfiguration.parserLabel.tag') },
|
||||
],
|
||||
[i18n.language, t],
|
||||
);
|
||||
|
||||
const parserList = useMemo(() => {
|
||||
const parserArray: Array<string> = tenantInfo?.parser_ids?.split(',') ?? [];
|
||||
const filteredArray = parserArray.filter((x) => x.trim() !== '');
|
||||
|
||||
if (filteredArray.length === 0) {
|
||||
return DEFAULT_PARSERS;
|
||||
return defaultParsers;
|
||||
}
|
||||
|
||||
return filteredArray.map((x) => {
|
||||
const arr = x.split(':');
|
||||
return { value: arr[0], label: arr[1] };
|
||||
});
|
||||
}, [tenantInfo]);
|
||||
}, [tenantInfo, defaultParsers]);
|
||||
|
||||
return parserList;
|
||||
};
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Add Bulgarian Language Support
|
||||
|
||||
**Repository:** https://github.com/trifonnt/ragflow
|
||||
|
||||
## Overview
|
||||
|
||||
Added Bulgarian (`bg` / `Български`) as the 13th supported UI language in RAGFlow.
|
||||
|
||||
## Changed Files
|
||||
|
||||
### `web/src/constants/common.ts`
|
||||
|
||||
Registered Bulgarian in all 5 language data structures:
|
||||
|
||||
- **`LanguageList`** — Added `'Bulgarian'` to the array of available languages
|
||||
- **`LanguageMap`** — Added `Bulgarian: 'Български'` mapping English name to native name
|
||||
- **`LanguageAbbreviation`** enum — Added `Bg = 'bg'` using ISO 639-1 code
|
||||
- **`LanguageAbbreviationMap`** — Added `[LanguageAbbreviation.Bg]: 'Български'` for the language selector display
|
||||
- **`LanguageTranslationMap`** — Added `Bulgarian: 'bg'` for language code resolution
|
||||
|
||||
### `web/src/locales/config.ts`
|
||||
|
||||
Added a lazy-loading dynamic import entry for the Bulgarian locale file:
|
||||
|
||||
```typescript
|
||||
[LanguageAbbreviation.Bg]: () => import('./bg'),
|
||||
```
|
||||
|
||||
This ensures the Bulgarian translation bundle is only loaded on demand when a user selects it.
|
||||
|
||||
### `web/src/locales/bg.ts` (new file)
|
||||
|
||||
Created the full Bulgarian translation file containing all 26 sections and 2001 translation keys, matching the English source (`en.ts`) exactly:
|
||||
|
||||
`common`, `login`, `header`, `memories`, `memory`, `knowledgeList`, `knowledgeDetails`, `knowledgeConfiguration`, `chunk`, `chat`, `setting`, `message`, `fileManager`, `flow`, `llmTools`, `modal`, `mcp`, `search`, `language`, `pagination`, `dataflowParser`, `datasetOverview`, `deleteModal`, `empty`, `admin`, `explore`
|
||||
|
||||
All interpolation placeholders (`{{variable}}`), HTML tags, and technical terms (model names, URLs, API references) are preserved as-is.
|
||||
|
||||
### `deepdoc/parser/mineru_parser.py`
|
||||
|
||||
Added Bulgarian to the `LANGUAGE_TO_MINERU_MAP` dictionary for OCR/PDF parser language support:
|
||||
|
||||
```python
|
||||
'Bulgarian': 'cyrillic',
|
||||
```
|
||||
|
||||
Bulgarian uses the Cyrillic script, so the `'cyrillic'` MinerU language code is used.
|
||||
|
||||
## How It Works
|
||||
|
||||
- The language selector in the header automatically picks up the new entry from `LanguageList` and `LanguageMap`
|
||||
- When a user selects "Български", `changeLanguageAsync('bg')` lazy-loads `bg.ts` and switches the UI
|
||||
- The user's preference is saved to the database and localStorage for persistence across sessions
|
||||
- `supportedLngs` in i18next is derived from `Object.values(LanguageAbbreviation)`, so adding `Bg` to the enum automatically registers it
|
||||
2527
web/src/locales/ar.ts
Normal file
2527
web/src/locales/ar.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,15 @@ const languageImports: Record<string, () => Promise<{ default: any }>> = {
|
||||
[LanguageAbbreviation.Fr]: () => import('./fr'),
|
||||
[LanguageAbbreviation.It]: () => import('./it'),
|
||||
[LanguageAbbreviation.Bg]: () => import('./bg'),
|
||||
[LanguageAbbreviation.Ar]: () => import('./ar'),
|
||||
};
|
||||
|
||||
const languageAliases: Record<string, string> = {
|
||||
'pt-br': LanguageAbbreviation.PtBr,
|
||||
};
|
||||
|
||||
const normalizeLanguageCode = (lng: string): string => {
|
||||
return languageAliases[lng] ?? lng;
|
||||
};
|
||||
|
||||
const enFlattened = flattenObject(translation_en);
|
||||
@@ -49,11 +58,13 @@ i18n
|
||||
});
|
||||
|
||||
export const loadLanguageAsync = async (lng: string): Promise<void> => {
|
||||
if (i18n.hasResourceBundle(lng, 'translation')) {
|
||||
const normalizedLng = normalizeLanguageCode(lng);
|
||||
|
||||
if (i18n.hasResourceBundle(normalizedLng, 'translation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importFn = languageImports[lng];
|
||||
const importFn = languageImports[normalizedLng];
|
||||
if (!importFn) {
|
||||
console.warn(`Language ${lng} is not supported for lazy loading`);
|
||||
return;
|
||||
@@ -62,7 +73,7 @@ export const loadLanguageAsync = async (lng: string): Promise<void> => {
|
||||
try {
|
||||
const module = await importFn();
|
||||
const translationData = module.default?.translation || module.default;
|
||||
i18n.addResourceBundle(lng, 'translation', translationData);
|
||||
i18n.addResourceBundle(normalizedLng, 'translation', translationData);
|
||||
|
||||
const flattened = flattenObject({ translation: translationData });
|
||||
translationTable.push(flattened);
|
||||
@@ -72,16 +83,22 @@ export const loadLanguageAsync = async (lng: string): Promise<void> => {
|
||||
};
|
||||
|
||||
export const changeLanguageAsync = async (lng: string): Promise<void> => {
|
||||
if (lng !== 'en' && !i18n.hasResourceBundle(lng, 'translation')) {
|
||||
await loadLanguageAsync(lng);
|
||||
const normalizedLng = normalizeLanguageCode(lng);
|
||||
if (
|
||||
normalizedLng !== LanguageAbbreviation.En &&
|
||||
!i18n.hasResourceBundle(normalizedLng, 'translation')
|
||||
) {
|
||||
await loadLanguageAsync(normalizedLng);
|
||||
}
|
||||
await i18n.changeLanguage(lng);
|
||||
await i18n.changeLanguage(normalizedLng);
|
||||
};
|
||||
|
||||
export const initLanguage = async (): Promise<void> => {
|
||||
const currentLng = i18n.language || localStorage.getItem('lng') || 'en';
|
||||
const currentLng = normalizeLanguageCode(
|
||||
i18n.language || localStorage.getItem('lng') || LanguageAbbreviation.En,
|
||||
);
|
||||
|
||||
if (currentLng !== 'en' && languageImports[currentLng]) {
|
||||
if (currentLng !== LanguageAbbreviation.En && languageImports[currentLng]) {
|
||||
await loadLanguageAsync(currentLng);
|
||||
await i18n.changeLanguage(currentLng);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,16 @@ export default {
|
||||
chinese: 'Simplified Chinese',
|
||||
traditionalChinese: 'Traditional Chinese',
|
||||
russian: 'Russian',
|
||||
indonesian: 'Indonesian',
|
||||
indonesia: 'Indonesian',
|
||||
spanish: 'Spanish',
|
||||
vietnamese: 'Vietnamese',
|
||||
japanese: 'Japanese',
|
||||
german: 'German',
|
||||
french: 'French',
|
||||
italian: 'Italian',
|
||||
bulgarian: 'Bulgarian',
|
||||
arabic: 'Arabic',
|
||||
language: 'Language',
|
||||
languageMessage: 'Please input your language!',
|
||||
languagePlaceholder: 'select your language',
|
||||
@@ -529,6 +538,22 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
|
||||
portugueseBr: 'Portuguese (Brazil)',
|
||||
embeddingModelPlaceholder: 'Please select a embedding model.',
|
||||
chunkMethodPlaceholder: 'Please select a chunking method.',
|
||||
parserLabel: {
|
||||
naive: 'General',
|
||||
qa: 'Q&A',
|
||||
resume: 'Resume',
|
||||
manual: 'Manual',
|
||||
table: 'Table',
|
||||
paper: 'Paper',
|
||||
book: 'Book',
|
||||
laws: 'Laws',
|
||||
presentation: 'Presentation',
|
||||
picture: 'Picture',
|
||||
one: 'One',
|
||||
audio: 'Audio',
|
||||
email: 'Email',
|
||||
tag: 'Tag',
|
||||
},
|
||||
save: 'Save',
|
||||
me: 'Only me',
|
||||
team: 'Team',
|
||||
|
||||
@@ -426,7 +426,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
|
||||
<TableHead>
|
||||
{t('knowledgeDetails.metadata.description')}
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<TableHead className="[text-align:end]">
|
||||
{t('knowledgeDetails.metadata.action')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -451,7 +451,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
|
||||
{row.description}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="[text-align:end]">
|
||||
<Switch
|
||||
checked={builtInSelectionKeys.has(row.field)}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -539,7 +539,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
|
||||
)}
|
||||
</div>
|
||||
{metadataType === MetadataType.Manage && (
|
||||
<div className=" absolute bottom-6 left-5 text-text-secondary text-sm">
|
||||
<div className=" absolute bottom-6 start-5 text-text-secondary text-sm">
|
||||
{t('knowledgeDetails.metadata.toMetadataSettingTip')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function Dataset() {
|
||||
</div>
|
||||
<section className="p-5 min-w-[880px]">
|
||||
<ListFilterBar
|
||||
title="Dataset"
|
||||
title={t('header.dataset')}
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
value={filterValue}
|
||||
@@ -236,7 +236,7 @@ export default function Dataset() {
|
||||
hideModal={hideCreateModal}
|
||||
onOk={onCreateOk}
|
||||
loading={createLoading}
|
||||
title={'File Name'}
|
||||
title={t('knowledgeDetails.fileName')}
|
||||
></RenameDialog>
|
||||
)}
|
||||
{manageMetadataVisible && (
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function MemoryMessage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<ListFilterBar
|
||||
title="Dataset"
|
||||
title={t('header.dataset')}
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
// showFilter={false}
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function SearchPage() {
|
||||
</div>
|
||||
{openSetting && (
|
||||
<SearchSetting
|
||||
className="mt-20 mr-2"
|
||||
className="mt-20 me-2"
|
||||
open={openSetting}
|
||||
setOpen={setOpenSetting}
|
||||
data={SearchData as ISearchAppDetailProps}
|
||||
@@ -128,7 +128,7 @@ export default function SearchPage() {
|
||||
// ></EmbedDialog>
|
||||
}
|
||||
</div>
|
||||
<div className="absolute right-5 top-4 ">
|
||||
<div className="absolute end-5 top-4 ">
|
||||
<Button
|
||||
className="bg-text-primary text-bg-base border-b-accent-primary border-b-2"
|
||||
onClick={() => {
|
||||
@@ -145,7 +145,7 @@ export default function SearchPage() {
|
||||
</Button>
|
||||
</div>
|
||||
{!isSearching && (
|
||||
<div className="absolute left-5 bottom-12 ">
|
||||
<div className="absolute start-5 bottom-12 ">
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="bg-bg-card"
|
||||
|
||||
@@ -64,11 +64,16 @@ const SourceDetailPage = () => {
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
render: (fieldProps: FormFieldConfig) => (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-[58px] text-text-secondary italic ">
|
||||
{t('setting.minutes')}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input
|
||||
{...fieldProps}
|
||||
type={FormFieldType.Number}
|
||||
suffix={
|
||||
<span className="text-text-secondary italic">
|
||||
{t('setting.minutes')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button hover:bg-border-button hover:text-text-primary"
|
||||
@@ -94,11 +99,14 @@ const SourceDetailPage = () => {
|
||||
hidden: true,
|
||||
render: (fieldProps: FormFieldConfig) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-6 text-text-secondary italic ">
|
||||
hours
|
||||
</span>
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input
|
||||
{...fieldProps}
|
||||
type={FormFieldType.Number}
|
||||
suffix={
|
||||
<span className="text-text-secondary italic">hours</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -109,11 +117,16 @@ const SourceDetailPage = () => {
|
||||
type: FormFieldType.Number,
|
||||
required: false,
|
||||
render: (fieldProps: FormFieldConfig) => (
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input {...fieldProps} type={FormFieldType.Number} />
|
||||
<span className="absolute right-0 -translate-x-6 text-text-secondary italic ">
|
||||
{t('setting.seconds')}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 w-full relative">
|
||||
<Input
|
||||
{...fieldProps}
|
||||
type={FormFieldType.Number}
|
||||
suffix={
|
||||
<span className="text-text-secondary italic">
|
||||
{t('setting.seconds')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -120,11 +120,11 @@ const TranslationTable: React.FC<TranslationTableProps> = ({
|
||||
const sortOrder = colState?.sortOrder;
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return <ArrowUp className="ml-1 h-4 w-4" />;
|
||||
return <ArrowUp className="ms-1 h-4 w-4" />;
|
||||
} else if (sortOrder === 'desc') {
|
||||
return <ArrowDown className="ml-1 h-4 w-4" />;
|
||||
return <ArrowDown className="ms-1 h-4 w-4" />;
|
||||
} else {
|
||||
return <ArrowUpDown className="ml-1 h-4 w-4" />;
|
||||
return <ArrowUpDown className="ms-1 h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,7 +140,7 @@ const TranslationTable: React.FC<TranslationTableProps> = ({
|
||||
<TableHeader className="bg-bg-title">
|
||||
<TableRow className="hover:bg-bg-title">
|
||||
<TableHead
|
||||
className="h-12 px-4 cursor-pointer sticky left-0 bg-bg-title"
|
||||
className="h-12 px-4 cursor-pointer sticky start-0 bg-bg-title"
|
||||
onClick={() => handleSort('key')}
|
||||
>
|
||||
<div className="flex items-center min-w-[200px]">
|
||||
@@ -202,7 +202,7 @@ const TranslationTable: React.FC<TranslationTableProps> = ({
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((record) => (
|
||||
<TableRow key={record.key} className="hover:bg-bg-card">
|
||||
<TableCell className="p-4 font-medium sticky left-0 bg-bg-base hover:bg-bg-card">
|
||||
<TableCell className="p-4 font-medium sticky start-0 bg-bg-base hover:bg-bg-card">
|
||||
{record.key}
|
||||
</TableCell>
|
||||
{languages.map((lang) => (
|
||||
|
||||
@@ -19,7 +19,10 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
isAbsolute = true,
|
||||
params,
|
||||
}) => {
|
||||
const { t } = useTranslate('setting');
|
||||
const { t, i18n } = useTranslate('setting');
|
||||
const isArabic = (i18n.resolvedLanguage || i18n.language || '')
|
||||
.toLowerCase()
|
||||
.startsWith('ar');
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [verifyResult, setVerifyResult] = useState<VerifyResult | null>(null);
|
||||
const form = useFormContext();
|
||||
@@ -78,7 +81,7 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
className={cn(
|
||||
!isAbsolute || (verifyResult && verifyResult.isValid === false)
|
||||
? 'flex flex-col gap-5 w-full '
|
||||
: 'absolute left-6 bottom-6 z-[100]',
|
||||
: `absolute bottom-6 z-[100] ${isArabic ? 'right-6' : 'left-6'}`,
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
||||
Reference in New Issue
Block a user