From 3d2c86548d38c534332eafa79ca9067a2f525c21 Mon Sep 17 00:00:00 2001 From: Niro2605 Date: Wed, 2 Sep 2026 15:29:38 +0530 Subject: [PATCH] Add multilingual support and improve SkillPath features --- backend/pnpm-lock.yaml | 2 +- backend/pnpm-workspace.yaml | 2 + backend/src/app.ts | 16 +- backend/src/db/checker-recommendations.ts | 20 + backend/src/db/official-handbook-query.ts | 156 +++- frontend/pnpm-lock.yaml | 2 +- frontend/pnpm-workspace.yaml | 3 + .../components/checker/checker-results.tsx | 506 +++++++++--- frontend/src/components/language-switcher.tsx | 69 ++ frontend/src/components/layout.tsx | 98 ++- frontend/src/lib/i18n.ts | 754 ++++++++++++++++++ frontend/src/pages/careers.tsx | 16 +- frontend/src/pages/chat.tsx | 67 +- frontend/src/pages/checker.tsx | 68 +- frontend/src/pages/course-detail.tsx | 55 +- frontend/src/pages/courses.tsx | 79 +- frontend/src/pages/dashboard.tsx | 52 +- frontend/src/pages/home.tsx | 73 +- frontend/src/pages/login.tsx | 18 +- frontend/src/pages/profile.tsx | 215 +++-- frontend/src/pages/register.tsx | 20 +- frontend/src/pages/reviews.tsx | 72 +- frontend/src/pages/roadmap.tsx | 150 ++-- frontend/src/pages/stories.tsx | 18 +- frontend/src/pages/universities.tsx | 69 +- 25 files changed, 1987 insertions(+), 613 deletions(-) create mode 100644 backend/pnpm-workspace.yaml create mode 100644 frontend/pnpm-workspace.yaml create mode 100644 frontend/src/components/language-switcher.tsx create mode 100644 frontend/src/lib/i18n.ts diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index c6ec401..e63315b 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: false + autoInstallPeers: true excludeLinksFromLockfile: false importers: diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/backend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/backend/src/app.ts b/backend/src/app.ts index a3ae9c9..63f5f58 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -11,7 +11,7 @@ const corsOrigins = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).fi app.use( helmet({ - contentSecurityPolicy: process.env.NODE_ENV === "production" ? undefined : false, + contentSecurityPolicy: false, }), ); app.use( @@ -28,6 +28,20 @@ app.use( }), ); +// Gracefully handle browser DevTools probes +app.use("/.well-known", (_req, res) => { + res.status(204).end(); +}); + +// Redirect browser traffic on port 5000 to the frontend app +app.get("/", (req, res) => { + if (req.accepts("html")) { + res.redirect("http://localhost:5173"); + } else { + res.json({ status: "ok", message: "SkillPath API Server", webApp: "http://localhost:5173" }); + } +}); + app.use("/api", apiRouter); app.use( diff --git a/backend/src/db/checker-recommendations.ts b/backend/src/db/checker-recommendations.ts index 6cdf1e8..0354afc 100644 --- a/backend/src/db/checker-recommendations.ts +++ b/backend/src/db/checker-recommendations.ts @@ -597,6 +597,26 @@ export async function buildCheckerRecommendations( }); } + const sortByNearestZScore = (items: CheckerRecommendation[]) => + [...items].sort((a, b) => { + const cutoffA = a.officialCutoff ?? a.estimatedCenter ?? (a.estimatedMin != null && a.estimatedMax != null ? (a.estimatedMin + a.estimatedMax) / 2 : null); + const cutoffB = b.officialCutoff ?? b.estimatedCenter ?? (b.estimatedMin != null && b.estimatedMax != null ? (b.estimatedMin + b.estimatedMax) / 2 : null); + if (cutoffA == null && cutoffB == null) return 0; + if (cutoffA == null) return 1; + if (cutoffB == null) return -1; + const distA = Math.abs(input.zscore - cutoffA); + const distB = Math.abs(input.zscore - cutoffB); + if (Math.abs(distA - distB) > 0.0001) { + return distA - distB; + } + return cutoffB - cutoffA; + }); + + groups.competitiveOptions = sortByNearestZScore(groups.competitiveOptions); + groups.nearHistoricalRange = sortByNearestZScore(groups.nearHistoricalRange); + groups.strongMatches = sortByNearestZScore(groups.strongMatches); + groups.notEligible = sortByNearestZScore(groups.notEligible); + return { ...checkerResponse(input, mode, groups), message: responseMessage(groups, mode, candidates.length), diff --git a/backend/src/db/official-handbook-query.ts b/backend/src/db/official-handbook-query.ts index 23a1665..1f32b4f 100644 --- a/backend/src/db/official-handbook-query.ts +++ b/backend/src/db/official-handbook-query.ts @@ -168,8 +168,54 @@ function eligibility(zscore: number | undefined, cutoff: number | null): string return "unlikely"; } +export const SECTION9_STREAM_MAPPINGS: Record = { + // Software Engineering + "Software Engineering": ["Physical Science"], + // Electronic & Intelligent Systems Engineering + "Electronic and Intelligent Systems Engineering (New)": ["Physical Science"], + // Information Technology (Moratuwa) + "Information Technology (IT)": ["Physical Science", "Biological Science", "Commerce", "Arts", "Technology"], + // Information Technology & Management + "Information Technology & Management": ["Physical Science", "Biological Science", "Commerce", "Arts", "Technology"], + // Information Systems + "Information Systems": ["Physical Science", "Biological Science", "Commerce", "Arts", "Technology"], + // Computer Science & Technology + "Computer Science & Technology": ["Physical Science"], + // Industrial Information Technology + "Industrial Information Technology": ["Physical Science", "Biological Science", "Technology"], + // Quantity Surveying + "Quantity Surveying": ["Physical Science"], + // Surveying Science + "Surveying Science": ["Physical Science"], + // Facilities Management + "Facilities Management": ["Physical Science"], + // Urban Informatics and Planning + "Urban Informatics and Planning": ["Physical Science"], + // Architecture + "Architecture": ["Physical Science", "Arts", "Technology"], + // Management and Information Technology (MIT) + "Management and Information Technology (MIT)": ["Physical Science"], + "Management and Information Technology (SEUSL)": ["Physical Science", "Biological Science"], + // Information and Communication Technology + "Information and Communication Technology (ICT)": ["Physical Science", "Technology"], + "Information Communication Technology": ["Physical Science", "Technology"], + // Financial Engineering + "Financial Engineering": ["Physical Science"], + // Mineral Resources and Technology + "Mineral Resources and Technology": ["Physical Science"], + // Science and Technology + "Science and Technology": ["Physical Science"], +}; + +export function getCourseEligibleStreams(row: { course_name?: string | null; eligible_streams?: unknown }): string[] { + const fromDb = asStringList(row.eligible_streams); + if (fromDb.length > 0) return fromDb; + const name = row.course_name?.trim() ?? ""; + return SECTION9_STREAM_MAPPINGS[name] ?? []; +} + function rowToCourse(row: OfficialCourseRow, opts: { district?: string; zscore?: number }): OfficialCourse { - const eligibleStreams = asStringList(row.eligible_streams); + const eligibleStreams = getCourseEligibleStreams(row); const medium = asMedium(row.medium); return { @@ -232,8 +278,8 @@ async function fetchCourseRows(params: { from official_handbook_zscore_cutoffs z where z.source_handbook_year = $2 and z.uni_code = c.uni_code - and z.district = $3 - order by z.academic_year desc + and (z.district = $3 or z.district = 'All Island') + order by case when z.district = $3 then 0 else 1 end, z.academic_year desc limit 1 ) as latest_cutoff, ( @@ -241,8 +287,8 @@ async function fetchCourseRows(params: { from official_handbook_zscore_cutoffs z where z.source_handbook_year = $2 and z.uni_code = c.uni_code - and z.district = $3 - order by z.academic_year desc + and (z.district = $3 or z.district = 'All Island') + order by case when z.district = $3 then 0 else 1 end, z.academic_year desc limit 1 ) as latest_cutoff_year from official_handbook_courses c @@ -273,14 +319,11 @@ export async function listOfficialCourses(opts: { return rows .filter((row) => { - const streams = asStringList(row.eligible_streams); + const streams = getCourseEligibleStreams(row); if (opts.stream && !streams.includes(opts.stream)) return false; if (opts.universityId != null && officialUniversityApiId(row.university) !== opts.universityId) return false; if (opts.faculty && row.faculty !== opts.faculty) return false; if (opts.duration != null && row.duration !== String(opts.duration)) return false; - if (opts.zscore != null && !Number.isNaN(opts.zscore) && row.latest_cutoff != null) { - return opts.zscore >= row.latest_cutoff - 0.15; - } return true; }) .map((row) => rowToCourse(row, { district: opts.district, zscore: opts.zscore })); @@ -363,7 +406,21 @@ export async function listOfficialStreams(): Promise { `, [OFFICIAL_HANDBOOK_YEAR], ); - return result.rows.map((row) => row.stream); + const streams = new Set(result.rows.map((row) => row.stream)); + for (const list of Object.values(SECTION9_STREAM_MAPPINGS)) { + for (const s of list) streams.add(s); + } + const standardStreams = [ + "Physical Science", + "Biological Science", + "Commerce", + "Arts", + "Technology", + "Engineering Technology", + "Biosystems Technology", + ]; + for (const s of standardStreams) streams.add(s); + return Array.from(streams).sort(); } export async function listOfficialSubjects(): Promise { @@ -423,23 +480,26 @@ export async function getOfficialCheckerRecommendations(input: { zscore: number; }): Promise { const groups = emptyRecommendationGroups(); - const courses = await listOfficialCourses({ + const rows = await fetchCourseRows({ academicYear: input.academicYear || OFFICIAL_HANDBOOK_YEAR, district: input.district, - stream: input.stream, }); - for (const course of courses) { - const detail = await getOfficialCourseDetail(course.id, { - academicYear: input.academicYear || OFFICIAL_HANDBOOK_YEAR, - district: input.district, - }); - if (!detail) continue; + const matchingRows = rows.filter((row) => { + const streams = getCourseEligibleStreams(row); + return !input.stream || streams.includes(input.stream); + }); + + for (const row of matchingRows) { + const subjects = asStringList(row.required_subjects); + const minimumGrades = asStringList(row.minimum_grades); + const specialRequirements = asStringList(row.special_requirements); + const medium = asMedium(row.medium); - const missingSubjects = detail.subjects + const missingSubjects = subjects .filter(isSimpleSubjectRequirement) .filter((subject) => !isPassingGrade(input.subjectGrades[subject])); - const manualSubjects = detail.subjects.filter((subject) => !isSimpleSubjectRequirement(subject)); + const manualSubjects = subjects.filter((subject) => !isSimpleSubjectRequirement(subject)); const reasons: string[] = []; if (missingSubjects.length > 0) { @@ -451,39 +511,39 @@ export async function getOfficialCheckerRecommendations(input: { if (manualSubjects.length > 0) { reasons.push(`Manual subject verification required: ${manualSubjects.join("; ")}`); } - if (detail.minimumGrades.length > 0) { - reasons.push(`Minimum grade rule: ${detail.minimumGrades.join("; ")}`); + if (minimumGrades.length > 0) { + reasons.push(`Minimum grade rule: ${minimumGrades.join("; ")}`); } - if (detail.specialRequirements.length > 0) { - reasons.push(`Special requirement: ${detail.specialRequirements.join("; ")}`); + if (specialRequirements.length > 0) { + reasons.push(`Special requirement: ${specialRequirements.join("; ")}`); } - if (course.officialMinimumZScore == null) { + if (row.latest_cutoff == null) { reasons.push(`No exact official cutoff mapping for ${input.district}`); } else { - reasons.push(`Official cutoff ${course.officialMinimumZScore} for ${input.district}`); + reasons.push(`Official cutoff ${row.latest_cutoff} for ${input.district}`); } const recommendation: OfficialCheckerRecommendation = { - programmeId: detail.id, - universityId: detail.universityId, - university: detail.universityName, - courseName: detail.degreeName, - faculty: detail.faculty, + programmeId: officialCourseApiId(row.uni_code), + universityId: officialUniversityApiId(row.university), + university: row.university, + courseName: row.course_name, + faculty: row.faculty, degreeDuration: null, - duration: detail.duration, - medium: mediumLabel(detail.medium), + duration: row.duration, + medium: mediumLabel(medium), studentZScore: input.zscore, - officialCutoff: detail.officialMinimumZScore, + officialCutoff: row.latest_cutoff, estimatedMin: null, estimatedMax: null, estimatedCenter: null, - confidence: detail.officialMinimumZScore == null ? null : "High", + confidence: row.latest_cutoff == null ? null : "High", academicYear: input.academicYear || OFFICIAL_HANDBOOK_YEAR, sourceHandbook: `UGC Admissions Handbook ${SOURCE_HANDBOOK_YEAR}`, sourcePage: null, requiredStream: input.stream, - requiredSubjects: detail.subjects.map((subjectName) => ({ + requiredSubjects: subjects.map((subjectName) => ({ subjectName, requirementType: "official_text", minimumGrade: null, @@ -491,13 +551,33 @@ export async function getOfficialCheckerRecommendations(input: { reasons, }; - if (missingSubjects.length > 0 || detail.officialMinimumZScore == null) { + if (missingSubjects.length > 0 || row.latest_cutoff == null) { groups.notEligible.push(recommendation); } else { - groups[groupForCutoff(input.zscore, detail.officialMinimumZScore)].push(recommendation); + groups[groupForCutoff(input.zscore, row.latest_cutoff)].push(recommendation); } } + const sortByNearestZScore = (items: OfficialCheckerRecommendation[]) => + [...items].sort((a, b) => { + const cutoffA = a.officialCutoff ?? a.estimatedCenter; + const cutoffB = b.officialCutoff ?? b.estimatedCenter; + if (cutoffA == null && cutoffB == null) return 0; + if (cutoffA == null) return 1; + if (cutoffB == null) return -1; + const distA = Math.abs(input.zscore - cutoffA); + const distB = Math.abs(input.zscore - cutoffB); + if (Math.abs(distA - distB) > 0.0001) { + return distA - distB; + } + return cutoffB - cutoffA; + }); + + groups.competitiveOptions = sortByNearestZScore(groups.competitiveOptions); + groups.nearHistoricalRange = sortByNearestZScore(groups.nearHistoricalRange); + groups.strongMatches = sortByNearestZScore(groups.strongMatches); + groups.notEligible = sortByNearestZScore(groups.notEligible); + return { mode: "historical_estimate", resultModeLabel: "Historical Estimate", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3eaaf12..24b8c9f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: false + autoInstallPeers: true excludeLinksFromLockfile: false importers: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..0b48919 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + core-js: true + esbuild: true diff --git a/frontend/src/components/checker/checker-results.tsx b/frontend/src/components/checker/checker-results.tsx index 8dc7051..c64e059 100644 --- a/frontend/src/components/checker/checker-results.tsx +++ b/frontend/src/components/checker/checker-results.tsx @@ -1,38 +1,39 @@ -import { BookOpen, Building2, CheckCircle2, GraduationCap, XCircle } from "lucide-react"; +import { useState, useMemo } from "react"; +import { BookOpen, Building2, CheckCircle2, GraduationCap, Search, Sparkles, Target, XCircle } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { useTranslations } from "@/lib/i18n"; +import { cn } from "@/lib/utils"; import type { CheckerRecommendation, CheckerRecommendationsResponse, CheckerResultGroup, } from "@/api"; -const GROUP_META: Record = { - strongMatches: { - title: "Strong Matches", - className: "bg-green-500/15 text-green-700 border-green-500/30", - }, - competitiveOptions: { - title: "Competitive Options", - className: "bg-blue-500/15 text-blue-700 border-blue-500/30", - }, - nearHistoricalRange: { - title: "Near Historical Range", - className: "bg-amber-500/15 text-amber-700 border-amber-500/30", - }, - notEligible: { - title: "Not Eligible", - className: "bg-red-500/15 text-red-700 border-red-500/30", - }, +const GROUP_CLASS_NAMES: Record = { + competitiveOptions: "bg-blue-500/15 text-blue-700 border-blue-500/30 dark:text-blue-400", + strongMatches: "bg-emerald-500/15 text-emerald-700 border-emerald-500/30 dark:text-emerald-400", + nearHistoricalRange: "bg-amber-500/15 text-amber-700 border-amber-500/30 dark:text-amber-400", + notEligible: "bg-red-500/15 text-red-700 border-red-500/30 dark:text-red-400", }; const GROUPS: CheckerResultGroup[] = [ - "strongMatches", "competitiveOptions", + "strongMatches", "nearHistoricalRange", "notEligible", ]; +function getEffectiveCutoff(rec: CheckerRecommendation): number | null { + if (rec.officialCutoff != null) return rec.officialCutoff; + if (rec.estimatedCenter != null) return rec.estimatedCenter; + if (rec.estimatedMin != null && rec.estimatedMax != null) { + return (rec.estimatedMin + rec.estimatedMax) / 2; + } + return null; +} + function ResultCard({ result, modeLabel, @@ -42,37 +43,96 @@ function ResultCard({ modeLabel: string; group: CheckerResultGroup; }) { - const ReasonIcon = group === "notEligible" ? XCircle : CheckCircle2; - const reasonIconClass = group === "notEligible" ? "text-red-500" : "text-green-600"; + const isEligible = group === "competitiveOptions" || group === "strongMatches"; + const ReasonIcon = isEligible ? CheckCircle2 : XCircle; + const reasonIconClass = isEligible ? "text-emerald-600 dark:text-emerald-400" : "text-red-500"; + + const cutoff = getEffectiveCutoff(result); + const diff = cutoff != null ? result.studentZScore - cutoff : null; + const absDiff = diff != null ? Math.abs(diff) : null; return ( - - + +
- - {modeLabel} + +
+ {modeLabel} + + {group === "competitiveOptions" && "Top Close Match"} + {group === "strongMatches" && "Strong / Safe Match"} + {group === "nearHistoricalRange" && "Near Range (Reach)"} + {group === "notEligible" && "Not Eligible"} + +
{result.courseName} -
- {result.university} +
+ + {result.university}
- -
- {result.faculty ?? "Faculty not listed"} - - {result.duration ?? (result.degreeDuration != null ? `${result.degreeDuration} years` : "Duration not listed")} + + {/* Proximity / Match Banner */} + {cutoff != null && diff != null ? ( +
= 0 + ? diff <= 0.1 + ? "bg-blue-500/10 text-blue-900 dark:text-blue-300 border-blue-500/30" + : "bg-emerald-500/10 text-emerald-900 dark:text-emerald-300 border-emerald-500/30" + : diff >= -0.15 + ? "bg-amber-500/10 text-amber-900 dark:text-amber-300 border-amber-500/30" + : "bg-red-500/10 text-red-900 dark:text-red-300 border-red-500/30" + )} + > + + + Cutoff: {cutoff.toFixed(4)} + + + {diff >= 0 ? ( + +{diff.toFixed(3)} (Qualified) + ) : ( + {diff.toFixed(3)} (Gap) + )} + {absDiff != null && absDiff <= 0.05 && " ⭐"} + +
+ ) : ( +
+ No exact district cutoff available +
+ )} + +
+ {result.faculty ?? "Faculty"} + + {result.duration ?? (result.degreeDuration != null ? `${result.degreeDuration} years` : "4 Years")} - {result.medium && {result.medium}} - {result.requiredStream} stream + {result.medium && {result.medium}} + {result.requiredStream}
{result.requiredSubjects.length > 0 && ( -
+

Required subjects

-
+
{result.requiredSubjects.map((s, i) => ( - + {s.subjectName} {s.minimumGrade ? ` ≥ ${s.minimumGrade}` : ""} {s.requirementType === "one_of" ? " (either)" : ""} @@ -82,41 +142,43 @@ function ResultCard({
)} -
-

- Student Z-score: {result.studentZScore} -

+
+
+ Your Z-score: + {result.studentZScore} +
{result.officialCutoff != null ? ( -

- Official cutoff: {result.officialCutoff} -

+
+ Official cutoff: + {result.officialCutoff} +
) : ( -

- Estimated range:{" "} - +

+ Estimated range: + {result.estimatedMin ?? "?"} - {result.estimatedMax ?? "?"} - {result.estimatedCenter != null ? ` (center ${result.estimatedCenter})` : ""} -

+
)} {result.confidence && ( -

- Confidence: {result.confidence} -

+
+ Confidence: + {result.confidence} +
)}
-
- - +
+ + {result.sourceHandbook} {result.sourcePage != null ? `, page ${result.sourcePage}` : ""}
-
    +
      {result.reasons.map((reason) => ( -
    • +
    • {reason}
    • @@ -127,49 +189,313 @@ function ResultCard({ ); } -export function CheckerResults({ response }: { response: CheckerRecommendationsResponse }) { - const total = GROUPS.reduce((count, group) => count + response.groups[group].length, 0); +export function CheckerResults({ + response, + recommendations, +}: { + response?: CheckerRecommendationsResponse; + recommendations?: CheckerRecommendationsResponse; + inputs?: unknown; +}) { + const { language } = useTranslations(); + const res = response || recommendations; + const [filterStatus, setFilterStatus] = useState<"eligible" | "non-eligible" | "engineering-ai" | "all">("eligible"); + const [searchQuery, setSearchQuery] = useState(""); + + const isEngineeringOrAi = (name: string) => + /engineering|artificial intelligence|\bai\b|intelligent systems/i.test(name); + + const allItemsWithMeta = useMemo(() => { + if (!res || !res.groups) return []; + const items: Array<{ + result: CheckerRecommendation; + group: CheckerResultGroup; + isEligible: boolean; + distance: number; + cutoff: number | null; + }> = []; + + for (const group of GROUPS) { + const list = res.groups[group] || []; + const isEligible = group === "competitiveOptions" || group === "strongMatches"; + for (const result of list) { + const cutoff = getEffectiveCutoff(result); + const distance = cutoff != null ? Math.abs(result.studentZScore - cutoff) : Number.POSITIVE_INFINITY; + items.push({ result, group, isEligible, distance, cutoff }); + } + } + + return items; + }, [res]); + + const eligibleCount = useMemo( + () => allItemsWithMeta.filter((x) => x.isEligible).length, + [allItemsWithMeta] + ); + const nonEligibleCount = useMemo( + () => allItemsWithMeta.filter((x) => !x.isEligible).length, + [allItemsWithMeta] + ); + const engineeringAiItems = useMemo( + () => allItemsWithMeta.filter((x) => isEngineeringOrAi(x.result.courseName)), + [allItemsWithMeta] + ); + const engineeringAiCount = engineeringAiItems.length; + const totalCount = allItemsWithMeta.length; + + // Filtered and sorted list based on active tab and search + const displayedResults = useMemo(() => { + let filtered = allItemsWithMeta; + + if (filterStatus === "eligible") { + filtered = filtered.filter((x) => x.isEligible); + // For eligible courses: most matching (closest cutoff to student score) appears FIRST! + filtered.sort((a, b) => { + if (a.distance !== b.distance) { + return a.distance - b.distance; + } + // If same distance, highest cutoff first + const cutA = a.cutoff ?? -Infinity; + const cutB = b.cutoff ?? -Infinity; + if (cutA !== cutB) return cutB - cutA; + return a.result.courseName.localeCompare(b.result.courseName); + }); + } else if (filterStatus === "non-eligible") { + filtered = filtered.filter((x) => !x.isEligible); + // For non-eligible: closest reach courses (smallest gap to cutoff) appear FIRST! + filtered.sort((a, b) => { + if (a.distance !== b.distance) { + return a.distance - b.distance; + } + return a.result.courseName.localeCompare(b.result.courseName); + }); + } else if (filterStatus === "engineering-ai") { + filtered = engineeringAiItems; + // Sort by closest distance to student score + filtered = [...filtered].sort((a, b) => { + if (a.distance !== b.distance) { + return a.distance - b.distance; + } + return a.result.courseName.localeCompare(b.result.courseName); + }); + } else { + // All courses: closest distance overall appears FIRST + filtered = [...filtered].sort((a, b) => { + if (a.distance !== b.distance) { + return a.distance - b.distance; + } + return a.result.courseName.localeCompare(b.result.courseName); + }); + } + + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + const inTab = filtered.filter( + (x) => + x.result.courseName.toLowerCase().includes(q) || + x.result.university.toLowerCase().includes(q) || + (x.result.faculty && x.result.faculty.toLowerCase().includes(q)) + ); + if (inTab.length > 0) { + return inTab; + } + // Fallback: search across all evaluated courses so courses like AI or Engineering are never missing + return allItemsWithMeta.filter( + (x) => + x.result.courseName.toLowerCase().includes(q) || + x.result.university.toLowerCase().includes(q) || + (x.result.faculty && x.result.faculty.toLowerCase().includes(q)) + ); + } + + return filtered; + }, [allItemsWithMeta, engineeringAiItems, filterStatus, searchQuery]); - if (total === 0) { + if (!res || !res.groups) { + return null; + } + + if (totalCount === 0) { return (

      - No verified programmes found for this academic year and stream yet. + {language === "si" + ? "මෙම විෂය ධාරාව සහ වර්ෂය සඳහා පාඨමාලා හමු නොවීය." + : language === "ta" + ? "இந்த கல்வியாண்டு மற்றும் பிரிவிற்கு பாடநெறிகள் எதுவும் கிடைக்கவில்லை." + : "No verified programmes found for this academic year and stream yet."}

      ); } return ( -
      +
      + {/* Header Info Badges */}
      - {response.resultModeLabel} - {response.academicYear} - {response.district} + {res.resultModeLabel} + {res.academicYear} + {res.district}
      -

      {response.disclaimer}

      - - {GROUPS.map((group) => { - const meta = GROUP_META[group]; - const results = response.groups[group]; - if (results.length === 0) return null; - return ( -
      -
      - {meta.title} - {results.length} -
      -
      - {results.map((result) => ( - - ))} -
      -
      - ); - })} + +

      {res.disclaimer}

      + + {/* 1st Prominent Filter: Eligible vs Non-Eligible vs Engineering & AI vs All */} +
      +
      + {/* ELIGIBLE BUTTON (Default) */} + + + {/* ENGINEERING & AI FOCUS BUTTON */} + + + {/* NON-ELIGIBLE BUTTON */} + + + {/* ALL BUTTON */} + +
      + + {/* Quick Search */} +
      + + setSearchQuery(e.target.value)} + className="pl-8 h-9 text-xs bg-card" + /> +
      +
      + + {/* Engineering & AI Quick Overview Alert when on Eligible tab */} + {filterStatus === "eligible" && engineeringAiCount > 0 && ( +
      +
      + + + Looking for Engineering or Artificial Intelligence?{" "} + {engineeringAiCount} programmes evaluated (including Moratuwa, Peradeniya, USJ, Ruhuna, and SEUSL). + +
      + +
      + )} + + {/* Filter Status Explanation Header */} +
      +
      + + {filterStatus === "eligible" && ( + + Showing {displayedResults.length} eligible courses —{" "} + + most matching courses closest to your Z-Score appear first + + + )} + {filterStatus === "engineering-ai" && ( + + Showing all {displayedResults.length} Engineering and AI programmes ranked by closest cutoff to your score + + )} + {filterStatus === "non-eligible" && ( + + Showing {displayedResults.length} non-eligible courses —{" "} + + closest reach targets appear first + + + )} + {filterStatus === "all" && ( + + Showing all {displayedResults.length} courses ranked by nearest Z-score distance + + )} +
      +
      + + {/* Results Grid */} + {displayedResults.length === 0 ? ( + +

      No courses found matching the search criteria.

      +
      + ) : ( +
      + {displayedResults.map(({ result, group }) => ( + + ))} +
      + )}
      ); } diff --git a/frontend/src/components/language-switcher.tsx b/frontend/src/components/language-switcher.tsx new file mode 100644 index 0000000..e961bee --- /dev/null +++ b/frontend/src/components/language-switcher.tsx @@ -0,0 +1,69 @@ +import { Globe } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useTranslations, type SupportedLanguage } from "@/lib/i18n"; +import { useProfileStore } from "@/hooks/use-profile"; +import { cn } from "@/lib/utils"; + +interface LanguageSwitcherProps { + className?: string; + variant?: "select" | "pills"; +} + +export function LanguageSwitcher({ className, variant = "select" }: LanguageSwitcherProps) { + const { language, setLanguage, supportedLanguages } = useTranslations(); + const setProfileLanguage = useProfileStore((s) => s.setLanguage); + + const handleLanguageChange = (val: string) => { + const lang = val as SupportedLanguage; + setLanguage(lang); + setProfileLanguage(lang); + }; + + if (variant === "pills") { + return ( +
      + + {supportedLanguages.map((lang) => ( + + ))} +
      + ); + } + + return ( +
      + + +
      + ); +} diff --git a/frontend/src/components/layout.tsx b/frontend/src/components/layout.tsx index ee3e1e5..b40c4d4 100644 --- a/frontend/src/components/layout.tsx +++ b/frontend/src/components/layout.tsx @@ -17,63 +17,70 @@ import { import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { useAuthStore } from "@/hooks/use-auth"; +import { useTranslations } from "@/lib/i18n"; +import { LanguageSwitcher } from "@/components/language-switcher"; function SkipLink() { + const { t } = useTranslations(); return ( - Skip to main content + {t.nav.skipToMain} ); } -const navItems = [ - { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, - { href: "/profile", label: "Profile", icon: User }, - { href: "/courses", label: "Courses", icon: BookOpen }, - { href: "/checker", label: "Z-Score Checker", icon: Target }, - { href: "/universities", label: "Universities", icon: Building2 }, - { href: "/careers", label: "Careers", icon: Briefcase }, - { href: "/roadmap", label: "Roadmap", icon: Map }, - { href: "/reviews", label: "Reviews", icon: Star }, - { href: "/stories", label: "Success Stories", icon: Trophy }, - { href: "/chat", label: "AI Mentor", icon: MessageSquare }, -]; +function getNavItems(t: ReturnType["t"]) { + return [ + { href: "/dashboard", label: t.nav.dashboard, icon: LayoutDashboard }, + { href: "/profile", label: t.nav.profile, icon: User }, + { href: "/courses", label: t.nav.courses, icon: BookOpen }, + { href: "/checker", label: t.nav.checker, icon: Target }, + { href: "/universities", label: t.nav.universities, icon: Building2 }, + { href: "/careers", label: t.nav.careers, icon: Briefcase }, + { href: "/roadmap", label: t.nav.roadmap, icon: Map }, + { href: "/reviews", label: t.nav.reviews, icon: Star }, + { href: "/stories", label: t.nav.stories, icon: Trophy }, + { href: "/chat", label: t.nav.chat, icon: MessageSquare }, + ]; +} function MarketingHeader() { const user = useAuthStore((s) => s.user); const isAuthenticated = useAuthStore((s) => s.isAuthenticated()); const logout = useAuthStore((s) => s.logout); + const { t } = useTranslations(); return ( -
      -
      +
      +
      SkillPath AI -
      +
      + {isAuthenticated ? ( <> - Hi, {user?.name} + {t.nav.hi}, {user?.name} ) : ( )}
      @@ -87,17 +94,19 @@ function AppShell({ children }: { children: React.ReactNode }) { const user = useAuthStore((s) => s.user); const isAuthenticated = useAuthStore((s) => s.isAuthenticated()); const logout = useAuthStore((s) => s.logout); + const { t } = useTranslations(); + const navItems = getNavItems(t); return (
      -
      - SkillPath AI - {isAuthenticated ? ( - - ) : ( - - )} +
      + + SkillPath AI + +
      +
      + + {isAuthenticated ? ( + + ) : ( + + )} +
      ); diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts new file mode 100644 index 0000000..12ab657 --- /dev/null +++ b/frontend/src/lib/i18n.ts @@ -0,0 +1,754 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export type SupportedLanguage = "en" | "si" | "ta"; + +export interface LanguageOption { + code: SupportedLanguage; + label: string; + nativeName: string; +} + +export const SUPPORTED_LANGUAGES: LanguageOption[] = [ + { code: "en", label: "English", nativeName: "English" }, + { code: "si", label: "Sinhala", nativeName: "සිංහල" }, + { code: "ta", label: "Tamil", nativeName: "தமிழ்" }, +]; + +export const translations = { + en: { + // Navigation + nav: { + home: "Home", + dashboard: "Dashboard", + profile: "Profile", + courses: "Courses", + checker: "Z-Score Checker", + universities: "Universities", + careers: "Careers", + roadmap: "Roadmap", + reviews: "Reviews", + stories: "Success Stories", + chat: "AI Mentor", + signIn: "Sign In", + logout: "Logout", + register: "Register", + skipToMain: "Skip to main content", + admin: "Admin", + hi: "Hi", + }, + // Common Actions & Badges + actions: { + search: "Search", + filter: "Filter", + reset: "Reset", + save: "Save", + saving: "Saving...", + saved: "Saved", + viewDetails: "View Details", + learnMore: "Learn More", + getStarted: "Get Started Free", + setUpProfile: "Set Up Profile", + browseCourses: "Browse Courses", + generateRoadmap: "Generate Roadmap", + downloadPdf: "Download PDF", + send: "Send", + askQuestion: "Ask a question...", + submit: "Submit", + cancel: "Cancel", + selectLanguage: "Language", + back: "Back", + all: "All", + retry: "Retry", + }, + // Home Page + home: { + heroTitle: "Find your path to university success", + heroSubtitle: "SkillPath AI helps Sri Lankan students discover courses, plan careers, and get AI-powered guidance.", + featuresHeading: "Everything you need to decide", + ctaHeading: "Ready to start?", + ctaSubtitle: "Create your profile and get personalized recommendations today.", + smartRecommendations: "Smart Recommendations", + smartRecommendationsDesc: "Get course matches based on your A/L stream and Z-score.", + exploreCourses: "Explore Courses", + exploreCoursesDesc: "Browse degrees across Sri Lankan state universities with filters.", + aiRoadmap: "AI Career Roadmap", + aiRoadmapDesc: "Generate personalized year-by-year plans for your chosen degree.", + alumniInsights: "Alumni Insights", + alumniInsightsDesc: "Read reviews and success stories from graduates in your field.", + footerText: "SkillPath AI — Navigate your university and career journey", + }, + // Dashboard + dashboard: { + title: "Dashboard", + welcomeBack: "Welcome back", + overviewSubtitle: "Your personalized overview", + statUniversities: "Universities", + statCourses: "Courses", + statReviews: "Reviews", + statStories: "Success Stories", + profileAlertTitle: "Complete your profile for personalized recommendations", + profileAlertDesc: "Add your stream, Z-score, and district to unlock UGC-based course matches.", + topStreamsTitle: "Top A/L Streams by Programme Count", + recommendedTitle: "Recommended Courses", + recommendedSubtitle: "Top matches for your profile", + noRecommendations: "No recommendations yet. Update your profile.", + careerPathsTitle: "Career Paths", + careerPathsSubtitle: "Aligned with your stream", + noCareers: "No career paths yet.", + savedCoursesTitle: "Saved Courses", + noSavedCourses: "No saved courses yet", + savedUniversitiesTitle: "Saved Universities", + noSavedUniversities: "No saved universities yet", + roadmapsTitle: "Roadmaps", + noRoadmaps: "No roadmaps generated yet", + recentSearchesTitle: "Recent Searches", + handbookBadge: "UGC Handbook", + }, + // Courses Page & Detail + courses: { + title: "Courses & Degree Programmes", + subtitle: "Explore undergraduate programmes across all Sri Lankan state universities.", + searchPlaceholder: "Search by course, university, or faculty...", + allUniversities: "All Universities", + filterByUni: "Filter by University", + noCoursesFound: "No courses found matching your criteria.", + viewCourse: "View Details", + eligibilityLikely: "Eligible (Likely)", + eligibilityBorderline: "Borderline", + eligibilityReach: "Aspirational", + eligibilityUnlikely: "Unlikely", + backToCourses: "Back to Courses", + courseDetails: "Course Details", + duration: "Duration", + years: "Years", + medium: "Medium of Instruction", + faculty: "Faculty", + uniCode: "UGC Code", + cutoffHistory: "Official Z-Score Cutoff History", + districtCutoff: "District Cutoff", + aiPrediction: "AI Cutoff Trend & Insights", + getPrediction: "Show Prediction Insight", + generateCareerRoadmap: "Generate Career Roadmap", + viewReviews: "View Alumni Reviews", + }, + // Z-Score Checker + checker: { + title: "Z-Score & Course Checker", + subtitle: "Check course eligibility against verified UGC admissions handbook data. No login required.", + notice: "Eligibility is evaluated using verified handbook data and a rule engine. Upcoming-year figures are statistical estimates, not a guarantee of admission.", + academicYear: "Academic Year", + stream: "A/L Stream", + district: "District (UGC Quota)", + zscore: "Your Z-Score", + subjectGrades: "Subject Grades (Optional)", + addSubject: "Add Subject Grade", + checkBtn: "Check Eligibility", + resultsHeading: "Eligible Degree Programmes", + eligibleBadge: "Eligible", + borderlineBadge: "Borderline", + reachBadge: "Aspirational", + noResults: "No programmes found matching these criteria.", + }, + // Universities + universities: { + title: "Sri Lankan State Universities", + subtitle: "Browse all recognized UGC state universities and their offered faculties.", + searchPlaceholder: "Search universities...", + officialCourses: "official courses", + viewCourses: "View Courses", + allUniversities: "All Universities", + notFound: "University not found", + backToUniversities: "Back to Universities", + providerDescription: "Official handbook-derived course provider", + }, + // Careers + careers: { + title: "Career Paths", + subtitle: "Explore career opportunities, salary prospects, and industry demand by degree type.", + allCategories: "All Categories", + salaryEstimate: "Estimated Starting Salary", + growth: "Growth", + demand: "Industry Demand", + noCareersFound: "No career paths found.", + }, + // Roadmap + roadmap: { + title: "AI Career Roadmap", + subtitle: "Generate a personalized academic and career execution plan for your degree.", + selectCourseLabel: "Select a Degree Programme", + selectCoursePlaceholder: "Choose a course...", + generateBtn: "Generate Roadmap", + generatingBtn: "Generating AI Roadmap...", + downloadPdfBtn: "Download PDF", + year: "Year", + milestones: "Key Milestones & Skills", + afterGraduation: "After Graduation & Career Steps", + generatedSuccess: "Roadmap generated successfully!", + }, + // Reviews + reviews: { + title: "Alumni Reviews", + subtitle: "Real experiences and candid feedback from university graduates.", + reviewsFor: "Reviews for", + averageRating: "Average Rating", + noReviews: "No reviews found for this course.", + verifiedAlumni: "Verified Graduate", + courseReviews: "Course Reviews", + }, + // Stories + stories: { + title: "Success Stories", + subtitle: "Inspiring journeys from Sri Lankan graduates who built thriving careers.", + classOf: "Class of", + careerJourney: "Career Journey", + noStories: "No success stories available.", + }, + // AI Chat + chat: { + title: "AI University & Career Mentor", + subtitle: "Ask questions in English, Sinhala, or Tamil about UGC admissions, degrees, and career opportunities.", + placeholder: "Ask about cutoffs, university courses, careers...", + suggestionsTitle: "Suggested Questions:", + disclaimer: "AI guidance is based on official UGC guidelines and historical cutoffs.", + }, + // Profile + profile: { + title: "Student Profile", + subtitle: "Personalize your Z-score, A/L stream, and district for tailored recommendations.", + streamLabel: "A/L Stream", + streamPlaceholder: "Select your stream", + zscoreLabel: "Z-Score", + districtLabel: "District (UGC Quota)", + districtPlaceholder: "Select your district", + languageLabel: "Preferred Language", + saveButton: "Save Profile", + savedToast: "Profile updated successfully!", + }, + // Auth (Sign in & Register) + auth: { + signInTitle: "Sign In", + signInSubtitle: "Access your saved courses, roadmaps, and student profile.", + registerTitle: "Create an Account", + registerSubtitle: "Start your personalized university journey with SkillPath AI.", + nameLabel: "Full Name", + emailLabel: "Email Address", + passwordLabel: "Password", + signInBtn: "Sign In", + registerBtn: "Create Account", + alreadyHaveAccount: "Already have an account? Sign in", + dontHaveAccount: "Don't have an account? Register free", + }, + // Streams + streams: { + physical: "Physical Science", + biological: "Biological Science", + commerce: "Commerce", + arts: "Arts", + technology: "Technology", + common: "Common Stream", + }, + }, + + si: { + // Navigation + nav: { + home: "මුල් පිටුව", + dashboard: "උපකරණ පුවරුව", + profile: "මගේ තොරතුරු", + courses: "පාඨමාලා", + checker: "Z-ලකුණු පරීක්ෂකය", + universities: "විශ්වවිද්‍යාල", + careers: "වෘත්තීන්", + roadmap: "වෘත්තීය සැලැස්ම", + reviews: "සමාලෝචන", + stories: "සාර්ථක කතා", + chat: "AI උපදේශක", + signIn: "ඇතුල් වන්න", + logout: "ඉවත් වන්න", + register: "ලියාපදිංචි වන්න", + skipToMain: "ප්‍රධාන අන්තර්ගතයට යන්න", + admin: "පරිපාලක", + hi: "ආයුබෝවන්", + }, + // Common Actions & Badges + actions: { + search: "සොයන්න", + filter: "පෙරහන්", + reset: "යළි පිහිටුවන්න", + save: "සුරකින්න", + saving: "සුරකිමින්...", + saved: "සුරකින ලදී", + viewDetails: "විස්තර බලන්න", + learnMore: "තවත් දැනගන්න", + getStarted: "නොමිලේ ආරම්භ කරන්න", + setUpProfile: "තොරතුරු ඇතුළත් කරන්න", + browseCourses: "පාඨමාලා බලන්න", + generateRoadmap: "සැලැස්ම සාදන්න", + downloadPdf: "PDF බාගන්න", + send: "යවන්න", + askQuestion: "ප්‍රශ්නයක් අසන්න...", + submit: "යොමු කරන්න", + cancel: "අවලංගු කරන්න", + selectLanguage: "භාෂාව", + back: "ආපසු", + all: "සියල්ල", + retry: "නැවත උත්සාහ කරන්න", + }, + // Home Page + home: { + heroTitle: "විශ්වවිද්‍යාල ජයග්‍රහණයට නිවැරදි මඟපෙන්වීම", + heroSubtitle: "SkillPath AI මඟින් ශ්‍රී ලාංකික සිසුන්ට පාඨමාලා තෝරාගැනීමට, වෘත්තීන් සැලසුම් කිරීමට සහ AI මඟපෙන්වීම් ලබාදෙයි.", + featuresHeading: "ඔබට අවශ්‍ය සියලුම මඟපෙන්වීම් එකම තැනකින්", + ctaHeading: "ආරම්භ කිරීමට සූදානම්ද?", + ctaSubtitle: "ඔබගේ තොරතුරු ඇතුළත් කර අදම ඔබට ගැළපෙන පාඨමාලා සොයාගන්න.", + smartRecommendations: "බුද්ධිමත් නිර්දේශ", + smartRecommendationsDesc: "ඔබේ උසස් පෙළ විෂය ධාරාව සහ Z-ලකුණ අනුව ගැළපෙන පාඨමාලා.", + exploreCourses: "පාඨමාලා ගවේෂණය", + exploreCoursesDesc: "ශ්‍රී ලංකාවේ සියලුම රාජ්‍ය විශ්වවිද්‍යාල වල උපාධි පාඨමාලා පරීක්ෂා කරන්න.", + aiRoadmap: "AI වෘත්තීය සැලැස්ම", + aiRoadmapDesc: "ඔබ තෝරාගත් උපාධියට අදාළ වසරින් වසර වෘත්තීය සැලැස්මක් සාදාගන්න.", + alumniInsights: "පැරණි සිසුන්ගේ අත්දැකීම්", + alumniInsightsDesc: "උපාධිධාරීන්ගේ සැබෑ අත්දැකීම් සහ සාර්ථක කතා කියවන්න.", + footerText: "SkillPath AI — විශ්වවිද්‍යාල සහ වෘත්තීය ජීවිතය ජයගන්න", + }, + // Dashboard + dashboard: { + title: "උපකරණ පුවරුව", + welcomeBack: "නැවත සාදරයෙන් පිළිගනිමු", + overviewSubtitle: "ඔබගේ පෞද්ගලික දළ විශ්ලේෂණය", + statUniversities: "විශ්වවිද්‍යාල", + statCourses: "පාඨමාලා", + statReviews: "සමාලෝචන", + statStories: "සාර්ථක කතා", + profileAlertTitle: "පෞද්ගලික නිර්දේශ ලබාගැනීමට ඔබේ තොරතුරු සම්පූර්ණ කරන්න", + profileAlertDesc: "UGC කඩයිම් අනුව ගැළපෙන පාඨමාලා බැලීමට විෂය ධාරාව, Z-ලකුණ සහ දිස්ත්‍රික්කය ඇතුළත් කරන්න.", + topStreamsTitle: "වැඩිම පාඨමාලා ඇති උසස් පෙළ විෂය ධාරාවන්", + recommendedTitle: "නිර්දේශිත පාඨමාලා", + recommendedSubtitle: "ඔබේ Z-ලකුණට ගැළපෙන හොඳම තේරීම්", + noRecommendations: "තවමත් නිර්දේශ නොමැත. කරුණාකර තොරතුරු යාවත්කාලීන කරන්න.", + careerPathsTitle: "ගැළපෙන වෘත්තීය මාර්ග", + careerPathsSubtitle: "ඔබේ විෂය ධාරාවට අදාළ ක්ෂේත්‍ර", + noCareers: "වෘත්තීය මාර්ග හමු නොවීය.", + savedCoursesTitle: "සුරකින ලද පාඨමාලා", + noSavedCourses: "තවමත් පාඨමාලා සුරැකී නොමැත", + savedUniversitiesTitle: "සුරකින ලද විශ්වවිද්‍යාල", + noSavedUniversities: "තවමත් විශ්වවිද්‍යාල සුරැකී නොමැත", + roadmapsTitle: "වෘත්තීය සැලසුම්", + noRoadmaps: "තවමත් සැලසුම් සාදා නොමැත", + recentSearchesTitle: "මෑත සෙවීම්", + handbookBadge: "UGC අත්පොත", + }, + // Courses Page & Detail + courses: { + title: "උපාධි පාඨමාලා නාමාවලිය", + subtitle: "ශ්‍රී ලංකාවේ සියලුම රාජ්‍ය විශ්වවිද්‍යාල වල උපාධි පාඨමාලා පරීක්ෂා කරන්න.", + searchPlaceholder: "පාඨමාලාව, විශ්වවිද්‍යාලය හෝ පීඨය අනුව සොයන්න...", + allUniversities: "සියලුම විශ්වවිද්‍යාල", + filterByUni: "විශ්වවිද්‍යාලය අනුව පෙරහන් කරන්න", + noCoursesFound: "ඔබ සෙවූ පාඨමාලා හමු නොවීය.", + viewCourse: "විස්තර බලන්න", + eligibilityLikely: "සුදුසුයි (ඉහළ සම්භාවිතාව)", + eligibilityBorderline: "ආසන්නයි (අවදානම්)", + eligibilityReach: "උත්සාහ කළ හැකියි", + eligibilityUnlikely: "සුදුසුකම් නොලබයි", + backToCourses: "නැවත පාඨමාලා වෙත", + courseDetails: "පාඨමාලාවේ විස්තර", + duration: "කාලසීමාව", + years: "වසර", + medium: "ඉගැන්වීමේ භාෂාව", + faculty: "පීඨය", + uniCode: "UGC කේතය", + cutoffHistory: "පසුගිය නිල Z-ලකුණු කඩයිම් ඉතිහාසය", + districtCutoff: "දිස්ත්‍රික් කඩයිම් ලකුණ", + aiPrediction: "AI කඩයිම් ලකුණු අනාවැකිය", + getPrediction: "අනාවැකි විස්තර බලන්න", + generateCareerRoadmap: "වෘත්තීය සැලැස්ම සාදන්න", + viewReviews: "උපාධිධාරී අදහස් බලන්න", + }, + // Z-Score Checker + checker: { + title: "UGC Z-ලකුණු සුදුසුකම් පරීක්ෂකය", + subtitle: "නිල UGC දත්ත මත පදනම්ව ඔබේ සුදුසුකම් පරීක්ෂා කරගන්න. ලියාපදිංචිය අවශ්‍ය නොවේ.", + notice: "සුදුසුකම් පරීක්ෂා කිරීම UGC නිල අත්පොතේ නීති රීති අනුව සිදුවේ. ඉදිරි වසර සඳහා වන අගයන් පෙර වසර වල සාමාන්‍ය ගණනය කිරීම් වේ.", + academicYear: "අධ්‍යයන වර්ෂය", + stream: "උසස් පෙළ විෂය ධාරාව", + district: "දිස්ත්‍රික්කය (UGC කෝටා)", + zscore: "ඔබගේ Z-ලකුණ", + subjectGrades: "විෂය සාමාර්ථ (විකල්ප)", + addSubject: "විෂය සාමාර්ථ එක් කරන්න", + checkBtn: "සුදුසුකම් පරීක්ෂා කරන්න", + resultsHeading: "සුදුසුකම් ලබන උපාධි පාඨමාලා", + eligibleBadge: "සුදුසුයි", + borderlineBadge: "ආසන්නයි", + reachBadge: "උත්සාහ කළ හැකියි", + noResults: "සුදුසු පාඨමාලා හමු නොවීය.", + }, + // Universities + universities: { + title: "ශ්‍රී ලංකාවේ රාජ්‍ය විශ්වවිද්‍යාල", + subtitle: "UGC පිළිගත් සියලුම රාජ්‍ය විශ්වවිද්‍යාල සහ ඒවායේ පීඨ ගවේෂණය කරන්න.", + searchPlaceholder: "විශ්වවිද්‍යාල සොයන්න...", + officialCourses: "නිල පාඨමාලා", + viewCourses: "පාඨමාලා බලන්න", + allUniversities: "සියලුම විශ්වවිද්‍යාල", + notFound: "විශ්වවිද්‍යාලය හමු නොවීය", + backToUniversities: "නැවත විශ්වවිද්‍යාල වෙත", + providerDescription: "නිල UGC අත්පොත අනුව පාඨමාලා සපයන්නා", + }, + // Careers + careers: { + title: "වෘත්තීය මාර්ග සහ අවස්ථා", + subtitle: "උපාධි ක්ෂේත්‍රය අනුව රැකියා අවස්ථා, වැටුප් මට්ටම් සහ ඉල්ලුම පරීක්ෂා කරන්න.", + allCategories: "සියලුම වර්ගීකරණයන්", + salaryEstimate: "ආරම්භක වැටුප් පරාසය", + growth: "වර්ධන හැකියාව", + demand: "ක්ෂේත්‍රයේ ඉල්ලුම", + noCareersFound: "වෘත්තීය තොරතුරු හමු නොවීය.", + }, + // Roadmap + roadmap: { + title: "AI වෘත්තීය සැලැස්ම", + subtitle: "ඔබ තෝරාගත් උපාධියට අදාළව වසර 4 ක අධ්‍යයන සහ වෘත්තීය සැලැස්මක් සාදාගන්න.", + selectCourseLabel: "උපාධි පාඨමාලාව තෝරන්න", + selectCoursePlaceholder: "පාඨමාලාවක් තෝරන්න...", + generateBtn: "සැලැස්ම සාදන්න", + generatingBtn: "සැලැස්ම සකසමින් පවතී...", + downloadPdfBtn: "PDF බාගන්න", + year: "වසර", + milestones: "ප්‍රධාන කුසලතා සහ ඉලක්ක", + afterGraduation: "උපාධිය ලැබීමෙන් පසු වෘත්තීය පියවර", + generatedSuccess: "වෘත්තීය සැලැස්ම සාර්ථකව සාදන ලදී!", + }, + // Reviews + reviews: { + title: "උපාධිධාරීන්ගේ සමාලෝචන", + subtitle: "විශ්වවිද්‍යාල සිසුන් සහ උපාධිධාරීන්ගේ සැබෑ අත්දැකීම් සහ අදහස්.", + reviewsFor: "සමාලෝචන", + averageRating: "සාමාන්‍ය ඇගයීම", + noReviews: "මෙම පාඨමාලාව සඳහා තවමත් සමාලෝචන නොමැත.", + verifiedAlumni: "තහවුරු කළ උපාධිධාරී", + courseReviews: "පාඨමාලා සමාලෝචන", + }, + // Stories + stories: { + title: "සාර්ථක කතා", + subtitle: "විශ්වවිද්‍යාලය ජයගෙන සාර්ථක වෘත්තීය ජීවිතයක් ගොඩනඟාගත් අයගේ කතා.", + classOf: "කණ්ඩායම", + careerJourney: "වෘත්තීය ගමන් මග", + noStories: "සාර්ථක කතා හමු නොවීය.", + }, + // AI Chat + chat: { + title: "AI විශ්වවිද්‍යාල සහ වෘත්තීය උපදේශක", + subtitle: "UGC ඇතුළත් කිරීම්, උපාධි සහ රැකියා අවස්ථා ගැන සිංහලෙන්, දෙමළෙන් හෝ ඉංග්‍රීසියෙන් විමසන්න.", + placeholder: "කඩයිම් ලකුණු, විශ්වවිද්‍යාල පාඨමාලා, රැකියා ගැන අසන්න...", + suggestionsTitle: "යෝජිත ප්‍රශ්න:", + disclaimer: "AI මඟපෙන්වීම් UGC නිල මාර්ගෝපදේශ සහ පසුගිය කඩයිම් ලකුණු මත පදනම් වේ.", + }, + // Profile + profile: { + title: "ශිෂ්‍ය තොරතුරු", + subtitle: "ඔබේ Z-ලකුණ, උසස් පෙළ විෂය ධාරාව සහ දිස්ත්‍රික්කය තෝරාගන්න.", + streamLabel: "උසස් පෙළ විෂය ධාරාව", + streamPlaceholder: "විෂය ධාරාව තෝරන්න", + zscoreLabel: "Z-ලකුණ (Z-Score)", + districtLabel: "දිස්ත්‍රික්කය (UGC කෝටා)", + districtPlaceholder: "දිස්ත්‍රික්කය තෝරන්න", + languageLabel: "භාවිතා කරන භාෂාව", + saveButton: "තොරතුරු සුරකින්න", + savedToast: "තොරතුරු සාර්ථකව සුරකින ලදී!", + }, + // Auth (Sign in & Register) + auth: { + signInTitle: "ඇතුල් වන්න", + signInSubtitle: "ඔබේ සුරකින ලද පාඨමාලා සහ ශිෂ්‍ය තොරතුරු වෙත පිවිසෙන්න.", + registerTitle: "ගිණුමක් සාදන්න", + registerSubtitle: "SkillPath AI සමඟින් ඔබේ විශ්වවිද්‍යාල ගමන අදම අරඹන්න.", + nameLabel: "සම්පූර්ණ නම", + emailLabel: "විද්‍යුත් තැපෑල (Email)", + passwordLabel: "මුරපදය (Password)", + signInBtn: "ඇතුල් වන්න", + registerBtn: "ගිණුම සාදන්න", + alreadyHaveAccount: "දැනටමත් ගිණුමක් තිබේද? ඇතුල් වන්න", + dontHaveAccount: "ගිණුමක් නොමැතිද? නොමිලේ ලියාපදිංචි වන්න", + }, + // Streams + streams: { + physical: "භෞතික විද්‍යා (ගණිත)", + biological: "ජීව විද්‍යා", + commerce: "වාණිජ", + arts: "කලා", + technology: "තාක්ෂණවේදය", + common: "පොදු විෂය ධාරාව", + }, + }, + + ta: { + // Navigation + nav: { + home: "முகப்பு", + dashboard: "முகப்புப் பலகை", + profile: "சுயவிவரம்", + courses: "பாடநெறிகள்", + checker: "Z-புள்ளி சரிபார்ப்பான்", + universities: "பல்கலைக்கழகங்கள்", + careers: "தொழில்கள்", + roadmap: "தொழில் வரைபடம்", + reviews: "மதிப்புரைகள்", + stories: "வெற்றிக் கதைகள்", + chat: "AI வழிகாட்டி", + signIn: "உள்நுழைக", + logout: "வெளியேறுக", + register: "பதிவு செய்க", + skipToMain: "முக்கிய பகுதிக்குச் செல்க", + admin: "நிர்வாகம்", + hi: "வணக்கம்", + }, + // Common Actions & Badges + actions: { + search: "தேடுக", + filter: "வடிகட்டுக", + reset: "மீட்டமைக்க", + save: "சேமிக்க", + saving: "சேமிக்கப்படுகிறது...", + saved: "சேமிக்கப்பட்டது", + viewDetails: "விவரங்களை பார்க்க", + learnMore: "மேலும் அறிய", + getStarted: "இலவசமாக தொடங்குக", + setUpProfile: "சுயவிவரத்தை அமைக்க", + browseCourses: "பாடநெறிகளை உலாவுக", + generateRoadmap: "வரைபடத்தை உருவாக்கு", + downloadPdf: "PDF பதிவிறக்குக", + send: "அனுப்புக", + askQuestion: "கேள்வி கேளுங்கள்...", + submit: "சமர்ப்பிக்க", + cancel: "ரத்து செய்", + selectLanguage: "மொழி", + back: "பின்செல்க", + all: "அனைத்தும்", + retry: "மீண்டும் முயற்சிக்கவும்", + }, + // Home Page + home: { + heroTitle: "பல்கலைக்கழக வெற்றிக்கான உங்கள் வழிகாட்டி", + heroSubtitle: "SkillPath AI இலங்கை மாணவர்களுக்கு பாடநெறிகளைத் தேர்ந்தெடுக்கவும், தொழில்களைத் திட்டமிடவும், AI வழிகாட்டலைப் பெறவும் உதவுகிறது.", + featuresHeading: "முடிவெடுக்க உங்களுக்குத் தேவையான அனைத்தும்", + ctaHeading: "தொடங்க தயாரா?", + ctaSubtitle: "உங்கள் சுயவிவரத்தை உருவாக்கி இன்றே பொருத்தமான பாடநெறிகளைக் கண்டறியுங்கள்.", + smartRecommendations: "புத்திசாலித்தனமான பரிந்துரைகள்", + smartRecommendationsDesc: "உங்கள் உயர்தரப் பிரிவு மற்றும் Z-புள்ளியின் அடிப்படையில் பாடநெறிப் பொருத்தங்கள்.", + exploreCourses: "பாடநெறிகளை ஆராய்க", + exploreCoursesDesc: "இலங்கை அரச பல்கலைக்கழகங்களில் உள்ள பட்டப்படிப்புகளை வடிப்பான்களுடன் உலாவுக.", + aiRoadmap: "AI தொழில் வரைபடம்", + aiRoadmapDesc: "நீங்கள் தேர்ந்தெடுத்த பட்டப்படிப்புக்கான ஆண்டு வாரியான திட்டத்தை உருவாக்குங்கள்.", + alumniInsights: "பட்டதாரிகளின் அனுபவங்கள்", + alumniInsightsDesc: "உங்கள் துறையில் உள்ள பட்டதாரிகளின் உண்மையான மதிப்புரைகள் மற்றும் வெற்றிக் கதைகளைப் படியுங்கள்.", + footerText: "SkillPath AI — உங்கள் பல்கலைக்கழக மற்றும் தொழில் பயணத்தை வழிநடத்துங்கள்", + }, + // Dashboard + dashboard: { + title: "முகப்புப் பலகை", + welcomeBack: "மீண்டும் வருக", + overviewSubtitle: "உங்கள் தனிப்பயனாக்கப்பட்ட மேலோட்டம்", + statUniversities: "பல்கலைக்கழகங்கள்", + statCourses: "பாடநெறிகள்", + statReviews: "மதிப்புரைகள்", + statStories: "வெற்றிக் கதைகள்", + profileAlertTitle: "தனிப்பயனாக்கப்பட்ட பரிந்துரைகளுக்கு உங்கள் சுயவிவரத்தை பூர்த்தி செய்யவும்", + profileAlertDesc: "UGC வெட்டுப்புள்ளிகளுக்கு ஏற்ப பாடநெறிகளைப் பெற உங்கள் பிரிவு, Z-புள்ளி மற்றும் மாவட்டத்தை உள்ளிடவும்.", + topStreamsTitle: "அதிக பாடநெறிகளைக் கொண்ட உயர்தரப் பிரிவுகள்", + recommendedTitle: "பரிந்துரைக்கப்பட்ட பாடநெறிகள்", + recommendedSubtitle: "உங்கள் சுயவிவரத்திற்கு மிகவும் பொருத்தமானவை", + noRecommendations: "இன்னும் பரிந்துரைகள் இல்லை. உங்கள் சுயவிவரத்தை புதுப்பிக்கவும்.", + careerPathsTitle: "தொழில் பாதைகள்", + careerPathsSubtitle: "உங்கள் பிரிவுக்கு ஏற்ற தொழில்கள்", + noCareers: "தொழில் பாதைகள் கிடைக்கவில்லை.", + savedCoursesTitle: "சேமிக்கப்பட்ட பாடநெறிகள்", + noSavedCourses: "பாடநெறிகள் எதுவும் சேமிக்கப்படவில்லை", + savedUniversitiesTitle: "சேமிக்கப்பட்ட பல்கலைக்கழகங்கள்", + noSavedUniversities: "பல்கலைக்கழகங்கள் எதுவும் சேமிக்கப்படவில்லை", + roadmapsTitle: "தொழில் வரைபடங்கள்", + noRoadmaps: "வரைபடங்கள் எதுவும் உருவாக்கப்படவில்லை", + recentSearchesTitle: "சமீபத்திய தேடல்கள்", + handbookBadge: "UGC கையேடு", + }, + // Courses Page & Detail + courses: { + title: "பட்டப்படிப்பு பாடநெறிகள்", + subtitle: "இலங்கையின் அனைத்து அரச பல்கலைக்கழகங்களிலும் உள்ள பட்டப்படிப்புகளை ஆராயுங்கள்.", + searchPlaceholder: "பாடநெறி, பல்கலைக்கழகம் அல்லது பீடத்தின் மூலம் தேடுக...", + allUniversities: "அனைத்து பல்கலைக்கழகங்கள்", + filterByUni: "பல்கலைக்கழக வாரியாக வடிகட்டுக", + noCoursesFound: "பாடநெறிகள் எதுவும் கிடைக்கவில்லை.", + viewCourse: "விவரங்களை பார்க்க", + eligibilityLikely: "தகுதியானது (அதிக வாய்ப்பு)", + eligibilityBorderline: "எல்லைக்கோடு (அருகில்)", + eligibilityReach: "முயற்சிக்கலாம்", + eligibilityUnlikely: "வாய்ப்பு குறைவு", + backToCourses: "மீண்டும் பாடநெறிகளுக்கு", + courseDetails: "பாடநெறி விவரங்கள்", + duration: "கால அளவு", + years: "ஆண்டுகள்", + medium: "கற்பிக்கும் மொழி", + faculty: "பீடம்", + uniCode: "UGC குறியீடு", + cutoffHistory: "உத்தியோகபூர்வ Z-புள்ளி வெட்டுப்புள்ளி வரலாறு", + districtCutoff: "மாவட்ட வெட்டுப்புள்ளி", + aiPrediction: "AI வெட்டுப்புள்ளி கணிப்பு", + getPrediction: "கணிப்பு விவரங்களை பார்க்க", + generateCareerRoadmap: "தொழில் வரைபடத்தை உருவாக்கு", + viewReviews: "பட்டதாரிகளின் மதிப்புரைகள்", + }, + // Z-Score Checker + checker: { + title: "UGC Z-புள்ளி தகுதி சரிபார்ப்பான்", + subtitle: "உத்தியோகபூர்வ UGC தரவுகளின் அடிப்படையில் உங்கள் தகுதியைச் சரிபார்க்கவும். உள்நுழைவு தேவையில்லை.", + notice: "தகுதி மதிப்பீடு UGC கையேட்டின் விதிகளின்படி செய்யப்படுகிறது. எதிர்கால ஆண்டுகளுக்கான எண்கள் முந்தைய ஆண்டுகளின் மதிப்பீடுகளாகும்.", + academicYear: "கல்வியாண்டு", + stream: "உயர்தரப் பிரிவு (Stream)", + district: "மாவட்டம் (UGC ஒதுக்கீடு)", + zscore: "உங்கள் Z-புள்ளி", + subjectGrades: "பாடப் பெறுபேறுகள் (விருப்பமானது)", + addSubject: "பாடப் பெறுபேறைச் சேர்க்கவும்", + checkBtn: "தகுதியைச் சரிபார்க்கவும்", + resultsHeading: "தகுதியான பட்டப்படிப்புகள்", + eligibleBadge: "தகுதியானது", + borderlineBadge: "அருகில் உள்ளது", + reachBadge: "முயற்சிக்கலாம்", + noResults: "பொருத்தமான பாடநெறிகள் எதுவும் கிடைக்கவில்லை.", + }, + // Universities + universities: { + title: "இலங்கை அரச பல்கலைக்கழகங்கள்", + subtitle: "UGC அங்கீகரிக்கப்பட்ட அனைத்து பல்கலைக்கழகங்கள் மற்றும் அவற்றின் பீடங்களை ஆராயுங்கள்.", + searchPlaceholder: "பல்கலைக்கழகங்களை தேடுக...", + officialCourses: "உத்தியோகபூர்வ பாடநெறிகள்", + viewCourses: "பாடநெறிகளைப் பார்க்க", + allUniversities: "அனைத்து பல்கலைக்கழகங்கள்", + notFound: "பல்கலைக்கழகம் கிடைக்கவில்லை", + backToUniversities: "மீண்டும் பல்கலைக்கழகங்களுக்கு", + providerDescription: "உத்தியோகபூர்வ UGC கையேட்டு பாடநெறி வழங்குநர்", + }, + // Careers + careers: { + title: "தொழில் வாய்ப்புகள்", + subtitle: "பட்டப்படிப்பு வகை வாரியாக தொழில் வாய்ப்புகள், சம்பள எதிர்பார்ப்புகள் மற்றும் தேவையை ஆராயுங்கள்.", + allCategories: "அனைத்து பிரிவுகளும்", + salaryEstimate: "ஆரம்ப சம்பள மதிப்பீடு", + growth: "வளர்ச்சி", + demand: "தொழில்துறை தேவை", + noCareersFound: "தொழில் வாய்ப்புகள் கிடைக்கவில்லை.", + }, + // Roadmap + roadmap: { + title: "AI தொழில் வரைபடம்", + subtitle: "நீங்கள் தேர்ந்தெடுத்த பட்டப்படிப்புக்கான தனிப்பயனாக்கப்பட்ட 4 ஆண்டு திட்டத்தை உருவாக்குங்கள்.", + selectCourseLabel: "பட்டப்படிப்பைத் தேர்ந்தெடுக்கவும்", + selectCoursePlaceholder: "ஒரு பாடநெறியைத் தேர்ந்தெடுக்கவும்...", + generateBtn: "வரைபடத்தை உருவாக்கு", + generatingBtn: "வரைபடம் உருவாக்கப்படுகிறது...", + downloadPdfBtn: "PDF பதிவிறக்குக", + year: "ஆண்டு", + milestones: "முக்கிய திறன்கள் மற்றும் இலக்குகள்", + afterGraduation: "பட்டப்படிப்புக்குப் பிந்தைய தொழில் படிகள்", + generatedSuccess: "தொழில் வரைபடம் வெற்றிகரமாக உருவாக்கப்பட்டது!", + }, + // Reviews + reviews: { + title: "பட்டதாரிகளின் மதிப்புரைகள்", + subtitle: "பல்கலைக்கழக பட்டதாரிகளின் உண்மையான அனுபவங்கள் மற்றும் கருத்துக்கள்.", + reviewsFor: "மதிப்புரைகள்", + averageRating: "சராசரி மதிப்பீடு", + noReviews: "இந்த பாடநெறிக்கு இன்னும் மதிப்புரைகள் இல்லை.", + verifiedAlumni: "சரிபார்க்கப்பட்ட பட்டதாரி", + courseReviews: "பாடநெறி மதிப்புரைகள்", + }, + // Stories + stories: { + title: "வெற்றிக் கதைகள்", + subtitle: "பல்கலைக்கழகத்தை வெற்றிகரமாக முடித்து சிறந்த தொழில்களை உருவாக்கியவர்களின் கதைகள்.", + classOf: "பட்டமளிப்பு ஆண்டு", + careerJourney: "தொழில் பயணம்", + noStories: "வெற்றிக் கதைகள் எதுவும் கிடைக்கவில்லை.", + }, + // AI Chat + chat: { + title: "AI பல்கலைக்கழக & தொழில் வழிகாட்டி", + subtitle: "UGC அனுமதி, பட்டப்படிப்புகள் மற்றும் தொழில் வாய்ப்புகள் பற்றி தமிழ், சிங்களம் அல்லது ஆங்கிலத்தில் கேளுங்கள்.", + placeholder: "வெட்டுப்புள்ளிகள், பாடநெறிகள், தொழில்கள் பற்றி கேளுங்கள்...", + suggestionsTitle: "பரிந்துரைக்கப்பட்ட கேள்விகள்:", + disclaimer: "AI வழிகாட்டல் UGC உத்தியோகபூர்வ வழிகாட்டுதல்கள் மற்றும் கடந்தகால வெட்டுப்புள்ளிகளை அடிப்படையாகக் கொண்டது.", + }, + // Profile + profile: { + title: "மாணவர் சுயவிவரம்", + subtitle: "தனிப்பயனாக்கப்பட்ட பரிந்துரைகளுக்கு உங்கள் Z-புள்ளி, உயர்தரப் பிரிவு மற்றும் மாவட்டத்தைத் தேர்ந்தெடுக்கவும்.", + streamLabel: "உயர்தரப் பிரிவு (A/L Stream)", + streamPlaceholder: "உங்கள் பிரிவைத் தேர்ந்தெடுக்கவும்", + zscoreLabel: "Z-புள்ளி (Z-Score)", + districtLabel: "மாவட்டம் (UGC ஒதுக்கீடு)", + districtPlaceholder: "உங்கள் மாவட்டத்தைத் தேர்ந்தெடுக்கவும்", + languageLabel: "விருப்பமான மொழி", + saveButton: "சுயவிவரத்தை சேமிக்க", + savedToast: "சுயவிவரம் வெற்றிகரமாக புதுப்பிக்கப்பட்டது!", + }, + // Auth (Sign in & Register) + auth: { + signInTitle: "உள்நுழைக", + signInSubtitle: "உங்கள் சேமிக்கப்பட்ட பாடநெறிகள் மற்றும் சுயவிவரத்தை அணுகவும்.", + registerTitle: "கணக்கை உருவாக்கவும்", + registerSubtitle: "SkillPath AI உடன் உங்கள் பல்கலைக்கழக பயணத்தை இன்றே தொடங்குங்கள்.", + nameLabel: "முழுப் பெயர்", + emailLabel: "மின்னஞ்சல் முகவரி", + passwordLabel: "கடவுச்சொல்", + signInBtn: "உள்நுழைக", + registerBtn: "கணக்கை உருவாக்கு", + alreadyHaveAccount: "ஏற்கனவே கணக்கு உள்ளதா? உள்நுழைக", + dontHaveAccount: "கணக்கு இல்லையா? இலவசமாக பதிவு செய்க", + }, + // Streams + streams: { + physical: "பௌதீக விஞ்ஞானம் (கணிதம்)", + biological: "உயிரியல் விஞ்ஞானம்", + commerce: "வர்த்தகம்", + arts: "கலை", + technology: "தொழில்நுட்பம்", + common: "பொதுப் பிரிவு", + }, + }, +}; + +interface I18nStore { + language: SupportedLanguage; + setLanguage: (lang: SupportedLanguage) => void; + t: typeof translations["en"]; +} + +export const useI18n = create()( + persist( + (set, get) => ({ + language: "en", + setLanguage: (lang: SupportedLanguage) => { + set({ language: lang }); + if (typeof document !== "undefined") { + document.documentElement.lang = lang; + } + }, + get t() { + return translations[get().language] ?? translations.en; + }, + }), + { + name: "skillpath-language-preference", + }, + ), +); + +/** Helper hook for direct translation object */ +export function useTranslations() { + const language = useI18n((s) => s.language); + const setLanguage = useI18n((s) => s.setLanguage); + const t = translations[language] ?? translations.en; + return { language, setLanguage, t, supportedLanguages: SUPPORTED_LANGUAGES }; +} diff --git a/frontend/src/pages/careers.tsx b/frontend/src/pages/careers.tsx index a5fb493..0fd333a 100644 --- a/frontend/src/pages/careers.tsx +++ b/frontend/src/pages/careers.tsx @@ -5,6 +5,7 @@ import { Briefcase, TrendingUp, DollarSign } from "lucide-react"; import { useListCareerPaths } from "@/api"; import { usePageTitle } from "@/hooks/use-page-title"; +import { useTranslations } from "@/lib/i18n"; import { QueryError } from "@/components/query-error"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -16,7 +17,8 @@ function getQueryParam(search: string, key: string): string | null { } export default function Careers() { - usePageTitle("Career Paths"); + const { t } = useTranslations(); + usePageTitle(t.careers.title); const search = useSearch(); const degreeTypeParam = getQueryParam(search, "degreeType"); @@ -47,15 +49,15 @@ export default function Careers() { return (
      -

      Career Paths

      -

      Explore career opportunities by degree type.

      +

      {t.careers.title}

      +

      {t.careers.subtitle}

      {degreeTypes.map((type) => ( - {type === "all" ? "All" : type} + {type === "all" ? t.careers.allCategories : type} ))} @@ -85,11 +87,11 @@ export default function Careers() {
      - LKR {career.salaryMin.toLocaleString()} – {career.salaryMax.toLocaleString()} + {t.careers.salaryEstimate}: LKR {career.salaryMin.toLocaleString()} – {career.salaryMax.toLocaleString()}
      - Growth: {career.growthPotential} · Demand: {career.industryDemand} + {t.careers.growth}: {career.growthPotential} · {t.careers.demand}: {career.industryDemand}
      @@ -98,7 +100,7 @@ export default function Careers() { )} {!isLoading && filtered.length === 0 && ( -

      No career paths found.

      +

      {t.careers.noCareersFound}

      )}
      diff --git a/frontend/src/pages/chat.tsx b/frontend/src/pages/chat.tsx index c10bc2f..bd62585 100644 --- a/frontend/src/pages/chat.tsx +++ b/frontend/src/pages/chat.tsx @@ -6,6 +6,7 @@ import { useAiChat } from "@/api"; import { useProfileStore } from "@/hooks/use-profile"; import { usePageTitle } from "@/hooks/use-page-title"; import { useToast } from "@/hooks/use-toast"; +import { useTranslations } from "@/lib/i18n"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -15,23 +16,40 @@ interface Message { content: string; } -const SUGGESTIONS = [ - "What courses suit a Physical Science student with Z-score 1.5?", - "How do I choose between Engineering and Medicine?", - "What careers are available after a Commerce degree?", -]; +const LOCALIZED_SUGGESTIONS = { + en: [ + "What courses suit a Physical Science student with Z-score 1.5?", + "How do I choose between Engineering and Information Systems?", + "What careers are available after a Commerce degree?", + ], + si: [ + "භෞතික විද්‍යා ධාරාවේ Z-ලකුණ 1.5 ක් ඇති සිසුවෙකුට ගැළපෙන පාඨමාලා මොනවාද?", + "ඉංජිනේරු සහ තොරතුරු තාක්ෂණ උපාධි අතරින් හොඳම තේරීම කුමක්ද?", + "වාණිජ උපාධියකින් පසු ඇති රැකියා අවස්ථා මොනවාද?", + ], + ta: [ + "பௌதீக விஞ்ஞானப் பிரிவில் Z-புள்ளி 1.5 பெற்ற மாணவருக்குப் பொருத்தமான பாடநெறிகள் எவை?", + "பொறியியல் மற்றும் தகவல் தொழில்நுட்ப பட்டப்படிப்புகளுக்கு இடையிலான வேறுபாடு என்ன?", + "வர்த்தகப் பட்டப்படிப்பை முடித்த பின் உள்ள தொழில் வாய்ப்புகள் எவை?", + ], +}; export default function Chat() { - usePageTitle("AI Mentor"); + const { t, language } = useTranslations(); + usePageTitle(t.chat.title); const profile = useProfileStore(); const { toast } = useToast(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const bottomRef = useRef(null); - const context = profile.isComplete() - ? `Student stream: ${profile.stream}, Z-score: ${profile.zscore}, language: ${profile.language}` - : undefined; + const langName = language === "si" ? "Sinhala" : language === "ta" ? "Tamil" : "English"; + const context = `Language: ${langName} (${language}). ` + + (profile.isComplete() + ? `Student stream: ${profile.stream}, Z-score: ${profile.zscore}, district: ${profile.district}. Please reply in ${langName}.` + : `Please respond clearly in ${langName}.`); + + const suggestions = LOCALIZED_SUGGESTIONS[language] || LOCALIZED_SUGGESTIONS.en; const { mutate: sendMessage, isPending } = useAiChat({ mutation: { @@ -40,11 +58,18 @@ export default function Chat() { setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: "smooth" }), 100); }, onError: () => { + const errorMsg = + language === "si" + ? "සමාවන්න, මේ මොහොතේ ඔබේ ප්‍රශ්නයට පිළිතුරු දීමට නොහැක. කරුණාකර නැවත උත්සාහ කරන්න." + : language === "ta" + ? "மன்னிக்கவும், தற்போது உங்கள் கேள்விக்கு பதிலளிக்க முடியவில்லை. தயவுசெய்து சிறிது நேரம் கழித்து மீண்டும் முயற்சிக்கவும்." + : "Sorry, I couldn't process your question right now. Please try again."; + setMessages((prev) => [ ...prev, { role: "assistant", - content: "Sorry, I couldn't process your question right now. Please try again.", + content: errorMsg, }, ]); toast({ @@ -70,20 +95,20 @@ export default function Chat() {

      - AI Mentor + {t.chat.title}

      -

      Ask questions about courses, careers, and university life.

      +

      {t.chat.subtitle}

      {messages.length === 0 && ( - + - Get started - Try one of these questions + {t.chat.suggestionsTitle} + {t.chat.disclaimer} - {SUGGESTIONS.map((q) => ( - ))} @@ -105,7 +130,7 @@ export default function Chat() {
      )}
      -
      Thinking...
      +
      + {language === "si" ? "පිළිතුර සකසමින් පවතී..." : language === "ta" ? "பதிலை உருவாக்குகிறது..." : "Thinking..."} +
      )}
      @@ -133,7 +160,7 @@ export default function Chat() {