feat(i18n,rtl): full Arabic localization + RTL sweep across all layouts
Some checks failed
CI / Frontend — lint + build + e2e (pull_request) Has been cancelled
CI / Backend — Odoo HttpCase (pull_request) Has been cancelled

Frontend
- i18n: install tailwindcss-rtl, Cairo font, RTL-aware direction in index.css.
- Language toggle: localize aria-label / menu label, persist choice, update
  document dir synchronously.
- Sidebar: add `side` prop so the drawer pins to the right in RTL; wire up
  AdminLmsLayout, RoleLayout (student/teacher) and AppSidebar to pass
  side = i18n.dir() === 'rtl' ? 'right' : 'left'.
- AdminLmsLayout: convert every nav item from hard-coded title to titleKey,
  translate group labels (incl. the collapsible Training), breadcrumbs,
  user menu (Profile / Settings / Logout), help button and toggle aria
  labels; replace physical mr-/right- utilities with logical me-/end-.
- AI components (AiTipBanner, AiInsightsPanel, AiAlertBanner, AiSearchBar,
  AiAssistantDrawer): apply dir="auto" at the container level, localize
  titles, loading / error / empty states.
- Dashboards (admin / student / teacher): wrap numeric values in <bdi>,
  localize dates via ar-EG, fix flex direction for KPI and assignment cards.
- UI primitives (breadcrumb, calendar, carousel, dropdown-menu, menubar,
  context-menu, pagination, sidebar): flip chevrons in RTL via a scoped
  CSS rule, swap pl-/pr-/ml-/mr- for ps-/pe-/ms-/me-.
- Add logical-direction helpers and bidirectional isolation classes.

Locales
- Expand en.ts and ar.ts with full `nav`, `sidebarGroup`, `breadcrumb`,
  `userMenu`, `chrome`, `ai`, and dashboard key sets; keep key parity.

API client
- `api-client.ts` reads the active language from localStorage/i18n and sends
  `Accept-Language` on every request so the backend can localize AI output.

Backend (encoach_ai)
- openai_service: add _LANGUAGE_NAMES, normalize_language, language-aware
  system prompt injection for every OpenAI call.
- coach_service + controllers (coach_controller, ai_controller): thread
  the requested language from headers / user locale down to OpenAIService.
- ai_feedback: fix latent registry error by pointing course_id at op.course
  instead of the non-existent encoach.course.

Other
- .gitignore: ignore runtime odoo logs and local caches.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-19 18:13:16 +04:00
parent 6712d1d551
commit e1f059069f
56 changed files with 1471 additions and 491 deletions

View File

@@ -24,6 +24,25 @@ def _get_json():
return {}
def _request_language():
"""Return the caller's UI language from ``Accept-Language``.
The frontend ``api-client`` forwards the active i18n language (e.g. ``ar``
or ``en``) via this header so AI-generated natural-language strings can
be returned in the same language as the UI chrome.
"""
try:
return request.httprequest.headers.get("Accept-Language", "") or ""
except Exception:
return ""
def _openai_for_request():
"""Construct an OpenAIService bound to the caller's UI language."""
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
return OpenAIService(request.env, language=_request_language())
class AIController(http.Controller):
"""Handles /api/ai/* endpoints consumed by frontend AI components."""
@@ -37,7 +56,7 @@ class AIController(http.Controller):
return _json_response({"answer": "", "suggestions": []})
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
result = ai.search_with_rag(query, context=body.get("context", ""))
return _json_response(result)
except Exception as e:
@@ -69,7 +88,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
result = ai.generate_insights(
body.get("data", {}),
insight_type=body.get("type", "general"),
@@ -85,7 +104,7 @@ class AIController(http.Controller):
def ai_alerts(self, **kw):
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
context = request.params.get("context", "dashboard")
result = ai.generate_insights(
{"context": context, "request": "alerts"},
@@ -103,7 +122,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
narrative = ai.generate_report_narrative(
body.get("report_type", "performance"),
body.get("data", {}),
@@ -119,7 +138,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
result = ai.batch_optimize(
body.get("items", []),
optimization_type=body.get("type", "schedule"),
@@ -135,7 +154,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
skill = body.get("skill", "writing")
if skill == "speaking":
result = ai.grade_speaking(
@@ -160,7 +179,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
result = ai.generate_content_dedup(
body.get("content_type", "reading_passage"),
body.get("brief", {}),
@@ -204,7 +223,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"You are an educational taxonomy expert. Suggest topics for the given domain and level. "
@@ -224,7 +243,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"Create a personalized learning plan. Return JSON: "
@@ -246,7 +265,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"Generate a course outline. Return JSON: {\"chapters\": "
@@ -264,7 +283,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"Generate detailed chapter content for a course. Return JSON: "
@@ -283,7 +302,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"Create an assessment rubric. Return JSON: {\"rubric\": "
@@ -350,7 +369,7 @@ class AIController(http.Controller):
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
if not ai.client:
raise RuntimeError("OpenAI not configured")
@@ -480,7 +499,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
has_ai = bool(ai.client)
except Exception:
ai, has_ai = None, False
@@ -1618,7 +1637,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
messages = [
{"role": "system", "content": (
"You are an educational materials expert. Suggest learning materials "
@@ -1639,7 +1658,7 @@ class AIController(http.Controller):
body = _get_json()
try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
ai = OpenAIService(request.env)
ai = OpenAIService(request.env, language=_request_language())
result = ai.generate_content(
body.get("content_type", "explanation"),
{"topic_id": topic_id, **body},

View File

@@ -23,12 +23,25 @@ def _get_json():
return {}
def _request_language():
"""Read the caller's UI language from the ``Accept-Language`` header.
The frontend ``api-client`` automatically attaches this header from the
active i18n language so AI-generated text can be localized. Falls back
to English if the header is missing or malformed.
"""
try:
return request.httprequest.headers.get("Accept-Language", "") or ""
except Exception:
return ""
class CoachController(http.Controller):
"""Handles /api/coach/* endpoints consumed by frontend AI coaching components."""
def _get_coach(self):
from odoo.addons.encoach_ai.services.coach_service import CoachService
return CoachService(request.env)
return CoachService(request.env, language=_request_language())
# ── POST /api/coach/chat — AiAssistantDrawer.tsx ──
@http.route("/api/coach/chat", type="http", auth="none", methods=["POST"], csrf=False)