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:
Idriss Sbaaoui
2026-03-02 19:10:11 +08:00
committed by GitHub
parent f8c91e8854
commit b0ace2c5d0
30 changed files with 3149 additions and 157 deletions

View File

@@ -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
View 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 | &approx;9 | ✔️ | إصدار مستقر |
| v0.21.1-slim | &approx;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
```
_النتيجة التالية تؤكد الإطلاق الناجح للنظام:_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
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) أولاً.

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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);
}
}, []);

View File

@@ -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',
},
};

View File

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

View File

@@ -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}
/>
);

View File

@@ -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]" />}
/>
);
};

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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',

View File

@@ -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>
)}

View File

@@ -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 && (

View File

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

View File

@@ -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"

View File

@@ -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>
),
},

View File

@@ -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) => (

View File

@@ -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">