From fbd58fa5a627fc5fbb3aedb7366c70b2e0f4fbb8 Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Sun, 19 Apr 2026 18:13:16 +0400 Subject: [PATCH] feat(i18n,rtl): full Arabic localization + RTL sweep across all layouts 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 , 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 --- index.html | 2 +- package-lock.json | 8 + package.json | 1 + src/components/AdminLmsLayout.tsx | 278 ++++++++-------- src/components/AppSidebar.tsx | 5 +- src/components/ErrorBoundary.tsx | 23 +- src/components/LanguageToggle.tsx | 11 +- src/components/NotificationDropdown.tsx | 35 +- src/components/RoleLayout.tsx | 78 +++-- src/components/StudentLayout.tsx | 43 +-- src/components/TeacherLayout.tsx | 33 +- src/components/ThemeToggle.tsx | 17 +- src/components/ai/AiAlertBanner.tsx | 20 +- src/components/ai/AiAssistantDrawer.tsx | 59 ++-- src/components/ai/AiInsightsPanel.tsx | 20 +- src/components/ai/AiSearchBar.tsx | 26 +- src/components/ai/AiTipBanner.tsx | 37 ++- src/components/student/ExamPopup.tsx | 29 +- src/components/ui/breadcrumb.tsx | 7 +- src/components/ui/calendar.tsx | 4 +- src/components/ui/carousel.tsx | 4 +- src/components/ui/context-menu.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/menubar.tsx | 2 +- src/components/ui/pagination.tsx | 8 +- src/components/ui/sidebar.tsx | 7 +- src/i18n/locales/ar.ts | 279 +++++++++++++++- src/i18n/locales/en.ts | 298 +++++++++++++++++- src/index.css | 100 ++++++ src/lib/api-client.ts | 20 ++ src/pages/ExamStructuresPage.tsx | 22 +- src/pages/ForgotPassword.tsx | 2 +- src/pages/Login.tsx | 51 ++- src/pages/NotFound.tsx | 8 +- src/pages/admin/AdminBatchDetail.tsx | 2 +- src/pages/admin/AdminLmsDashboard.tsx | 96 +++--- src/pages/admin/AdmissionDetail.tsx | 2 +- src/pages/admin/ApprovalWorkflowConfig.tsx | 2 +- src/pages/admin/ExamReviewDetail.tsx | 2 +- src/pages/student/DiagnosticTest.tsx | 2 +- src/pages/student/LearningPlan.tsx | 2 +- src/pages/student/ProficiencyProfile.tsx | 2 +- src/pages/student/StudentCourseDetail.tsx | 2 +- src/pages/student/StudentDashboard.tsx | 72 +++-- src/pages/student/StudentDiscussionBoard.tsx | 2 +- src/pages/student/SubjectSelection.tsx | 4 +- src/pages/teacher/TeacherAssignmentDetail.tsx | 2 +- src/pages/teacher/TeacherDashboard.tsx | 61 ++-- src/pages/teacher/TeacherDiscussionBoard.tsx | 2 +- tailwind.config.ts | 9 +- 50 files changed, 1335 insertions(+), 470 deletions(-) diff --git a/index.html b/index.html index ea5a338..173935e 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ - +
diff --git a/package-lock.json b/package-lock.json index 497f4fa..0bd05df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "lovable-tagger": "^1.1.13", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", + "tailwindcss-rtl": "^0.9.0", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", "vite": "^5.4.19", @@ -7300,6 +7301,13 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/tailwindcss-rtl": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tailwindcss-rtl/-/tailwindcss-rtl-0.9.0.tgz", + "integrity": "sha512-y7yC8QXjluDBEFMSX33tV6xMYrf0B3sa+tOB5JSQb6/G6laBU313a+Z+qxu55M1Qyn8tDMttjomsA8IsJD+k+w==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", diff --git a/package.json b/package.json index 230daf3..eac4bdb 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "lovable-tagger": "^1.1.13", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", + "tailwindcss-rtl": "^0.9.0", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", "vite": "^5.4.19", diff --git a/src/components/AdminLmsLayout.tsx b/src/components/AdminLmsLayout.tsx index cc95573..b734e53 100644 --- a/src/components/AdminLmsLayout.tsx +++ b/src/components/AdminLmsLayout.tsx @@ -34,115 +34,123 @@ import { Library, Activity, Warehouse, UserCog, Sparkles, } from "lucide-react"; import React from "react"; +import { useTranslation } from "react-i18next"; // ============= Navigation Config ============= -interface NavItem { title: string; url: string; icon: LucideIcon } +// Items store i18n keys (`titleKey`) rather than literal strings so Arabic +// language switches update the sidebar without the component having to +// re-mount. +interface NavItem { titleKey: string; url: string; icon: LucideIcon } const overviewItems: NavItem[] = [ - { title: "Admin Dashboard", url: "/admin/dashboard", icon: LayoutDashboard }, - { title: "Platform Dashboard", url: "/admin/platform", icon: BarChart3 }, + { titleKey: "nav.adminDashboard", url: "/admin/dashboard", icon: LayoutDashboard }, + { titleKey: "nav.platformDashboard", url: "/admin/platform", icon: BarChart3 }, ]; const lmsItems: NavItem[] = [ - { title: "Courses", url: "/admin/courses", icon: BookOpen }, - { title: "Students", url: "/admin/students", icon: Users }, - { title: "Teachers", url: "/admin/teachers", icon: GraduationCap }, - { title: "Batches", url: "/admin/batches", icon: Layers }, - { title: "Timetable", url: "/admin/timetable", icon: Calendar }, - { title: "Reports", url: "/admin/reports", icon: BarChart3 }, + { titleKey: "nav.courses", url: "/admin/courses", icon: BookOpen }, + { titleKey: "nav.students", url: "/admin/students", icon: Users }, + { titleKey: "nav.teachers", url: "/admin/teachers", icon: GraduationCap }, + { titleKey: "nav.batches", url: "/admin/batches", icon: Layers }, + { titleKey: "nav.timetable", url: "/admin/timetable", icon: Calendar }, + { titleKey: "nav.reports", url: "/admin/reports", icon: BarChart3 }, ]; const academicItems: NavItem[] = [ - { title: "Assignments", url: "/admin/assignments", icon: ClipboardList }, - { title: "Exams List", url: "/admin/examsList", icon: FileText }, - { title: "Exam Structures", url: "/admin/exam-structures", icon: Layers }, - { title: "Rubrics", url: "/admin/rubrics", icon: BookOpen }, - { title: "Generation", url: "/admin/generation", icon: Wand2 }, - { title: "Review Queue", url: "/admin/exam/review-queue", icon: Sparkles }, - { title: "AI Prompts", url: "/admin/ai/prompts", icon: Wand2 }, - { title: "AI Feedback", url: "/admin/ai/feedback", icon: Sparkles }, - { title: "Approval Workflows", url: "/admin/approval-workflows", icon: GitBranch }, + { titleKey: "nav.assignments", url: "/admin/assignments", icon: ClipboardList }, + { titleKey: "nav.examsList", url: "/admin/examsList", icon: FileText }, + { titleKey: "nav.examStructures", url: "/admin/exam-structures", icon: Layers }, + { titleKey: "nav.rubrics", url: "/admin/rubrics", icon: BookOpen }, + { titleKey: "nav.generation", url: "/admin/generation", icon: Wand2 }, + { titleKey: "nav.reviewQueue", url: "/admin/exam/review-queue", icon: Sparkles }, + { titleKey: "nav.aiPrompts", url: "/admin/ai/prompts", icon: Wand2 }, + { titleKey: "nav.aiFeedback", url: "/admin/ai/feedback", icon: Sparkles }, + { titleKey: "nav.approvalWorkflows", url: "/admin/approval-workflows", icon: GitBranch }, ]; const adaptiveItems: NavItem[] = [ - { title: "Taxonomy", url: "/admin/taxonomy", icon: Target }, - { title: "Resources", url: "/admin/resources", icon: FolderOpen }, + { titleKey: "nav.taxonomy", url: "/admin/taxonomy", icon: Target }, + { titleKey: "nav.resources", url: "/admin/resources", icon: FolderOpen }, ]; const institutionalItems: NavItem[] = [ - { title: "Academic Years", url: "/admin/academic-years", icon: CalendarDays }, - { title: "Departments", url: "/admin/departments", icon: Landmark }, - { title: "Admissions", url: "/admin/admissions", icon: UserPlus }, - { title: "Admission Register", url: "/admin/admission-register", icon: ScrollText }, - { title: "Exam Sessions", url: "/admin/exam-sessions", icon: FileText }, - { title: "Marksheets", url: "/admin/marksheets", icon: Award }, - { title: "Student Leave", url: "/admin/student-leave", icon: CalendarOff }, - { title: "Fees & Payments", url: "/admin/fees", icon: DollarSign }, - { title: "Lessons", url: "/admin/lessons", icon: BookMarked }, - { title: "Gradebook", url: "/admin/gradebook", icon: BarChartHorizontal }, - { title: "Student Progress", url: "/admin/student-progress", icon: TrendingUp }, - { title: "Library", url: "/admin/library", icon: Library }, - { title: "Activities", url: "/admin/activities", icon: Activity }, - { title: "Facilities", url: "/admin/facilities", icon: Warehouse }, + { titleKey: "nav.academicYears", url: "/admin/academic-years", icon: CalendarDays }, + { titleKey: "nav.departments", url: "/admin/departments", icon: Landmark }, + { titleKey: "nav.admissions", url: "/admin/admissions", icon: UserPlus }, + { titleKey: "nav.admissionRegister", url: "/admin/admission-register", icon: ScrollText }, + { titleKey: "nav.examSessions", url: "/admin/exam-sessions", icon: FileText }, + { titleKey: "nav.marksheets", url: "/admin/marksheets", icon: Award }, + { titleKey: "nav.studentLeave", url: "/admin/student-leave", icon: CalendarOff }, + { titleKey: "nav.fees", url: "/admin/fees", icon: DollarSign }, + { titleKey: "nav.lessons", url: "/admin/lessons", icon: BookMarked }, + { titleKey: "nav.gradebook", url: "/admin/gradebook", icon: BarChartHorizontal }, + { titleKey: "nav.studentProgress", url: "/admin/student-progress", icon: TrendingUp }, + { titleKey: "nav.library", url: "/admin/library", icon: Library }, + { titleKey: "nav.activities", url: "/admin/activities", icon: Activity }, + { titleKey: "nav.facilities", url: "/admin/facilities", icon: Warehouse }, ]; const managementItems: NavItem[] = [ - { title: "Users", url: "/admin/users", icon: Users }, - { title: "Entities", url: "/admin/entities", icon: Building2 }, - { title: "Classrooms", url: "/admin/classrooms", icon: GraduationCap }, - { title: "User Roles", url: "/admin/user-roles", icon: UserCog }, - { title: "Roles & Permissions", url: "/admin/roles-permissions", icon: Settings }, - { title: "Authority Matrix", url: "/admin/authority-matrix", icon: Workflow }, + { titleKey: "nav.users", url: "/admin/users", icon: Users }, + { titleKey: "nav.entities", url: "/admin/entities", icon: Building2 }, + { titleKey: "nav.classrooms", url: "/admin/classrooms", icon: GraduationCap }, + { titleKey: "nav.userRoles", url: "/admin/user-roles", icon: UserCog }, + { titleKey: "nav.rolesPermissions", url: "/admin/roles-permissions", icon: Settings }, + { titleKey: "nav.authorityMatrix", url: "/admin/authority-matrix", icon: Workflow }, ]; const reportItems: NavItem[] = [ - { title: "Student Performance", url: "/admin/student-performance", icon: BarChart3 }, - { title: "Stats Corporate", url: "/admin/stats-corporate", icon: Building2 }, - { title: "Record", url: "/admin/record", icon: History }, + { titleKey: "nav.studentPerformance", url: "/admin/student-performance", icon: BarChart3 }, + { titleKey: "nav.statsCorporate", url: "/admin/stats-corporate", icon: Building2 }, + { titleKey: "nav.record", url: "/admin/record", icon: History }, ]; const trainingItems: NavItem[] = [ - { title: "Vocabulary", url: "/admin/training/vocabulary", icon: BookA }, - { title: "Grammar", url: "/admin/training/grammar", icon: PenTool }, + { titleKey: "nav.vocabulary", url: "/admin/training/vocabulary", icon: BookA }, + { titleKey: "nav.grammar", url: "/admin/training/grammar", icon: PenTool }, ]; const configItems: NavItem[] = [ - { title: "FAQ Manager", url: "/admin/faq", icon: FaqIcon }, - { title: "Notification Rules", url: "/admin/notification-rules", icon: Bell }, - { title: "Approval Config", url: "/admin/approval-config", icon: Workflow }, + { titleKey: "nav.faqManager", url: "/admin/faq", icon: FaqIcon }, + { titleKey: "nav.notificationRules", url: "/admin/notification-rules", icon: Bell }, + { titleKey: "nav.approvalConfig", url: "/admin/approval-config", icon: Workflow }, ]; const supportItems: NavItem[] = [ - { title: "Payment Record", url: "/admin/payment-record", icon: CreditCard }, - { title: "Tickets", url: "/admin/tickets", icon: Ticket }, - { title: "Settings", url: "/admin/settings-platform", icon: Settings }, + { titleKey: "nav.paymentRecord", url: "/admin/payment-record", icon: CreditCard }, + { titleKey: "nav.tickets", url: "/admin/tickets", icon: Ticket }, + { titleKey: "nav.settings", url: "/admin/settings-platform", icon: Settings }, ]; // ============= Reusable sidebar group ============= -function SidebarNavGroup({ label, items }: { label: string; items: NavItem[] }) { +function SidebarNavGroup({ labelKey, items }: { labelKey: string; items: NavItem[] }) { const { state } = useSidebar(); + const { t } = useTranslation(); const collapsed = state === "collapsed"; return ( - {label} + {t(labelKey)} - {items.map((item) => ( - - - - - {!collapsed && {item.title}} - - - - ))} + {items.map((item) => { + const label = t(item.titleKey); + return ( + + + + + {!collapsed && {label}} + + + + ); + })} @@ -150,42 +158,52 @@ function SidebarNavGroup({ label, items }: { label: string; items: NavItem[] }) } // ============= Breadcrumbs ============= -const routeLabels: Record = { - admin: "Admin", dashboard: "Dashboard", platform: "Platform", courses: "Courses", - students: "Students", teachers: "Teachers", batches: "Batches", timetable: "Timetable", - reports: "Reports", assignments: "Assignments", examsList: "Exams List", - "exam-structures": "Exam Structures", rubrics: "Rubrics", generation: "Generation", - "review-queue": "Review Queue", review: "Review", prompts: "AI Prompts", ai: "AI", - feedback: "Feedback", - "approval-workflows": "Approval Workflows", users: "Users", entities: "Entities", - classrooms: "Classrooms", "student-performance": "Student Performance", - "stats-corporate": "Stats Corporate", record: "Record", training: "Training", - vocabulary: "Vocabulary", grammar: "Grammar", "payment-record": "Payment Record", - tickets: "Tickets", "settings-platform": "Settings", settings: "Settings", - profile: "Profile", exam: "Exam", - "academic-years": "Academic Years", departments: "Departments", - admissions: "Admissions", "admission-register": "Admission Register", - "exam-sessions": "Exam Sessions", marksheets: "Marksheets", - faq: "FAQ Manager", "notification-rules": "Notification Rules", - "approval-config": "Approval Config", - "student-leave": "Student Leave", fees: "Fees & Payments", lessons: "Lessons", - gradebook: "Gradebook", "student-progress": "Student Progress", - library: "Library", activities: "Activities", facilities: "Facilities", +// Map URL segments to i18n keys so breadcrumbs follow the active locale. +// Segments without an entry fall back to a Title-cased version of the slug. +const routeLabelKeys: Record = { + admin: "breadcrumb.admin", dashboard: "breadcrumb.dashboard", + platform: "breadcrumb.platform", courses: "nav.courses", + students: "nav.students", teachers: "nav.teachers", batches: "nav.batches", + timetable: "nav.timetable", reports: "nav.reports", + assignments: "nav.assignments", examsList: "nav.examsList", + "exam-structures": "nav.examStructures", rubrics: "nav.rubrics", + generation: "nav.generation", "review-queue": "nav.reviewQueue", + review: "breadcrumb.review", prompts: "nav.aiPrompts", ai: "breadcrumb.ai", + feedback: "breadcrumb.feedback", + "approval-workflows": "nav.approvalWorkflows", users: "nav.users", + entities: "nav.entities", classrooms: "nav.classrooms", + "student-performance": "nav.studentPerformance", + "stats-corporate": "nav.statsCorporate", record: "nav.record", + training: "sidebarGroup.training", vocabulary: "nav.vocabulary", + grammar: "nav.grammar", "payment-record": "nav.paymentRecord", + tickets: "nav.tickets", "settings-platform": "nav.settings", + settings: "nav.settings", profile: "nav.profile", exam: "breadcrumb.exam", + "academic-years": "nav.academicYears", departments: "nav.departments", + admissions: "nav.admissions", "admission-register": "nav.admissionRegister", + "exam-sessions": "nav.examSessions", marksheets: "nav.marksheets", + faq: "nav.faqManager", "notification-rules": "nav.notificationRules", + "approval-config": "nav.approvalConfig", + "student-leave": "nav.studentLeave", fees: "nav.fees", + lessons: "nav.lessons", gradebook: "nav.gradebook", + "student-progress": "nav.studentProgress", library: "nav.library", + activities: "nav.activities", facilities: "nav.facilities", }; function AppBreadcrumbs() { const location = useLocation(); + const { t } = useTranslation(); const segments = location.pathname.split("/").filter(Boolean); return ( - Home + {t("common.home")} {segments.map((seg, i) => { const path = "/" + segments.slice(0, i + 1).join("/"); - const label = routeLabels[seg] || seg.charAt(0).toUpperCase() + seg.slice(1); + const key = routeLabelKeys[seg]; + const label = key ? t(key) : seg.charAt(0).toUpperCase() + seg.slice(1); const isLast = i === segments.length - 1; return ( @@ -205,6 +223,7 @@ function AppBreadcrumbs() { export default function AdminLmsLayout() { const { user, logout } = useAuth(); const navigate = useNavigate(); + const { t } = useTranslation(); const initials = user?.name?.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() ?? "??"; @@ -220,7 +239,7 @@ export default function AdminLmsLayout() {
- +
@@ -228,7 +247,7 @@ export default function AdminLmsLayout() { - @@ -241,19 +260,19 @@ export default function AdminLmsLayout() {
-

{user?.name ?? "User"}

+

{user?.name ?? t("userMenu.userFallback")}

{user?.email ?? ""}

navigate("/admin/profile")}> - Profile + {t("userMenu.profile")} navigate("/admin/settings-platform")}> - Settings + {t("userMenu.settings")} - Logout + {t("userMenu.logout")}
@@ -267,10 +286,10 @@ export default function AdminLmsLayout() { - Need help? + {t("chrome.needHelp")} ); @@ -282,25 +301,27 @@ function AdminSidebar() { const collapsed = state === "collapsed"; const { user } = useAuth(); const { hasAnyPermission, isAdmin } = usePermissions(); + const { t, i18n } = useTranslation(); + const sidebarSide = i18n.dir() === "rtl" ? "right" : "left"; const showManagement = isAdmin || hasAnyPermission(["view_entities", "view_students", "view_teachers"]); const showReports = isAdmin || hasAnyPermission(["view_statistics", "view_student_performance", "view_entity_statistics"]); const showPayments = isAdmin || hasAnyPermission(["view_payment_record", "pay_entity"]); return ( - +
{collapsed ? ( EnCoach ) : ( EnCoach — Unlock your potential with AI powered platform )} @@ -308,47 +329,50 @@ function AdminSidebar() { - - - - - - {showManagement && } - {showReports && } - + + + + + + {showManagement && } + {showReports && } + - Training + {t("sidebarGroup.training")} {!collapsed && } - {trainingItems.map((item) => ( - - - - - {!collapsed && {item.title}} - - - - ))} + {trainingItems.map((item) => { + const label = t(item.titleKey); + return ( + + + + + {!collapsed && {label}} + + + + ); + })} - i.url !== "/admin/payment-record")} /> + i.url !== "/admin/payment-record")} /> @@ -358,7 +382,7 @@ function AdminSidebar() {
{!collapsed && (
- {user?.name ?? "User"} + {user?.name ?? t("userMenu.userFallback")} {user?.email ?? ""}
)} diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index f557045..9f0bfb9 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -5,6 +5,7 @@ import { } from "lucide-react"; import { NavLink } from "@/components/NavLink"; import { useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, @@ -81,9 +82,11 @@ export function AppSidebar() { const { state } = useSidebar(); const collapsed = state === "collapsed"; const location = useLocation(); + const { i18n } = useTranslation(); + const sidebarSide = i18n.dir() === "rtl" ? "right" : "left"; return ( - +
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 5bb8332..1855697 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,6 +1,7 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; +import { withTranslation, type WithTranslation } from "react-i18next"; -interface Props { +interface Props extends WithTranslation { children: ReactNode; fallback?: ReactNode; } @@ -10,7 +11,12 @@ interface State { error: Error | null; } -export class ErrorBoundary extends Component { +/** + * Error boundary is a classical React class component, so we bridge it to + * i18next via the `withTranslation` HOC. That keeps the tree-shaking of + * `react-i18next`'s hook path intact while giving us a `t` prop here. + */ +class ErrorBoundaryInner extends Component { constructor(props: Props) { super(props); this.state = { hasError: false, error: null }; @@ -27,6 +33,7 @@ export class ErrorBoundary extends Component { render() { if (this.state.hasError) { if (this.props.fallback) return this.props.fallback; + const { t } = this.props; return (
@@ -36,13 +43,13 @@ export class ErrorBoundary extends Component {
-

Something went wrong

+

{t("errors.somethingWrong")}

- An unexpected error occurred. Please try refreshing the page. + {t("errors.unexpectedDescription")}

{this.state.error && ( -
- Error details +
+ {t("errors.errorDetails")}
{this.state.error.message}
)} @@ -50,7 +57,7 @@ export class ErrorBoundary extends Component { onClick={() => window.location.reload()} className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90" > - Refresh Page + {t("errors.refreshPage")}
@@ -60,3 +67,5 @@ export class ErrorBoundary extends Component { return this.props.children; } } + +export const ErrorBoundary = withTranslation()(ErrorBoundaryInner); diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx index ae6d1f5..b27cc58 100644 --- a/src/components/LanguageToggle.tsx +++ b/src/components/LanguageToggle.tsx @@ -12,18 +12,23 @@ import { import { SUPPORTED_LANGS, type SupportedLang } from "@/i18n"; export function LanguageToggle() { - const { i18n } = useTranslation(); + const { i18n, t } = useTranslation(); const current = (i18n.language?.split("-")[0] || "en") as SupportedLang; return ( - - Language + {t("language.label")} {SUPPORTED_LANGS.map((lang) => ( !n.read).length; return ( - -
Notifications
+
{t("notifications.title")}
- {notifications.slice(0, 4).map((n) => ( - - {n.title} - {n.message} - {n.time} - - ))} + {notifications.length === 0 ? ( +
+ {t("notifications.empty")} +
+ ) : ( + notifications.slice(0, 4).map((n) => ( + + {n.title} + {n.message} + {n.time} + + )) + )}
); diff --git a/src/components/RoleLayout.tsx b/src/components/RoleLayout.tsx index 57c9400..d069884 100644 --- a/src/components/RoleLayout.tsx +++ b/src/components/RoleLayout.tsx @@ -1,4 +1,5 @@ -import { Outlet, Link, useNavigate, useLocation } from "react-router-dom"; +import { Outlet, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, @@ -15,17 +16,21 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { LogOut, User, Settings, LucideIcon } from "lucide-react"; -import React from "react"; +import { LogOut, User, LucideIcon } from "lucide-react"; +/** + * Non-admin portal shell (student / teacher / etc.). Nav items hold i18n + * keys (`titleKey` / `labelKey`) rather than literal strings so they react + * to language changes without re-mounting the layout. + */ export interface NavItem { - title: string; + titleKey: string; url: string; icon: LucideIcon; } export interface NavGroup { - label: string; + labelKey: string; items: NavItem[]; } @@ -36,30 +41,34 @@ interface RoleLayoutProps { function SidebarNav({ navGroups }: { navGroups: NavGroup[] }) { const { state } = useSidebar(); + const { t } = useTranslation(); const collapsed = state === "collapsed"; return ( <> {navGroups.map((group) => ( - - {group.label} + + {t(group.labelKey)} - {group.items.map((item) => ( - - - - - {!collapsed && {item.title}} - - - - ))} + {group.items.map((item) => { + const label = t(item.titleKey); + return ( + + + + + {!collapsed && {label}} + + + + ); + })} @@ -71,6 +80,8 @@ function SidebarNav({ navGroups }: { navGroups: NavGroup[] }) { export default function RoleLayout({ navGroups, role }: RoleLayoutProps) { const { user, logout } = useAuth(); const navigate = useNavigate(); + const { t, i18n } = useTranslation(); + const sidebarSide = i18n.dir() === "rtl" ? "right" : "left"; const initials = user?.name?.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() ?? "??"; @@ -79,20 +90,23 @@ export default function RoleLayout({ navGroups, role }: RoleLayoutProps) { navigate("/login"); }; + const roleLabel = t(`roles.${role}`, { defaultValue: role }); + const portalTitle = `${roleLabel} ${t("chrome.portalSuffix")}`.trim(); + return (
- +
EnCoach EnCoach — Unlock your potential with AI powered platform
@@ -108,7 +122,9 @@ export default function RoleLayout({ navGroups, role }: RoleLayoutProps) { {initials}
- {user?.name} + + {user?.name ?? t("userMenu.userFallback")} + {user?.email}
@@ -118,8 +134,8 @@ export default function RoleLayout({ navGroups, role }: RoleLayoutProps) {
- - {role} Portal + + {portalTitle}
@@ -135,16 +151,16 @@ export default function RoleLayout({ navGroups, role }: RoleLayoutProps) {
-

{user?.name}

+

{user?.name ?? t("userMenu.userFallback")}

{user?.email}

navigate(`/${role}/profile`)}> - Profile + {t("userMenu.profile")} - Logout + {t("userMenu.logout")}
diff --git a/src/components/StudentLayout.tsx b/src/components/StudentLayout.tsx index 9fca00f..201c060 100644 --- a/src/components/StudentLayout.tsx +++ b/src/components/StudentLayout.tsx @@ -2,47 +2,52 @@ import RoleLayout, { NavGroup } from "./RoleLayout"; import ExamPopup from "./student/ExamPopup"; import { LayoutDashboard, BookOpen, ClipboardList, BarChart3, - CalendarCheck, Calendar, User, Target, GraduationCap, ListChecks, + CalendarCheck, Calendar, User, Target, ListChecks, MessageSquare, Megaphone, Mail, TrendingUp, } from "lucide-react"; +/** + * Student portal shell. Nav items hold i18n keys which `RoleLayout` + * resolves via `useTranslation`, so switching language does not require + * re-mounting the layout. + */ const navGroups: NavGroup[] = [ { - label: "Learning", + labelKey: "sidebarGroup.learning", items: [ - { title: "Dashboard", url: "/student/dashboard", icon: LayoutDashboard }, - { title: "My Courses", url: "/student/courses", icon: BookOpen }, - { title: "Subject Registration", url: "/student/subject-registration", icon: ListChecks }, - { title: "Assignments", url: "/student/assignments", icon: ClipboardList }, + { titleKey: "nav.dashboard", url: "/student/dashboard", icon: LayoutDashboard }, + { titleKey: "nav.myCourses", url: "/student/courses", icon: BookOpen }, + { titleKey: "nav.subjectRegistration", url: "/student/subject-registration", icon: ListChecks }, + { titleKey: "nav.assignments", url: "/student/assignments", icon: ClipboardList }, ], }, { - label: "Adaptive Learning", + labelKey: "sidebarGroup.adaptiveLearning", items: [ - { title: "My Subjects", url: "/student/subjects", icon: Target }, + { titleKey: "nav.mySubjects", url: "/student/subjects", icon: Target }, ], }, { - label: "Progress", + labelKey: "sidebarGroup.progress", items: [ - { title: "Grades", url: "/student/grades", icon: BarChart3 }, - { title: "Attendance", url: "/student/attendance", icon: CalendarCheck }, - { title: "Timetable", url: "/student/timetable", icon: Calendar }, - { title: "My Journey", url: "/student/journey", icon: TrendingUp }, + { titleKey: "nav.grades", url: "/student/grades", icon: BarChart3 }, + { titleKey: "nav.attendance", url: "/student/attendance", icon: CalendarCheck }, + { titleKey: "nav.timetable", url: "/student/timetable", icon: Calendar }, + { titleKey: "nav.myJourney", url: "/student/journey", icon: TrendingUp }, ], }, { - label: "Communication", + labelKey: "sidebarGroup.communication", items: [ - { title: "Discussions", url: "/student/discussions", icon: MessageSquare }, - { title: "Messages", url: "/student/messages", icon: Mail }, - { title: "Announcements", url: "/student/announcements", icon: Megaphone }, + { titleKey: "nav.discussions", url: "/student/discussions", icon: MessageSquare }, + { titleKey: "nav.messages", url: "/student/messages", icon: Mail }, + { titleKey: "nav.announcements", url: "/student/announcements", icon: Megaphone }, ], }, { - label: "Account", + labelKey: "sidebarGroup.account", items: [ - { title: "Profile", url: "/student/profile", icon: User }, + { titleKey: "nav.profile", url: "/student/profile", icon: User }, ], }, ]; diff --git a/src/components/TeacherLayout.tsx b/src/components/TeacherLayout.tsx index a939eb8..768d481 100644 --- a/src/components/TeacherLayout.tsx +++ b/src/components/TeacherLayout.tsx @@ -1,39 +1,40 @@ import RoleLayout, { NavGroup } from "./RoleLayout"; import { - LayoutDashboard, BookOpen, ClipboardList, FileText, + LayoutDashboard, BookOpen, ClipboardList, CalendarCheck, Users, Calendar, User, MessageSquare, - Megaphone, Wand2, Library, + Megaphone, Library, } from "lucide-react"; +/** Teacher portal shell. See `StudentLayout` for the nav-config rationale. */ const navGroups: NavGroup[] = [ { - label: "Teaching", + labelKey: "sidebarGroup.teaching", items: [ - { title: "Dashboard", url: "/teacher/dashboard", icon: LayoutDashboard }, - { title: "Courses", url: "/teacher/courses", icon: BookOpen }, - { title: "Resource Library", url: "/teacher/library", icon: Library }, - { title: "Assignments", url: "/teacher/assignments", icon: ClipboardList }, + { titleKey: "nav.dashboard", url: "/teacher/dashboard", icon: LayoutDashboard }, + { titleKey: "nav.courses", url: "/teacher/courses", icon: BookOpen }, + { titleKey: "nav.resourceLibrary", url: "/teacher/library", icon: Library }, + { titleKey: "nav.assignments", url: "/teacher/assignments", icon: ClipboardList }, ], }, { - label: "Management", + labelKey: "sidebarGroup.management", items: [ - { title: "Attendance", url: "/teacher/attendance", icon: CalendarCheck }, - { title: "Students", url: "/teacher/students", icon: Users }, - { title: "Timetable", url: "/teacher/timetable", icon: Calendar }, + { titleKey: "nav.attendance", url: "/teacher/attendance", icon: CalendarCheck }, + { titleKey: "nav.students", url: "/teacher/students", icon: Users }, + { titleKey: "nav.timetable", url: "/teacher/timetable", icon: Calendar }, ], }, { - label: "Communication", + labelKey: "sidebarGroup.communication", items: [ - { title: "Discussions", url: "/teacher/discussions", icon: MessageSquare }, - { title: "Announcements", url: "/teacher/announcements", icon: Megaphone }, + { titleKey: "nav.discussions", url: "/teacher/discussions", icon: MessageSquare }, + { titleKey: "nav.announcements", url: "/teacher/announcements", icon: Megaphone }, ], }, { - label: "Account", + labelKey: "sidebarGroup.account", items: [ - { title: "Profile", url: "/teacher/profile", icon: User }, + { titleKey: "nav.profile", url: "/teacher/profile", icon: User }, ], }, ]; diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 3569fed..bb7ef40 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,6 +1,7 @@ import { Moon, Sun, Monitor } from "lucide-react"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { @@ -20,6 +21,7 @@ import { export function ThemeToggle() { const { theme, setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); + const { t } = useTranslation(); useEffect(() => { setMounted(true); @@ -33,7 +35,8 @@ export function ThemeToggle() { @@ -67,7 +74,7 @@ export default function AiAssistantDrawer() { - EnCoach AI Assistant + {t("ai.assistantTitle")} @@ -90,8 +97,8 @@ export default function AiAssistantDrawer() { {messages.length === 0 && (
-

Ask me anything about the platform,

-

or click a quick action above.

+

{t("ai.emptyLine1")}

+

{t("ai.emptyLine2")}

)} {messages.map((msg, i) => ( @@ -99,27 +106,27 @@ export default function AiAssistantDrawer() { key={i} className={`rounded-lg p-3 text-sm ${ msg.role === "user" - ? "bg-primary text-primary-foreground ml-8" - : "bg-muted mr-8" + ? "bg-primary text-primary-foreground ms-8" + : "bg-muted me-8" }`} > {msg.role === "ai" && ( - + )} {msg.text}
))} {chatMutation.isPending && ( -
+
- Thinking... + {t("ai.thinking")}
)}
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSend(input)} @@ -128,6 +135,8 @@ export default function AiAssistantDrawer() { size="icon" onClick={() => handleSend(input)} disabled={!input.trim() || chatMutation.isPending} + aria-label={t("ai.send")} + title={t("ai.send")} > diff --git a/src/components/ai/AiInsightsPanel.tsx b/src/components/ai/AiInsightsPanel.tsx index 4bb83f2..ccc8220 100644 --- a/src/components/ai/AiInsightsPanel.tsx +++ b/src/components/ai/AiInsightsPanel.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo } from "react"; import { useMutation } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Sparkles, TrendingUp, AlertTriangle, Info, Loader2 } from "lucide-react"; import { analyticsService, type AiInsightItem } from "@/services/analytics.service"; @@ -35,14 +36,15 @@ interface Props { export default function AiInsightsPanel({ data = EMPTY_PAYLOAD }: Props) { const { toast } = useToast(); + const { t } = useTranslation(); const payloadKey = useMemo(() => JSON.stringify(data), [data]); const mutation = useMutation({ mutationFn: (payload: Record) => analyticsService.getInsights(payload), onError: (err: Error) => { toast({ - title: "Insights unavailable", - description: err.message || "Could not load AI insights.", + title: t("ai.insightsUnavailable"), + description: err.message || t("ai.couldNotLoadInsights"), variant: "destructive", }); }, @@ -60,21 +62,21 @@ export default function AiInsightsPanel({ data = EMPTY_PAYLOAD }: Props) { - AI Platform Insights + {t("ai.platformInsightsTitle")} {mutation.isPending && (
- Loading insights… + {t("ai.loadingInsights")}
)} {mutation.isError && !mutation.isPending && ( -

Could not load insights.

+

{t("ai.couldNotLoadInsights")}

)} {mutation.isSuccess && items.length === 0 && ( -

No insights available for this view.

+

{t("ai.noInsightsAvailable")}

)} {!mutation.isPending && items.length > 0 && (
@@ -82,10 +84,10 @@ export default function AiInsightsPanel({ data = EMPTY_PAYLOAD }: Props) { const Icon = insightIcon(item.severity); const color = insightColor(item.severity); return ( -
+
- - {item.title} + + {item.title}

{item.description}

{item.recommendation && ( diff --git a/src/components/ai/AiSearchBar.tsx b/src/components/ai/AiSearchBar.tsx index 9a31c52..f67b173 100644 --- a/src/components/ai/AiSearchBar.tsx +++ b/src/components/ai/AiSearchBar.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useMutation } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Input } from "@/components/ui/input"; import { Sparkles, Search, Loader2, X } from "lucide-react"; import { useNavigate } from "react-router-dom"; @@ -10,14 +11,15 @@ export default function AiSearchBar() { const [query, setQuery] = useState(""); const navigate = useNavigate(); const { toast } = useToast(); + const { t } = useTranslation(); const searchMutation = useMutation({ mutationFn: (q: string) => analyticsService.search(q), onError: (err: Error) => { toast({ variant: "destructive", - title: "Search failed", - description: err.message || "Could not complete AI search.", + title: t("chrome.aiSearchFailedTitle"), + description: err.message || t("chrome.aiSearchFailedDesc"), }); }, }); @@ -32,11 +34,11 @@ export default function AiSearchBar() { return (
- - + + { setQuery(e.target.value); @@ -50,7 +52,7 @@ export default function AiSearchBar() { setQuery(""); searchMutation.reset(); }} - className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" + className="absolute end-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" > @@ -58,11 +60,11 @@ export default function AiSearchBar() {
{(searchMutation.isPending || result !== undefined) && ( -
+
{searchMutation.isPending ? (
- AI is searching... + {t("chrome.aiSearching")}
) : result?.answer ? (
@@ -72,7 +74,7 @@ export default function AiSearchBar() {
{result.suggestions?.length > 0 && (
-

Related queries

+

{t("chrome.aiRelatedQueries")}

{result.suggestions.map((s, i) => (
)}
diff --git a/src/components/ai/AiTipBanner.tsx b/src/components/ai/AiTipBanner.tsx index 3484e70..4889314 100644 --- a/src/components/ai/AiTipBanner.tsx +++ b/src/components/ai/AiTipBanner.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Sparkles, X, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { coachingService } from "@/services/coaching.service"; @@ -12,6 +13,7 @@ interface Props { export default function AiTipBanner({ context = "dashboard", variant = "tip", dismissible = true }: Props) { const [dismissed, setDismissed] = useState(false); + const { t } = useTranslation(); const { data, isLoading, isError, error } = useQuery({ queryKey: ["ai", "tip", context], @@ -22,23 +24,30 @@ export default function AiTipBanner({ context = "dashboard", variant = "tip", di const bgClass = variant === "recommendation" ? "bg-accent/50 border-accent" : variant === "insight" ? "bg-primary/5 border-primary/20" : "bg-muted/50 border-muted-foreground/10"; + const variantLabel = + variant === "insight" + ? t("ai.insightLabel") + : variant === "recommendation" + ? t("ai.recommendationLabel") + : t("ai.tipLabel"); + if (isLoading) { return (
- Loading AI tip… + {t("ai.loadingTip")}
); } if (isError || !data) { return ( -
+
-
- AI Tip +
+ {t("ai.tipLabel")}

- {isError ? (error instanceof Error ? error.message : "Could not load tip.") : "No tip available."} + {isError ? (error instanceof Error ? error.message : t("ai.couldNotLoadTip")) : t("ai.noTipAvailable")}

{dismissible && ( @@ -52,25 +61,25 @@ export default function AiTipBanner({ context = "dashboard", variant = "tip", di if (!data.tip?.trim()) { return ( -
+
-
- AI {variant === "tip" ? "Tip" : variant === "insight" ? "Insight" : "Recommendation"} -

No tip for this context yet.

+
+ {variantLabel} +

{t("ai.noTipForContext")}

); } const label = data.category && data.category !== "general" - ? `AI ${data.category.charAt(0).toUpperCase() + data.category.slice(1)} Tip` - : `AI ${variant === "tip" ? "Tip" : variant === "insight" ? "Insight" : "Recommendation"}`; + ? t("ai.categoryTipLabel", { category: data.category.charAt(0).toUpperCase() + data.category.slice(1) }) + : variantLabel; return ( -
+
-
- {label} +
+ {label}

{data.tip}

{dismissible && ( diff --git a/src/components/student/ExamPopup.tsx b/src/components/student/ExamPopup.tsx index 5fc037f..7370c1a 100644 --- a/src/components/student/ExamPopup.tsx +++ b/src/components/student/ExamPopup.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -10,6 +11,7 @@ import type { StudentExamAssignment } from "@/types"; export default function ExamPopup() { const navigate = useNavigate(); + const { t, i18n } = useTranslation(); const [open, setOpen] = useState(false); const [dismissed, setDismissed] = useState>(new Set()); @@ -30,11 +32,14 @@ export default function ExamPopup() { if (pendingExams.length === 0) return null; + // Localize dates with the active language so Arabic users see ar-EG + // month names instead of "Apr 19, 2026". The locale is taken from i18n. + const dateLocale = i18n.language?.startsWith("ar") ? "ar-EG" : "en-US"; const formatDate = (iso: string | null) => { if (!iso) return "—"; const d = new Date(iso); - return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) + - " " + d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + return d.toLocaleDateString(dateLocale, { month: "short", day: "numeric", year: "numeric" }) + + " " + d.toLocaleTimeString(dateLocale, { hour: "2-digit", minute: "2-digit" }); }; const handleStart = (exam: StudentExamAssignment) => { @@ -54,10 +59,10 @@ export default function ExamPopup() { - Upcoming Exams ({pendingExams.length}) + {t("examPopup.title", { n: pendingExams.length })} -
+
{pendingExams.map((exam) => (
@@ -66,7 +71,7 @@ export default function ExamPopup() { variant={exam.schedule_state === "active" ? "default" : "secondary"} className="capitalize text-xs" > - {exam.schedule_state === "active" ? "Active Now" : exam.schedule_state} + {exam.schedule_state === "active" ? t("examPopup.activeNow") : exam.schedule_state}
@@ -77,11 +82,11 @@ export default function ExamPopup() {
- From: {formatDate(exam.start_date)} + {t("examPopup.from")} {formatDate(exam.start_date)} - To: {formatDate(exam.end_date)} + {t("examPopup.to")} {formatDate(exam.end_date)}
@@ -93,20 +98,20 @@ export default function ExamPopup() { className="gap-1.5" > - {exam.can_start ? "Start Exam" : "Not Available Yet"} + {exam.can_start ? t("examPopup.startExam") : t("examPopup.notAvailableYet")} {!exam.can_start && ( - + {exam.schedule_state === "planned" - ? "Exam will be available once it becomes active" - : "Exam is not currently active"} + ? t("examPopup.willBeAvailable") + : t("examPopup.notActive")} )}
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index ca91ff5..8adff19 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -60,7 +60,12 @@ const BreadcrumbPage = React.forwardRef) => ( - ); diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 900a69e..16c0809 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -42,8 +42,8 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C ...classNames, }} components={{ - IconLeft: ({ ..._props }) => , - IconRight: ({ ..._props }) => , + IconLeft: ({ ..._props }) => , + IconRight: ({ ..._props }) => , }} {...props} /> diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index 3aa0f31..0fd4133 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -185,7 +185,7 @@ const CarouselPrevious = React.forwardRef - + Previous slide ); @@ -213,7 +213,7 @@ const CarouselNext = React.forwardRef - + Next slide ); diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx index b5d9db0..0d154ff 100644 --- a/src/components/ui/context-menu.tsx +++ b/src/components/ui/context-menu.tsx @@ -32,7 +32,7 @@ const ContextMenuSubTrigger = React.forwardRef< {...props} > {children} - + )); ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index ddabcfd..8249f84 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -32,7 +32,7 @@ const DropdownMenuSubTrigger = React.forwardRef< {...props} > {children} - + )); DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx index 15687e4..f2924af 100644 --- a/src/components/ui/menubar.tsx +++ b/src/components/ui/menubar.tsx @@ -57,7 +57,7 @@ const MenubarSubTrigger = React.forwardRef< {...props} > {children} - + )); MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index 31ae1be..25ead1f 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -47,17 +47,17 @@ const PaginationLink = ({ className, isActive, size = "icon", ...props }: Pagina PaginationLink.displayName = "PaginationLink"; const PaginationPrevious = ({ className, ...props }: React.ComponentProps) => ( - - + + Previous ); PaginationPrevious.displayName = "PaginationPrevious"; const PaginationNext = ({ className, ...props }: React.ComponentProps) => ( - + Next - + ); PaginationNext.displayName = "PaginationNext"; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index beb1a59..cb48f4b 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -217,7 +217,7 @@ const Sidebar = React.forwardRef< Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef, React.ComponentProps>( - ({ className, onClick, ...props }, ref) => { + ({ className, onClick, "aria-label": ariaLabel, ...props }, ref) => { const { toggleSidebar } = useSidebar(); return ( @@ -226,6 +226,7 @@ const SidebarTrigger = React.forwardRef, React.C data-sidebar="trigger" variant="ghost" size="icon" + aria-label={ariaLabel ?? "Toggle sidebar"} className={cn("h-7 w-7", className)} onClick={(event) => { onClick?.(event); @@ -233,8 +234,8 @@ const SidebarTrigger = React.forwardRef, React.C }} {...props} > - - Toggle Sidebar + + {ariaLabel ?? "Toggle sidebar"} ); }, diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 5f5bfb9..1a97888 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -1,6 +1,6 @@ import type { Translations } from "./en"; -/** Arabic (RTL) translations. */ +/** Arabic (RTL) translations. Keep key parity with `en.ts`. */ const ar: Translations = { common: { cancel: "إلغاء", @@ -20,30 +20,293 @@ const ar: Translations = { status: "الحالة", settings: "الإعدادات", signOut: "تسجيل الخروج", + viewAll: "عرض الكل", + dismiss: "تجاهل", + pending: "قيد الانتظار", + active: "نشط", + inactive: "غير نشط", + available: "متاح", + unavailable: "غير متاح", + name: "الاسم", + email: "البريد الإلكتروني", + user: "المستخدم", + home: "الرئيسية", }, auth: { signIn: "تسجيل الدخول", signInTitle: "تسجيل الدخول إلى EnCoach", + welcomeBack: "مرحباً بعودتك", + signInDescription: "سجّل الدخول إلى حسابك للمتابعة", email: "البريد الإلكتروني", + emailPlaceholder: "you@example.com", password: "كلمة المرور", + rememberMe: "تذكرني", forgotPassword: "هل نسيت كلمة المرور؟", needAccount: "ليس لديك حساب؟", signUp: "إنشاء حساب", invalidCredentials: "البريد الإلكتروني أو كلمة المرور غير صحيحة", + missingCredentials: "يرجى إدخال البريد الإلكتروني وكلمة المرور", + loginFailedTitle: "فشل تسجيل الدخول", + errorTitle: "خطأ", }, nav: { + adminDashboard: "لوحة الإدارة", + platformDashboard: "لوحة المنصّة", dashboard: "لوحة التحكم", courses: "الدورات", + myCourses: "دوراتي", students: "الطلاب", teachers: "المعلمون", - exams: "الاختبارات", + batches: "الدفعات", + timetable: "الجدول الدراسي", reports: "التقارير", - settings: "الإعدادات", - profile: "الملف الشخصي", - privacy: "مركز الخصوصية", + assignments: "الواجبات", + examsList: "قائمة الاختبارات", + examStructures: "هياكل الاختبارات", + rubrics: "معايير التقييم", + generation: "التوليد", reviewQueue: "قائمة المراجعة", aiPrompts: "تعليمات الذكاء الاصطناعي", aiFeedback: "ملاحظات الذكاء الاصطناعي", + approvalWorkflows: "سير عمل الموافقات", + taxonomy: "التصنيف", + resources: "الموارد", + resourceLibrary: "مكتبة الموارد", + academicYears: "السنوات الأكاديمية", + departments: "الأقسام", + admissions: "القبول", + admissionRegister: "سجل القبول", + examSessions: "جلسات الاختبارات", + marksheets: "كشوفات الدرجات", + studentLeave: "إجازات الطلاب", + fees: "الرسوم والمدفوعات", + lessons: "الدروس", + gradebook: "سجل الدرجات", + studentProgress: "تقدم الطالب", + library: "المكتبة", + activities: "الأنشطة", + facilities: "المرافق", + users: "المستخدمون", + entities: "الكيانات", + classrooms: "الفصول", + userRoles: "أدوار المستخدمين", + rolesPermissions: "الأدوار والصلاحيات", + authorityMatrix: "مصفوفة الصلاحيات", + studentPerformance: "أداء الطلاب", + statsCorporate: "إحصاءات الشركات", + record: "السجل", + vocabulary: "المفردات", + grammar: "القواعد", + faqManager: "إدارة الأسئلة الشائعة", + notificationRules: "قواعد الإشعارات", + approvalConfig: "إعدادات الموافقات", + paymentRecord: "سجل المدفوعات", + tickets: "التذاكر", + settings: "الإعدادات", + profile: "الملف الشخصي", + privacy: "مركز الخصوصية", + subjectRegistration: "تسجيل المواد", + mySubjects: "موادي", + grades: "الدرجات", + attendance: "الحضور", + myJourney: "رحلتي", + discussions: "المناقشات", + messages: "الرسائل", + announcements: "الإعلانات", + }, + sidebarGroup: { + overview: "نظرة عامة", + lms: "نظام التعلم", + adaptiveLearning: "التعلم التكيّفي", + institutional: "المؤسسي", + academic: "الأكاديمي", + management: "الإدارة", + reports: "التقارير", + configuration: "الإعدادات", + training: "التدريب", + support: "الدعم", + learning: "التعلم", + progress: "التقدم", + communication: "التواصل", + account: "الحساب", + teaching: "التدريس", + }, + breadcrumb: { + home: "الرئيسية", + admin: "الإدارة", + dashboard: "لوحة التحكم", + platform: "المنصّة", + exam: "الاختبار", + review: "مراجعة", + ai: "الذكاء الاصطناعي", + feedback: "ملاحظات", + }, + userMenu: { + profile: "الملف الشخصي", + settings: "الإعدادات", + logout: "تسجيل الخروج", + userFallback: "مستخدم", + }, + chrome: { + needHelp: "هل تحتاج مساعدة؟", + ticketsTooltip: "تذاكر الدعم", + aiSearchPlaceholder: "اسأل عن أي شيء… مثال: «اعرض الطلاب ذوي الحضور المنخفض»", + aiSearching: "الذكاء الاصطناعي يبحث…", + aiRelatedQueries: "استفسارات ذات صلة", + aiNoResults: "لا توجد نتائج لـ «{{q}}». جرّب سؤالاً أو كلمة مفتاحية مختلفة.", + aiSearchFailedTitle: "فشل البحث", + aiSearchFailedDesc: "تعذّر إكمال بحث الذكاء الاصطناعي.", + portalSuffix: "البوابة", + adminAlt: "EnCoach", + adminAltLong: "EnCoach — أطلق إمكاناتك مع منصّة مدعومة بالذكاء الاصطناعي", + toggleSidebar: "إظهار/إخفاء الشريط الجانبي", + }, + notifications: { + title: "الإشعارات", + empty: "ليست لديك إشعارات جديدة.", + ariaLabel: "الإشعارات", + }, + theme: { + title: "المظهر", + light: "فاتح", + dark: "داكن", + system: "النظام", + toggleLabel: "تبديل المظهر", + }, + language: { + change: "تغيير اللغة", + label: "اللغة", + }, + roles: { + student: "طالب", + teacher: "معلّم", + admin: "مسؤول", + developer: "مطوّر", + corporate: "شركة", + mastercorporate: "شركة رئيسية", + agent: "وكيل", + }, + dashboard: { + greetingFallback: "الطالب", + }, + studentDash: { + welcome: "مرحباً بعودتك، {{name}}!", + subtitle: "إليك نظرة عامة على تقدّمك الدراسي.", + enrolledCourses: "الدورات المسجّلة", + overallProgress: "التقدم العام", + averageGrade: "متوسط الدرجات", + totalChapters: "إجمالي الفصول", + myCourses: "دوراتي", + noEnrolledCourses: "لم تسجّل في أي دورة بعد.", + chaptersMaterials: "{{chapters}} فصل · {{materials}} مادة", + quickActions: "إجراءات سريعة", + enrollToStart: "سجّل في دورة للبدء.", + continue: "متابعة", + start: "ابدأ", + chaptersDone: "{{done}}/{{total}} فصل مكتمل", + recentGrades: "آخر الدرجات", + noGradesYet: "لا توجد درجات بعد.", + }, + teacherDash: { + title: "لوحة المعلّم", + subtitle: "نظرة عامة على نشاطك التدريسي.", + activeCourses: "الدورات النشطة", + totalStudents: "إجمالي الطلاب", + pendingGrading: "تقييمات قيد الانتظار", + avgPassRate: "متوسط نسبة النجاح", + myCourses: "دوراتي", + colCourse: "الدورة", + colStudents: "الطلاب", + colStatus: "الحالة", + recentActivity: "النشاط الأخير", + submitted: "تم الإرسال {{when}}", + pending: "قيد الانتظار", + }, + adminDash: { + title: "لوحة الإدارة", + subtitle: "نظرة عامة على المنصّة والمؤشرات الرئيسية.", + addStudent: "إضافة طالب", + newCourse: "دورة جديدة", + totalStudents: "إجمالي الطلاب", + activeCourses: "الدورات النشطة", + teachers: "المعلمون", + activeBatches: "الدفعات النشطة", + openTickets: "تذاكر مفتوحة", + revenue: "الإيرادات", + departments: "الأقسام", + classrooms: "الفصول", + subjects: "المواد", + resources: "الموارد", + coursesOverview: "نظرة عامة على الدورات", + batchCapacity: "سعة الدفعات", + noCourseData: "لا توجد بيانات دورات متاحة.", + noBatchData: "لا توجد بيانات دفعات متاحة.", + quickSummary: "ملخّص سريع", + colModule: "الوحدة", + colCount: "العدد", + colStatus: "الحالة", + exams: "الاختبارات", + examSessionsCount: "{{n}} جلسة", + assignments: "الواجبات", + acrossCourses: "عبر جميع الدورات", + supportTickets: "تذاكر الدعم", + ticketsOpenCount: "{{n}} مفتوحة", + payments: "المدفوعات", + revenueTotal: "{{amount}} إجمالاً", + chartCapacity: "السعة", + chartEnrolled: "المسجّلون", + }, + examPopup: { + title: "اختبارات قادمة ({{n}})", + activeNow: "متاح الآن", + from: "من:", + to: "إلى:", + startExam: "ابدأ الاختبار", + notAvailableYet: "غير متاح بعد", + willBeAvailable: "سيتاح الاختبار عندما يصبح نشطاً", + notActive: "الاختبار غير نشط حالياً", + }, + errors: { + somethingWrong: "حدث خطأ ما", + unexpectedDescription: "حدث خطأ غير متوقع. يرجى تحديث الصفحة والمحاولة مرة أخرى.", + errorDetails: "تفاصيل الخطأ", + refreshPage: "تحديث الصفحة", + notFoundCode: "404", + notFoundMessage: "عذراً! الصفحة غير موجودة", + returnHome: "العودة إلى الرئيسية", + }, + ai: { + assistantLabel: "مساعد الذكاء الاصطناعي", + assistantTitle: "مساعد EnCoach الذكي", + placeholder: "اسأل عن أي شيء…", + send: "إرسال", + thinking: "يفكّر…", + emptyLine1: "اسألني عن أي شيء يخص المنصّة،", + emptyLine2: "أو اختر إجراءً سريعاً من الأعلى.", + fallbackReply: "عذراً، تعذّر الوصول إلى المساعد. يرجى المحاولة مرة أخرى بعد قليل.", + replyErrorTitle: "تعذّر الحصول على ردّ", + replyErrorDesc: "حدث خطأ ما. يرجى المحاولة مرة أخرى.", + quickHealth: "ملخّص حالة المنصّة", + quickDropouts: "عرض مخاطر التسرّب", + quickCompare: "مقارنة أداء الدورات", + quickBatch: "اقتراحات لتحسين الدفعات", + tipLabel: "نصيحة الذكاء الاصطناعي", + insightLabel: "رؤية الذكاء الاصطناعي", + recommendationLabel: "توصية الذكاء الاصطناعي", + categoryTipLabel: "نصيحة {{category}} من الذكاء الاصطناعي", + loadingTip: "جارٍ تحميل النصيحة…", + loadingInsights: "جارٍ تحميل الرؤى…", + loadingAlerts: "جارٍ تحميل التنبيهات…", + couldNotLoadTip: "تعذّر تحميل النصيحة.", + couldNotLoadInsights: "تعذّر تحميل الرؤى.", + couldNotLoadAlerts: "تعذّر تحميل التنبيهات.", + noTipAvailable: "لا توجد نصيحة متاحة.", + noTipForContext: "لا توجد نصيحة لهذا السياق بعد.", + noInsightsAvailable: "لا توجد رؤى متاحة لهذا العرض.", + noAlertsRightNow: "لا توجد تنبيهات ذكاء اصطناعي حالياً.", + alertsUnavailable: "التنبيهات غير متاحة", + insightsUnavailable: "الرؤى غير متاحة", + platformInsightsTitle: "رؤى المنصّة من الذكاء الاصطناعي", }, privacy: { title: "مركز الخصوصية", @@ -74,12 +337,6 @@ const ar: Translations = { commentRequired: "من فضلك أخبرنا بالخطأ.", submit: "إرسال الملاحظات", }, - theme: { - title: "المظهر", - light: "فاتح", - dark: "داكن", - system: "النظام", - }, }; export default ar; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 0e66845..0c08350 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -3,14 +3,33 @@ * We deliberately annotate the literal with `Translations` (not `as const`) * so sibling locale files can widen their string values without fighting * literal-string type mismatches. + * + * Adding a new key: add it here FIRST (English is the source of truth), then + * mirror it in `ar.ts`. The `Translations` interface is intentionally + * permissive (`Record` per group) so we don't have to update + * a giant type every time we add a string. */ export interface Translations { common: Record; auth: Record; nav: Record; + sidebarGroup: Record; + breadcrumb: Record; + userMenu: Record; + chrome: Record; + notifications: Record; + theme: Record; + language: Record; + roles: Record; + dashboard: Record; + studentDash: Record; + teacherDash: Record; + adminDash: Record; + examPopup: Record; + errors: Record; + ai: Record; privacy: Record; feedback: Record; - theme: Record; } const en: Translations = { @@ -32,30 +51,293 @@ const en: Translations = { status: "Status", settings: "Settings", signOut: "Sign out", + viewAll: "View All", + dismiss: "Dismiss", + pending: "Pending", + active: "Active", + inactive: "Inactive", + available: "Available", + unavailable: "Unavailable", + name: "Name", + email: "Email", + user: "User", + home: "Home", }, auth: { signIn: "Sign in", signInTitle: "Sign in to EnCoach", + welcomeBack: "Welcome back", + signInDescription: "Sign in to your account to continue", email: "Email", + emailPlaceholder: "you@example.com", password: "Password", + rememberMe: "Remember me", forgotPassword: "Forgot password?", needAccount: "Don't have an account?", signUp: "Sign up", invalidCredentials: "Invalid email or password", + missingCredentials: "Please enter email and password", + loginFailedTitle: "Login Failed", + errorTitle: "Error", }, nav: { + adminDashboard: "Admin Dashboard", + platformDashboard: "Platform Dashboard", dashboard: "Dashboard", courses: "Courses", + myCourses: "My Courses", students: "Students", teachers: "Teachers", - exams: "Exams", + batches: "Batches", + timetable: "Timetable", reports: "Reports", - settings: "Settings", - profile: "Profile", - privacy: "Privacy Center", + assignments: "Assignments", + examsList: "Exams List", + examStructures: "Exam Structures", + rubrics: "Rubrics", + generation: "Generation", reviewQueue: "Review Queue", aiPrompts: "AI Prompts", aiFeedback: "AI Feedback", + approvalWorkflows: "Approval Workflows", + taxonomy: "Taxonomy", + resources: "Resources", + resourceLibrary: "Resource Library", + academicYears: "Academic Years", + departments: "Departments", + admissions: "Admissions", + admissionRegister: "Admission Register", + examSessions: "Exam Sessions", + marksheets: "Marksheets", + studentLeave: "Student Leave", + fees: "Fees & Payments", + lessons: "Lessons", + gradebook: "Gradebook", + studentProgress: "Student Progress", + library: "Library", + activities: "Activities", + facilities: "Facilities", + users: "Users", + entities: "Entities", + classrooms: "Classrooms", + userRoles: "User Roles", + rolesPermissions: "Roles & Permissions", + authorityMatrix: "Authority Matrix", + studentPerformance: "Student Performance", + statsCorporate: "Stats Corporate", + record: "Record", + vocabulary: "Vocabulary", + grammar: "Grammar", + faqManager: "FAQ Manager", + notificationRules: "Notification Rules", + approvalConfig: "Approval Config", + paymentRecord: "Payment Record", + tickets: "Tickets", + settings: "Settings", + profile: "Profile", + privacy: "Privacy Center", + subjectRegistration: "Subject Registration", + mySubjects: "My Subjects", + grades: "Grades", + attendance: "Attendance", + myJourney: "My Journey", + discussions: "Discussions", + messages: "Messages", + announcements: "Announcements", + }, + sidebarGroup: { + overview: "Overview", + lms: "LMS", + adaptiveLearning: "Adaptive Learning", + institutional: "Institutional", + academic: "Academic", + management: "Management", + reports: "Reports", + configuration: "Configuration", + training: "Training", + support: "Support", + learning: "Learning", + progress: "Progress", + communication: "Communication", + account: "Account", + teaching: "Teaching", + }, + breadcrumb: { + home: "Home", + admin: "Admin", + dashboard: "Dashboard", + platform: "Platform", + exam: "Exam", + review: "Review", + ai: "AI", + feedback: "Feedback", + }, + userMenu: { + profile: "Profile", + settings: "Settings", + logout: "Logout", + userFallback: "User", + }, + chrome: { + needHelp: "Need help?", + ticketsTooltip: "Support tickets", + aiSearchPlaceholder: "Ask anything... e.g. 'Show students with low attendance'", + aiSearching: "AI is searching…", + aiRelatedQueries: "Related queries", + aiNoResults: "No results for \"{{q}}\". Try a different question or keyword.", + aiSearchFailedTitle: "Search failed", + aiSearchFailedDesc: "Could not complete AI search.", + portalSuffix: "Portal", + adminAlt: "EnCoach", + adminAltLong: "EnCoach — Unlock your potential with AI powered platform", + toggleSidebar: "Toggle sidebar", + }, + notifications: { + title: "Notifications", + empty: "You're all caught up.", + ariaLabel: "Notifications", + }, + theme: { + title: "Theme", + light: "Light", + dark: "Dark", + system: "System", + toggleLabel: "Toggle theme", + }, + language: { + change: "Change language", + label: "Language", + }, + roles: { + student: "student", + teacher: "teacher", + admin: "admin", + developer: "developer", + corporate: "corporate", + mastercorporate: "master corporate", + agent: "agent", + }, + dashboard: { + greetingFallback: "Student", + }, + studentDash: { + welcome: "Welcome back, {{name}}!", + subtitle: "Here's an overview of your learning progress.", + enrolledCourses: "Enrolled Courses", + overallProgress: "Overall Progress", + averageGrade: "Average Grade", + totalChapters: "Total Chapters", + myCourses: "My Courses", + noEnrolledCourses: "No enrolled courses yet.", + chaptersMaterials: "{{chapters}} chapters · {{materials}} materials", + quickActions: "Quick Actions", + enrollToStart: "Enroll in a course to get started.", + continue: "Continue", + start: "Start", + chaptersDone: "{{done}}/{{total}} chapters done", + recentGrades: "Recent Grades", + noGradesYet: "No grades yet.", + }, + teacherDash: { + title: "Teacher Dashboard", + subtitle: "Overview of your teaching activities.", + activeCourses: "Active Courses", + totalStudents: "Total Students", + pendingGrading: "Pending Grading", + avgPassRate: "Avg. Pass Rate", + myCourses: "My Courses", + colCourse: "Course", + colStudents: "Students", + colStatus: "Status", + recentActivity: "Recent Activity", + submitted: "Submitted {{when}}", + pending: "Pending", + }, + adminDash: { + title: "Admin Dashboard", + subtitle: "Platform overview and key metrics.", + addStudent: "Add Student", + newCourse: "New Course", + totalStudents: "Total Students", + activeCourses: "Active Courses", + teachers: "Teachers", + activeBatches: "Active Batches", + openTickets: "Open Tickets", + revenue: "Revenue", + departments: "Departments", + classrooms: "Classrooms", + subjects: "Subjects", + resources: "Resources", + coursesOverview: "Courses Overview", + batchCapacity: "Batch Capacity", + noCourseData: "No course data available.", + noBatchData: "No batch data available.", + quickSummary: "Quick Summary", + colModule: "Module", + colCount: "Count", + colStatus: "Status", + exams: "Exams", + examSessionsCount: "{{n}} sessions", + assignments: "Assignments", + acrossCourses: "across courses", + supportTickets: "Support Tickets", + ticketsOpenCount: "{{n}} open", + payments: "Payments", + revenueTotal: "{{amount}} total", + chartCapacity: "Capacity", + chartEnrolled: "Enrolled", + }, + examPopup: { + title: "Upcoming Exams ({{n}})", + activeNow: "Active Now", + from: "From:", + to: "To:", + startExam: "Start Exam", + notAvailableYet: "Not Available Yet", + willBeAvailable: "Exam will be available once it becomes active", + notActive: "Exam is not currently active", + }, + errors: { + somethingWrong: "Something went wrong", + unexpectedDescription: "An unexpected error occurred. Please try refreshing the page.", + errorDetails: "Error details", + refreshPage: "Refresh Page", + notFoundCode: "404", + notFoundMessage: "Oops! Page not found", + returnHome: "Return to Home", + }, + ai: { + assistantLabel: "AI Assistant", + assistantTitle: "EnCoach AI Assistant", + placeholder: "Ask anything...", + send: "Send", + thinking: "Thinking…", + emptyLine1: "Ask me anything about the platform,", + emptyLine2: "or click a quick action above.", + fallbackReply: "Sorry, I could not reach the assistant. Please try again in a moment.", + replyErrorTitle: "Could not get a reply", + replyErrorDesc: "Something went wrong. Try again.", + quickHealth: "Platform health summary", + quickDropouts: "Show dropout risks", + quickCompare: "Compare course performance", + quickBatch: "Suggest batch improvements", + tipLabel: "AI Tip", + insightLabel: "AI Insight", + recommendationLabel: "AI Recommendation", + categoryTipLabel: "AI {{category}} Tip", + loadingTip: "Loading AI tip…", + loadingInsights: "Loading insights…", + loadingAlerts: "Loading alerts…", + couldNotLoadTip: "Could not load tip.", + couldNotLoadInsights: "Could not load insights.", + couldNotLoadAlerts: "Could not load alerts.", + noTipAvailable: "No tip available.", + noTipForContext: "No tip for this context yet.", + noInsightsAvailable: "No insights available for this view.", + noAlertsRightNow: "No AI alerts right now.", + alertsUnavailable: "Alerts unavailable", + insightsUnavailable: "Insights unavailable", + platformInsightsTitle: "AI Platform Insights", }, privacy: { title: "Privacy Center", @@ -86,12 +368,6 @@ const en: Translations = { commentRequired: "Please tell us what was wrong.", submit: "Submit feedback", }, - theme: { - title: "Theme", - light: "Light", - dark: "Dark", - system: "System", - }, }; export default en; diff --git a/src/index.css b/src/index.css index f316264..a6a9bc9 100644 --- a/src/index.css +++ b/src/index.css @@ -134,3 +134,103 @@ font-family: 'JetBrains Mono', monospace; } } + +/* ------------------------------------------------------------------------- + * RTL / Arabic support + * + * `tailwindcss-rtl` handles the bulk of the work: physical margin / padding / + * position / border / rounded / text-align / space-x utilities are mirrored + * automatically when `html[dir="rtl"]` is active. + * + * Below we cover the things Tailwind can't: the webfont (Inter has weak + * Arabic glyphs, so swap in Cairo), recharts tooltips/axes which have their + * own DOM, and a few icons that should NOT mirror (arrows used as pure + * visual affordance e.g. ChevronRight in a dropdown). + * ------------------------------------------------------------------------- */ + +html[dir="rtl"] body, +html[dir="rtl"] h1, +html[dir="rtl"] h2, +html[dir="rtl"] h3, +html[dir="rtl"] h4, +html[dir="rtl"] h5, +html[dir="rtl"] h6, +html[dir="rtl"] input, +html[dir="rtl"] textarea, +html[dir="rtl"] button, +html[dir="rtl"] select { + font-family: 'Cairo', 'Inter', system-ui, sans-serif; +} + +html[dir="rtl"] code, +html[dir="rtl"] pre { + font-family: 'JetBrains Mono', monospace; +} + +html[dir="rtl"] .recharts-wrapper, +html[dir="rtl"] .recharts-legend-wrapper { + direction: ltr; +} + +/* Breadcrumb and submenu chevrons: always pointing "forward" in reading + direction. Lucide's ChevronRight is ">" which is forward in LTR, but in + RTL the same arrow must become "<". We flip via CSS so no component has + to know about direction. */ +html[dir="rtl"] nav[aria-label="breadcrumb"] li[role="presentation"] > svg, +html[dir="rtl"] [data-radix-menu-content] [role="menuitem"] > svg:last-child, +html[dir="rtl"] [data-radix-popper-content-wrapper] [role="menuitem"] > svg:last-child { + transform: rotate(180deg); +} + +/* Numbers, percentages, emails, URLs, dates should stay LTR-isolated even + when embedded in Arabic paragraphs, otherwise slashes / percent signs / + dashes drift to the wrong side and the value becomes unreadable. + already gives us this per-element; the helper classes below are + for places where adding a wrapper element is awkward. */ +.ltr-nums, +.dir-ltr { + direction: ltr; + unicode-bidi: isolate; +} + +/* Pure-number cells & badges align to the end (right in LTR, left in RTL) + but the digits themselves still read left-to-right. */ +html[dir="rtl"] .numeric { + text-align: end; + direction: ltr; + unicode-bidi: isolate; +} + +/* Some lucide icons are pure affordances (play, external-link, send) and + should NOT be mirrored even if tailwindcss-rtl would otherwise flip the + button that contains them. Authors opt-in with data-no-flip. */ +html[dir="rtl"] [data-no-flip] { + transform: none !important; +} + +/* Keep code snippets, identifiers, and file paths readable in RTL. */ +html[dir="rtl"] code, +html[dir="rtl"] pre, +html[dir="rtl"] kbd, +html[dir="rtl"] samp, +html[dir="rtl"] [data-monospace] { + direction: ltr; + unicode-bidi: isolate; + text-align: start; +} + +/* Scrollbars inside horizontally-scrollable tables already flip naturally + in RTL, but the shadow Radix portals (dropdowns, tooltips, dialogs) use + `right-0` / `left-0` positioning computed in JS. For safety, make sure + they inherit the document direction. */ +html[dir="rtl"] [data-radix-popper-content-wrapper] { + direction: rtl; +} + +/* Inputs with dir="ltr" (email, password, URL) override but keep the + placeholder aligned to the visual start (right in RTL, left in LTR). */ +html[dir="rtl"] input[dir="ltr"]::placeholder, +html[dir="rtl"] textarea[dir="ltr"]::placeholder { + text-align: right; +} + diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 1fcf691..bdce30b 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -130,12 +130,32 @@ function refreshOnce(): Promise { return refreshPromise; } +function getCurrentLanguage(): string { + try { + const stored = localStorage.getItem("encoach-lang"); + if (stored) return stored; + } catch { + // localStorage unavailable (SSR, sandboxing, etc.) + } + if (typeof navigator !== "undefined" && navigator.language) { + return navigator.language.split("-")[0]; + } + return "en"; +} + function buildHeaders(extra?: Record, withJson = true): Record { const headers: Record = { ...extra }; if (withJson) headers["Content-Type"] = headers["Content-Type"] || "application/json"; const token = getAccessToken(); if (token) headers["Authorization"] = `Bearer ${token}`; + + // Tell the backend which UI language the user is on so AI-generated content + // (coaching tips, insights, alerts, narratives) can be returned in the same + // language instead of always defaulting to English. + if (!headers["Accept-Language"]) { + headers["Accept-Language"] = getCurrentLanguage(); + } return headers; } diff --git a/src/pages/ExamStructuresPage.tsx b/src/pages/ExamStructuresPage.tsx index 32a1faf..98cdb47 100644 --- a/src/pages/ExamStructuresPage.tsx +++ b/src/pages/ExamStructuresPage.tsx @@ -1355,11 +1355,11 @@ export default function ExamStructuresPage() { const examType = getExamTypeBadge(s); return ( setEditTarget(s)}> - -
- - {s.name} - + +
+ + {s.name} +
-
-
- +
+
+
{s.entity_name && Entity: {s.entity_name}} {s.industry && Industry: {s.industry}} @@ -1382,9 +1382,9 @@ export default function ExamStructuresPage() { {(Array.isArray(s.modules) ? s.modules : []).map((m) => ( {m} ))} -
-
-
+
+ + ); })}
diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx index 29902c0..bf0050f 100644 --- a/src/pages/ForgotPassword.tsx +++ b/src/pages/ForgotPassword.tsx @@ -48,7 +48,7 @@ export default function ForgotPassword() { ) : null}
- Back to sign in + Back to sign in
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 792d7a8..7794816 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -10,6 +11,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { useToast } from "@/hooks/use-toast"; import { ApiError } from "@/lib/api-client"; import type { UserRole } from "@/types/auth"; +import { LanguageToggle } from "@/components/LanguageToggle"; /** Keep in sync with `ProtectedRoute` post-login targets */ function getRoleDashboard(role: string): string { @@ -48,11 +50,16 @@ export default function Login() { const navigate = useNavigate(); const { login } = useAuth(); const { toast } = useToast(); + const { t } = useTranslation(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!email || !password) { - toast({ title: "Error", description: "Please enter email and password", variant: "destructive" }); + toast({ + title: t("auth.errorTitle"), + description: t("auth.missingCredentials"), + variant: "destructive", + }); return; } @@ -61,7 +68,11 @@ export default function Login() { const user = await login(email, password); navigate(getRoleDashboard(user.user_type)); } catch (err: unknown) { - toast({ title: "Login Failed", description: loginErrorMessage(err), variant: "destructive" }); + toast({ + title: t("auth.loginFailedTitle"), + description: loginErrorMessage(err), + variant: "destructive", + }); } finally { setLoading(false); } @@ -69,6 +80,9 @@ export default function Login() { return (
+
+ +
- Welcome back - Sign in to your account to continue + {t("auth.welcomeBack")} + {t("auth.signInDescription")}
- + setEmail(e.target.value)} disabled={loading} + dir="ltr" />
- +
setPassword(e.target.value)} disabled={loading} + dir="ltr" /> @@ -119,19 +136,25 @@ export default function Login() {
- +
- Forgot password? + + {t("auth.forgotPassword")} +

- Don't have an account?{" "} - Sign up + {t("auth.needAccount")}{" "} + + {t("auth.signUp")} +

diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 7bc2234..fcd571e 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,8 +1,10 @@ import { useLocation } from "react-router-dom"; import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; const NotFound = () => { const location = useLocation(); + const { t } = useTranslation(); useEffect(() => { console.error("404 Error: User attempted to access non-existent route:", location.pathname); @@ -11,10 +13,10 @@ const NotFound = () => { return (
-

404

-

Oops! Page not found

+

{t("errors.notFoundCode")}

+

{t("errors.notFoundMessage")}

- Return to Home + {t("errors.returnHome")}
diff --git a/src/pages/admin/AdminBatchDetail.tsx b/src/pages/admin/AdminBatchDetail.tsx index 8a2714f..71975bd 100644 --- a/src/pages/admin/AdminBatchDetail.tsx +++ b/src/pages/admin/AdminBatchDetail.tsx @@ -35,7 +35,7 @@ export default function AdminBatchDetail() { return (
- +

{batch.name}

{batch.course_name} · {batch.teacher_name}

diff --git a/src/pages/admin/AdminLmsDashboard.tsx b/src/pages/admin/AdminLmsDashboard.tsx index e09eb5b..7ad4512 100644 --- a/src/pages/admin/AdminLmsDashboard.tsx +++ b/src/pages/admin/AdminLmsDashboard.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Users, BookOpen, GraduationCap, Layers, Ticket, DollarSign, Plus, BarChart3, FolderOpen } from "lucide-react"; import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { useCourses, useBatches, useStudents, useTeachers } from "@/hooks/queries"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api-client"; @@ -32,6 +33,7 @@ interface DashboardStats { } export default function AdminLmsDashboard() { + const { t } = useTranslation(); const { data: coursesData, isLoading: lc } = useCourses(); const { data: studentsData, isLoading: ls } = useStudents({ size: 500 }); const { data: teachersData, isLoading: lt } = useTeachers({ size: 500 }); @@ -58,12 +60,12 @@ export default function AdminLmsDashboard() { const openTickets = dbStats?.open_tickets ?? 0; const statCards = [ - { label: "Total Students", value: String(students.length), icon: Users, color: "text-primary" }, - { label: "Active Courses", value: `${courses.filter(c => c.status === "active").length} / ${courses.length}`, icon: BookOpen, color: "text-info" }, - { label: "Teachers", value: String(teachers.length), icon: GraduationCap, color: "text-success" }, - { label: "Active Batches", value: `${batches.filter(b => b.status === "active").length} / ${batches.length}`, icon: Layers, color: "text-warning" }, - { label: "Open Tickets", value: String(openTickets), icon: Ticket, color: "text-destructive" }, - { label: "Revenue", value: `$${revenue.toLocaleString()}`, icon: DollarSign, color: "text-success" }, + { label: t("adminDash.totalStudents"), value: String(students.length), icon: Users, color: "text-primary" }, + { label: t("adminDash.activeCourses"), value: `${courses.filter(c => c.status === "active").length} / ${courses.length}`, icon: BookOpen, color: "text-info" }, + { label: t("adminDash.teachers"), value: String(teachers.length), icon: GraduationCap, color: "text-success" }, + { label: t("adminDash.activeBatches"), value: `${batches.filter(b => b.status === "active").length} / ${batches.length}`, icon: Layers, color: "text-warning" }, + { label: t("adminDash.openTickets"), value: String(openTickets), icon: Ticket, color: "text-destructive" }, + { label: t("adminDash.revenue"), value: `$${revenue.toLocaleString()}`, icon: DollarSign, color: "text-success" }, ]; const courseChartData = courses.map(c => { @@ -82,12 +84,16 @@ export default function AdminLmsDashboard() {
-

Admin Dashboard

-

Platform overview and key metrics.

+

{t("adminDash.title")}

+

{t("adminDash.subtitle")}

- - + +
@@ -99,7 +105,7 @@ export default function AdminLmsDashboard() { -

{s.value}

+

{s.value}

{s.label}

@@ -108,16 +114,16 @@ export default function AdminLmsDashboard() {
{[ - { label: "Departments", value: dbStats?.total_departments ?? 0, icon: FolderOpen }, - { label: "Classrooms", value: dbStats?.total_classrooms ?? 0, icon: BarChart3 }, - { label: "Subjects", value: dbStats?.total_subjects ?? 0, icon: BookOpen }, - { label: "Resources", value: dbStats?.total_resources ?? 0, icon: Layers }, + { label: t("adminDash.departments"), value: dbStats?.total_departments ?? 0, icon: FolderOpen }, + { label: t("adminDash.classrooms"), value: dbStats?.total_classrooms ?? 0, icon: BarChart3 }, + { label: t("adminDash.subjects"), value: dbStats?.total_subjects ?? 0, icon: BookOpen }, + { label: t("adminDash.resources"), value: dbStats?.total_resources ?? 0, icon: Layers }, ].map(s => ( - -
-

{s.value}

+ +
+

{s.value}

{s.label}

@@ -127,7 +133,7 @@ export default function AdminLmsDashboard() {
- Courses Overview + {t("adminDash.coursesOverview")} {courseChartData.length > 0 ? ( @@ -136,18 +142,18 @@ export default function AdminLmsDashboard() { - - + + ) : ( -

No course data available.

+

{t("adminDash.noCourseData")}

)}
- Batch Capacity + {t("adminDash.batchCapacity")} {batchChartData.length > 0 ? ( @@ -156,11 +162,11 @@ export default function AdminLmsDashboard() { - + ) : ( -

No batch data available.

+

{t("adminDash.noBatchData")}

)}
@@ -168,31 +174,43 @@ export default function AdminLmsDashboard() { - Quick Summary + {t("adminDash.quickSummary")} - ModuleCountStatus + + + {t("adminDash.colModule")} + {t("adminDash.colCount")} + {t("adminDash.colStatus")} + + - Exams - {dbStats?.total_exams ?? 0} - {dbStats?.total_exam_sessions ?? 0} sessions + {t("adminDash.exams")} + {dbStats?.total_exams ?? 0} + + {t("adminDash.examSessionsCount", { n: dbStats?.total_exam_sessions ?? 0 })} + - Assignments - {dbStats?.total_assignments ?? 0} - across courses + {t("adminDash.assignments")} + {dbStats?.total_assignments ?? 0} + {t("adminDash.acrossCourses")} - Support Tickets - {dbStats?.total_tickets ?? 0} - {openTickets} open + {t("adminDash.supportTickets")} + {dbStats?.total_tickets ?? 0} + + {t("adminDash.ticketsOpenCount", { n: openTickets })} + - Payments - {dbStats?.total_payments ?? 0} - ${revenue.toLocaleString()} total + {t("adminDash.payments")} + {dbStats?.total_payments ?? 0} + + {t("adminDash.revenueTotal", { amount: `$${revenue.toLocaleString()}` })} +
diff --git a/src/pages/admin/AdmissionDetail.tsx b/src/pages/admin/AdmissionDetail.tsx index eb16fdf..9c52d27 100644 --- a/src/pages/admin/AdmissionDetail.tsx +++ b/src/pages/admin/AdmissionDetail.tsx @@ -57,7 +57,7 @@ export default function AdmissionDetail() {

{admission.first_name} {admission.last_name}

diff --git a/src/pages/admin/ApprovalWorkflowConfig.tsx b/src/pages/admin/ApprovalWorkflowConfig.tsx index f93434a..10d65ff 100644 --- a/src/pages/admin/ApprovalWorkflowConfig.tsx +++ b/src/pages/admin/ApprovalWorkflowConfig.tsx @@ -216,7 +216,7 @@ export default function ApprovalWorkflowConfig() { {step.order} {step.approver_name}
- {i < wf.steps.length - 1 && } + {i < wf.steps.length - 1 && }
))}
diff --git a/src/pages/admin/ExamReviewDetail.tsx b/src/pages/admin/ExamReviewDetail.tsx index 34dfa0a..a865207 100644 --- a/src/pages/admin/ExamReviewDetail.tsx +++ b/src/pages/admin/ExamReviewDetail.tsx @@ -155,7 +155,7 @@ export default function ExamReviewDetail() {

{exam.title}

diff --git a/src/pages/student/DiagnosticTest.tsx b/src/pages/student/DiagnosticTest.tsx index 0ae3f7a..7dcdf11 100644 --- a/src/pages/student/DiagnosticTest.tsx +++ b/src/pages/student/DiagnosticTest.tsx @@ -117,7 +117,7 @@ export default function DiagnosticTest() { )} diff --git a/src/pages/student/LearningPlan.tsx b/src/pages/student/LearningPlan.tsx index 2b1dbd7..4bab24c 100644 --- a/src/pages/student/LearningPlan.tsx +++ b/src/pages/student/LearningPlan.tsx @@ -106,7 +106,7 @@ export default function LearningPlanPage() {
{(item.status === "available" || item.status === "in_progress") && ( )} {item.status === "completed" && ( diff --git a/src/pages/student/ProficiencyProfile.tsx b/src/pages/student/ProficiencyProfile.tsx index 23c56c9..db765fa 100644 --- a/src/pages/student/ProficiencyProfile.tsx +++ b/src/pages/student/ProficiencyProfile.tsx @@ -41,7 +41,7 @@ export default function ProficiencyProfile() {
- +
diff --git a/src/pages/student/StudentCourseDetail.tsx b/src/pages/student/StudentCourseDetail.tsx index 8cae969..f51193e 100644 --- a/src/pages/student/StudentCourseDetail.tsx +++ b/src/pages/student/StudentCourseDetail.tsx @@ -28,7 +28,7 @@ export default function StudentCourseDetail() { return (
- +

{course.title}

{course.code} · {chapters.length} chapters

diff --git a/src/pages/student/StudentDashboard.tsx b/src/pages/student/StudentDashboard.tsx index e052600..d92f4c1 100644 --- a/src/pages/student/StudentDashboard.tsx +++ b/src/pages/student/StudentDashboard.tsx @@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { BookOpen, ClipboardList, BarChart3, Calendar, ArrowRight, Play } from "lucide-react"; import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { useMyEnrolledCourses, useGrades } from "@/hooks/queries"; import { useAuth } from "@/contexts/AuthContext"; import AiStudyCoach from "@/components/ai/AiStudyCoach"; @@ -11,6 +12,7 @@ import AiTipBanner from "@/components/ai/AiTipBanner"; export default function StudentDashboard() { const { user } = useAuth(); + const { t } = useTranslation(); const { data: enrolledData, isLoading: lc } = useMyEnrolledCourses(); const { data: gradesData, isLoading: lg } = useGrades(); const myCourses = enrolledData?.items ?? []; @@ -22,20 +24,20 @@ export default function StudentDashboard() { const avgGrade = gradeRecords.length > 0 ? Math.round(gradeRecords.reduce((s, g) => s + (g.grade / g.max_grade) * 100, 0) / gradeRecords.length) : 0; - const firstName = user?.name?.split(" ")[0] || "Student"; + const firstName = user?.name?.split(" ")[0] || t("dashboard.greetingFallback"); const stats = [ - { label: "Enrolled Courses", value: String(myCourses.length), icon: BookOpen, color: "text-primary" }, - { label: "Overall Progress", value: `${avgProgress}%`, icon: ClipboardList, color: "text-warning" }, - { label: "Average Grade", value: gradeRecords.length > 0 ? `${avgGrade}%` : "N/A", icon: BarChart3, color: "text-success" }, - { label: "Total Chapters", value: String(myCourses.reduce((s, c) => s + c.chapter_count, 0)), icon: Calendar, color: "text-info" }, + { label: t("studentDash.enrolledCourses"), value: String(myCourses.length), icon: BookOpen, color: "text-primary" }, + { label: t("studentDash.overallProgress"), value: `${avgProgress}%`, icon: ClipboardList, color: "text-warning" }, + { label: t("studentDash.averageGrade"), value: gradeRecords.length > 0 ? `${avgGrade}%` : "N/A", icon: BarChart3, color: "text-success" }, + { label: t("studentDash.totalChapters"), value: String(myCourses.reduce((s, c) => s + c.chapter_count, 0)), icon: Calendar, color: "text-info" }, ]; return (
-

Welcome back, {firstName}!

-

Here's an overview of your learning progress.

+

{t("studentDash.welcome", { name: firstName })}

+

{t("studentDash.subtitle")}

@@ -46,12 +48,12 @@ export default function StudentDashboard() { {stats.map((s) => ( -
-
+
+

{s.label}

-

{s.value}

+

{s.value}

- +
@@ -61,22 +63,28 @@ export default function StudentDashboard() {
- My Courses - + {t("studentDash.myCourses")} + {myCourses.length === 0 ? ( -

No enrolled courses yet.

+

{t("studentDash.noEnrolledCourses")}

) : myCourses.map((c) => (

{c.title || c.name}

-

{c.chapter_count} chapters · {c.total_materials} materials

+

+ {t("studentDash.chaptersMaterials", { chapters: c.chapter_count, materials: c.total_materials })} +

- {c.progress}% + {c.progress}%
@@ -87,35 +95,45 @@ export default function StudentDashboard() {
- Quick Actions + {t("studentDash.quickActions")} {myCourses.slice(0, 3).map(c => (
-

{c.progress > 0 ? "Continue" : "Start"} {c.title || c.name}

-

{c.completed_chapters}/{c.chapter_count} chapters done

+

+ {c.progress > 0 ? t("studentDash.continue") : t("studentDash.start")} {c.title || c.name} +

+

+ {t("studentDash.chaptersDone", { done: c.completed_chapters, total: c.chapter_count })} +

- {c.progress}% + {c.progress}% ))} - {myCourses.length === 0 &&

Enroll in a course to get started.

} + {myCourses.length === 0 && ( +

{t("studentDash.enrollToStart")}

+ )}
- Recent Grades - + {t("studentDash.recentGrades")} + {recentGrades.length === 0 ? ( -

No grades yet.

+

{t("studentDash.noGradesYet")}

) : recentGrades.map((g) => ( -
-

{g.assignment_title}

{g.course_name}

- {g.grade}/{g.max_grade} +
+

{g.assignment_title}

{g.course_name}

+ {g.grade}/{g.max_grade}
))} diff --git a/src/pages/student/StudentDiscussionBoard.tsx b/src/pages/student/StudentDiscussionBoard.tsx index 5d44c7f..1f886eb 100644 --- a/src/pages/student/StudentDiscussionBoard.tsx +++ b/src/pages/student/StudentDiscussionBoard.tsx @@ -133,7 +133,7 @@ export default function StudentDiscussionBoard() {
diff --git a/src/pages/student/SubjectSelection.tsx b/src/pages/student/SubjectSelection.tsx index c066c69..4b9773d 100644 --- a/src/pages/student/SubjectSelection.tsx +++ b/src/pages/student/SubjectSelection.tsx @@ -58,7 +58,7 @@ export default function SubjectSelection() { View Profile
@@ -66,7 +66,7 @@ export default function SubjectSelection() { <>

Take a diagnostic assessment to discover your proficiency level and get a personalized learning plan.

)} diff --git a/src/pages/teacher/TeacherAssignmentDetail.tsx b/src/pages/teacher/TeacherAssignmentDetail.tsx index 04da86e..caf97d4 100644 --- a/src/pages/teacher/TeacherAssignmentDetail.tsx +++ b/src/pages/teacher/TeacherAssignmentDetail.tsx @@ -32,7 +32,7 @@ export default function TeacherAssignmentDetail() { return (
- +

{assignment.title}

{assignment.entity_name} · Due: {assignment.end_date}

diff --git a/src/pages/teacher/TeacherDashboard.tsx b/src/pages/teacher/TeacherDashboard.tsx index 543076c..303a3b4 100644 --- a/src/pages/teacher/TeacherDashboard.tsx +++ b/src/pages/teacher/TeacherDashboard.tsx @@ -4,11 +4,13 @@ import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { BookOpen, Users, ClipboardList, TrendingUp, ArrowRight } from "lucide-react"; import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { useCourses, useAssignments } from "@/hooks/queries"; import AiInsightsPanel from "@/components/ai/AiInsightsPanel"; import AiTipBanner from "@/components/ai/AiTipBanner"; export default function TeacherDashboard() { + const { t } = useTranslation(); const { data: coursesData, isLoading: lc } = useCourses(); const { data: assignmentsData, isLoading: la } = useAssignments(); const courses = coursesData?.items ?? []; @@ -21,17 +23,17 @@ export default function TeacherDashboard() { const pendingGrading = (submissions as { id: string; studentName: string; submittedAt: string; status: string }[]).filter(s => s.status === "pending"); const stats = [ - { label: "Active Courses", value: String(teacherCourses.filter(c => c.status === "active").length), icon: BookOpen, color: "text-primary" }, - { label: "Total Students", value: "55", icon: Users, color: "text-info" }, - { label: "Pending Grading", value: String(pendingGrading.length), icon: ClipboardList, color: "text-warning" }, - { label: "Avg. Pass Rate", value: "82%", icon: TrendingUp, color: "text-success" }, + { label: t("teacherDash.activeCourses"), value: String(teacherCourses.filter(c => c.status === "active").length), icon: BookOpen, color: "text-primary" }, + { label: t("teacherDash.totalStudents"), value: "55", icon: Users, color: "text-info" }, + { label: t("teacherDash.pendingGrading"), value: String(pendingGrading.length), icon: ClipboardList, color: "text-warning" }, + { label: t("teacherDash.avgPassRate"), value: "82%", icon: TrendingUp, color: "text-success" }, ]; return (
-

Teacher Dashboard

-

Overview of your teaching activities.

+

{t("teacherDash.title")}

+

{t("teacherDash.subtitle")}

@@ -42,9 +44,9 @@ export default function TeacherDashboard() { {stats.map(s => ( -
-

{s.label}

{s.value}

- +
+

{s.label}

{s.value}

+
@@ -54,17 +56,27 @@ export default function TeacherDashboard() {
- My Courses - + {t("teacherDash.myCourses")} + - CourseStudentsStatus + + + {t("teacherDash.colCourse")} + {t("teacherDash.colStudents")} + {t("teacherDash.colStatus")} + + {teacherCourses.map(c => ( - {c.title} - {c.enrolled}/{c.max_capacity} + {c.title} + {c.enrolled}/{c.max_capacity} {c.status} ))} @@ -76,26 +88,35 @@ export default function TeacherDashboard() {
- Pending Grading - + {t("teacherDash.pendingGrading")} + {pendingGrading.slice(0, 4).map(s => (
-

{s.studentName}

Submitted {s.submittedAt}

- Pending +
+

{s.studentName}

+

+ {t("teacherDash.submitted", { when: s.submittedAt })} +

+
+ {t("teacherDash.pending")}
))}
- Recent Activity + {t("teacherDash.recentActivity")} {activityFeed.slice(0, 4).map(a => (
{a.user} {a.action} {a.target} - · {a.time} + · {a.time}
))}
diff --git a/src/pages/teacher/TeacherDiscussionBoard.tsx b/src/pages/teacher/TeacherDiscussionBoard.tsx index ea39410..1f756d3 100644 --- a/src/pages/teacher/TeacherDiscussionBoard.tsx +++ b/src/pages/teacher/TeacherDiscussionBoard.tsx @@ -155,7 +155,7 @@ export default function TeacherDiscussionBoard() {
diff --git a/tailwind.config.ts b/tailwind.config.ts index 5a464bd..40d85d4 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -96,5 +96,12 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [ + require("tailwindcss-animate"), + // Mirrors physical utilities (ml-*, mr-*, pl-*, pr-*, left-*, right-*, + // text-left/right, border-l/r, rounded-l/r, space-x-*) under `dir="rtl"` + // so the whole app flips visually when Arabic is selected, without + // having to rewrite every component to use logical properties. + require("tailwindcss-rtl"), + ], } satisfies Config;