Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
46 changes: 46 additions & 0 deletions frontend/app/[lang]/dictionaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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),
}

// Fill missing translations of primary with its key by comparing with reference
// path is the recursively generated key
function fillWithKey(
primary: any,
reference: any,
path: string[] = []
): any {
// Reach end of JSON (past leaf)
if (typeof reference !== 'object' || reference === null) {
return primary ?? path.join('.');
}

// Result dictionary
const result: any = {};

// Iterate through list of keys from the reference dictionary
for (const key of Object.keys(reference)) {
// Recursive call of subproperties (if any)
result[key] = fillWithKey(
primary?.[key],
reference[key],
[...path, key]
);
}

return result;
}


export const getDictionary = async (locale: 'en' | 'fr') => {
const dict = await dictionaries[locale]();

if (locale === 'en') return dict;

// Compare and fill with keys if French
const reference = await dictionaries.en();
return fillWithKey(dict, reference);
}
File renamed without changes.
14 changes: 12 additions & 2 deletions frontend/app/login/page.tsx → frontend/app/[lang]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<div className="flex min-h-screen overflow-hidden">
{/* Left side - Health Canada Image (reduced overlay opacity) */}
Expand All @@ -87,7 +97,7 @@ export default function LoginPage() {
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<h2 className="text-3xl font-bold text-gray-900">
Access the Government of Canada AI Assistant Portal
{ dict.login.formTitle }
</h2>
</div>

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions frontend/dictionaries/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"login": {
"formTitle": "Access the Government of Canada AI Assistant Portal"
}
}
5 changes: 5 additions & 0 deletions frontend/dictionaries/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"login": {
"formTitle": "Accédez au portail de l'assistant IA du gouvernement du Canada"
}
}
47 changes: 47 additions & 0 deletions frontend/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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: [
/*
* 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).*)',
],
}
38 changes: 38 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down