From 54152b8094f89a62a4796510845db5fcf866b2e5 Mon Sep 17 00:00:00 2001 From: Steven <97002253+wu0x51@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:49:26 -0500 Subject: [PATCH 1/5] Implement middleware.ts for language redirects and add npm dependencies required --- frontend/middleware.ts | 42 ++++++++++++++++++++++++++++++++++++++ frontend/package-lock.json | 38 ++++++++++++++++++++++++++++++++++ frontend/package.json | 3 +++ 3 files changed, 83 insertions(+) create mode 100644 frontend/middleware.ts diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 00000000..3e1e31ff --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,42 @@ +import { NextResponse, NextRequest } from "next/server"; +import { match } from "@formatjs/intl-localematcher"; +import Negotiator from "negotiator"; + +const locales = ["en", "fr"]; +const defaultLocale = "en" + +function getLocale(request: NextRequest) { + // Get Accept-Language header + const headers = { "accept-language": request.headers.get("accept-language") ?? "" }; + // Get requested locales (sorted in decreasing preference) + const requestedLocales = new Negotiator({ headers }).languages(); + + // Match to en or fr + return match(requestedLocales, locales, defaultLocale); +} + +export function middleware(request: NextRequest) { + // Check if there is any supported locale in the pathname + const { pathname } = request.nextUrl; + const pathnameHasLocale = locales.some( + (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` + ); + + if (pathnameHasLocale) return; + + // Redirect if there is no locale + const locale = getLocale(request); + request.nextUrl.pathname = `/${locale}${pathname}`; + // e.g. incoming request is /login + // The new URL is now /en/login + return NextResponse.redirect(request.nextUrl); +} + +export const config = { + matcher: [ + // Skip all internal paths (_next) + '/((?!_next).*)', + // Optional: only run on root (/) URL + // '/' + ], +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b50f9aab..8a70854e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@ai-sdk/openai": "^1.3.22", "@ai-sdk/react": "^1.2.12", + "@formatjs/intl-localematcher": "^0.8.0", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", @@ -31,6 +32,7 @@ "framer-motion": "^12.18.1", "katex": "^0.16.22", "lucide-react": "^0.515.0", + "negotiator": "^1.0.0", "next": "^15.2.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -47,6 +49,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/negotiator": "^0.6.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -413,6 +416,25 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.0.tgz", + "integrity": "sha512-zgMYWdUlmEZpX2Io+v3LHrfq9xZ6khpQVf9UAw2xYWhGerGgI9XgH1HvL/A34jWiruUJpYlP5pk4g8nIcaDrXQ==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2548,6 +2570,13 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, + "node_modules/@types/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", @@ -7297,6 +7326,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "15.5.9", "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index 10b41728..75ceb074 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ai-sdk/openai": "^1.3.22", "@ai-sdk/react": "^1.2.12", + "@formatjs/intl-localematcher": "^0.8.0", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", @@ -34,6 +35,7 @@ "framer-motion": "^12.18.1", "katex": "^0.16.22", "lucide-react": "^0.515.0", + "negotiator": "^1.0.0", "next": "^15.2.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -50,6 +52,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/negotiator": "^0.6.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", From 6864bd81a5679420db3187aea31b8da5a422c1d3 Mon Sep 17 00:00:00 2001 From: Steven <97002253+wu0x51@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:02:43 -0500 Subject: [PATCH 2/5] Move frontend pages to language dynamic segment --- frontend/app/{ => [lang]}/can-sr/citations/full-text/route.ts | 0 frontend/app/{ => [lang]}/can-sr/extract/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/extract/view/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/l1-screen/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/l1-screen/view/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/l2-screen/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/l2-screen/view/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/search/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/setup/page.tsx | 0 frontend/app/{ => [lang]}/can-sr/sr/page.tsx | 0 frontend/app/{ => [lang]}/layout.tsx | 0 frontend/app/{ => [lang]}/login/page.tsx | 0 frontend/app/{ => [lang]}/page.tsx | 0 frontend/app/{ => [lang]}/register/page.tsx | 0 frontend/app/{ => [lang]}/sso-login/page.tsx | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename frontend/app/{ => [lang]}/can-sr/citations/full-text/route.ts (100%) rename frontend/app/{ => [lang]}/can-sr/extract/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/extract/view/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/l1-screen/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/l1-screen/view/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/l2-screen/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/l2-screen/view/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/search/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/setup/page.tsx (100%) rename frontend/app/{ => [lang]}/can-sr/sr/page.tsx (100%) rename frontend/app/{ => [lang]}/layout.tsx (100%) rename frontend/app/{ => [lang]}/login/page.tsx (100%) rename frontend/app/{ => [lang]}/page.tsx (100%) rename frontend/app/{ => [lang]}/register/page.tsx (100%) rename frontend/app/{ => [lang]}/sso-login/page.tsx (100%) diff --git a/frontend/app/can-sr/citations/full-text/route.ts b/frontend/app/[lang]/can-sr/citations/full-text/route.ts similarity index 100% rename from frontend/app/can-sr/citations/full-text/route.ts rename to frontend/app/[lang]/can-sr/citations/full-text/route.ts diff --git a/frontend/app/can-sr/extract/page.tsx b/frontend/app/[lang]/can-sr/extract/page.tsx similarity index 100% rename from frontend/app/can-sr/extract/page.tsx rename to frontend/app/[lang]/can-sr/extract/page.tsx diff --git a/frontend/app/can-sr/extract/view/page.tsx b/frontend/app/[lang]/can-sr/extract/view/page.tsx similarity index 100% rename from frontend/app/can-sr/extract/view/page.tsx rename to frontend/app/[lang]/can-sr/extract/view/page.tsx diff --git a/frontend/app/can-sr/l1-screen/page.tsx b/frontend/app/[lang]/can-sr/l1-screen/page.tsx similarity index 100% rename from frontend/app/can-sr/l1-screen/page.tsx rename to frontend/app/[lang]/can-sr/l1-screen/page.tsx diff --git a/frontend/app/can-sr/l1-screen/view/page.tsx b/frontend/app/[lang]/can-sr/l1-screen/view/page.tsx similarity index 100% rename from frontend/app/can-sr/l1-screen/view/page.tsx rename to frontend/app/[lang]/can-sr/l1-screen/view/page.tsx diff --git a/frontend/app/can-sr/l2-screen/page.tsx b/frontend/app/[lang]/can-sr/l2-screen/page.tsx similarity index 100% rename from frontend/app/can-sr/l2-screen/page.tsx rename to frontend/app/[lang]/can-sr/l2-screen/page.tsx diff --git a/frontend/app/can-sr/l2-screen/view/page.tsx b/frontend/app/[lang]/can-sr/l2-screen/view/page.tsx similarity index 100% rename from frontend/app/can-sr/l2-screen/view/page.tsx rename to frontend/app/[lang]/can-sr/l2-screen/view/page.tsx diff --git a/frontend/app/can-sr/page.tsx b/frontend/app/[lang]/can-sr/page.tsx similarity index 100% rename from frontend/app/can-sr/page.tsx rename to frontend/app/[lang]/can-sr/page.tsx diff --git a/frontend/app/can-sr/search/page.tsx b/frontend/app/[lang]/can-sr/search/page.tsx similarity index 100% rename from frontend/app/can-sr/search/page.tsx rename to frontend/app/[lang]/can-sr/search/page.tsx diff --git a/frontend/app/can-sr/setup/page.tsx b/frontend/app/[lang]/can-sr/setup/page.tsx similarity index 100% rename from frontend/app/can-sr/setup/page.tsx rename to frontend/app/[lang]/can-sr/setup/page.tsx diff --git a/frontend/app/can-sr/sr/page.tsx b/frontend/app/[lang]/can-sr/sr/page.tsx similarity index 100% rename from frontend/app/can-sr/sr/page.tsx rename to frontend/app/[lang]/can-sr/sr/page.tsx diff --git a/frontend/app/layout.tsx b/frontend/app/[lang]/layout.tsx similarity index 100% rename from frontend/app/layout.tsx rename to frontend/app/[lang]/layout.tsx diff --git a/frontend/app/login/page.tsx b/frontend/app/[lang]/login/page.tsx similarity index 100% rename from frontend/app/login/page.tsx rename to frontend/app/[lang]/login/page.tsx diff --git a/frontend/app/page.tsx b/frontend/app/[lang]/page.tsx similarity index 100% rename from frontend/app/page.tsx rename to frontend/app/[lang]/page.tsx diff --git a/frontend/app/register/page.tsx b/frontend/app/[lang]/register/page.tsx similarity index 100% rename from frontend/app/register/page.tsx rename to frontend/app/[lang]/register/page.tsx diff --git a/frontend/app/sso-login/page.tsx b/frontend/app/[lang]/sso-login/page.tsx similarity index 100% rename from frontend/app/sso-login/page.tsx rename to frontend/app/[lang]/sso-login/page.tsx From 96f7155de0ab020adf513d4b460ab92f6e7487a9 Mon Sep 17 00:00:00 2001 From: Steven <97002253+wu0x51@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:35:29 -0500 Subject: [PATCH 3/5] Modify Middleware Matcher to exclude files from /public/images and other routes --- frontend/middleware.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/middleware.ts b/frontend/middleware.ts index 3e1e31ff..9eb40ddd 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -34,9 +34,14 @@ export function middleware(request: NextRequest) { export const config = { matcher: [ - // Skip all internal paths (_next) - '/((?!_next).*)', - // Optional: only run on root (/) URL - // '/' + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - images (static image assets from /public/images) + */ + '/((?!api|_next/static|_next/image|favicon.ico|images).*)', ], } \ No newline at end of file From 0c9f41afbfa39f0fc2f137fbf2be208c0a49edde Mon Sep 17 00:00:00 2001 From: Steven <97002253+wu0x51@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:24:20 -0500 Subject: [PATCH 4/5] Implement basic translation --- frontend/app/[lang]/dictionaries.ts | 10 ++++++++++ frontend/app/[lang]/login/page.tsx | 14 ++++++++++++-- frontend/dictionaries/en.json | 5 +++++ frontend/dictionaries/fr.json | 5 +++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 frontend/app/[lang]/dictionaries.ts create mode 100644 frontend/dictionaries/en.json create mode 100644 frontend/dictionaries/fr.json diff --git a/frontend/app/[lang]/dictionaries.ts b/frontend/app/[lang]/dictionaries.ts new file mode 100644 index 00000000..9a638fb8 --- /dev/null +++ b/frontend/app/[lang]/dictionaries.ts @@ -0,0 +1,10 @@ +// Might want to add server-only to dependencies +// import 'server-only' + +const dictionaries = { + en: () => import('../../dictionaries/en.json').then((module) => module.default), + fr: () => import('../../dictionaries/fr.json').then((module) => module.default), +} + +export const getDictionary = async (locale: 'en' | 'fr') => + dictionaries[locale](); \ No newline at end of file diff --git a/frontend/app/[lang]/login/page.tsx b/frontend/app/[lang]/login/page.tsx index ffa19530..e307a601 100644 --- a/frontend/app/[lang]/login/page.tsx +++ b/frontend/app/[lang]/login/page.tsx @@ -9,8 +9,13 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Eye, EyeOff } from 'lucide-react' import { API_ENDPOINTS } from '@/lib/config' +import { getDictionary } from '../dictionaries' -export default function LoginPage() { +export default async function LoginPage({ + params, +}: { + params: Promise<{ lang: 'en' | 'fr' }> +}) { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [isLoading, setIsLoading] = useState(false) @@ -68,6 +73,11 @@ export default function LoginPage() { } } + // Get language dictionary + const { lang } = await params; + const dict = await getDictionary(lang) + console.log(dict.login.formTitle) + return (