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
53 changes: 49 additions & 4 deletions frontend/src/app/[locale]/settings/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import SettingsPage from "./page";

const mockReplace = jest.fn();

jest.mock("next-intl", () => ({
useLocale: () => "en",
}));

jest.mock("next/navigation", () => ({
useRouter: () => ({ replace: mockReplace }),
usePathname: () => "/en/settings",
}));

jest.mock("../../lib/session", () => ({
logoutUser: jest.fn(),
}));
Expand Down Expand Up @@ -32,10 +43,21 @@ jest.mock("../../stores/useWalletStore", () => ({
}));

jest.mock("../../stores/useThemeStore", () => ({
useThemeStore: jest.fn(() => ({
theme: "system",
setTheme: jest.fn(),
})),
useThemeStore: jest.fn((selector) =>
selector
? selector({
theme: "system",
hydrated: true,
initializeTheme: jest.fn(),
setTheme: jest.fn(),
})
: {
theme: "system",
hydrated: true,
initializeTheme: jest.fn(),
setTheme: jest.fn(),
},
),
}));

jest.mock("../../hooks/useApi", () => ({
Expand All @@ -48,6 +70,10 @@ jest.mock("../../components/gamification/GamificationSettings", () => ({
}));

describe("SettingsPage section navigation", () => {
beforeEach(() => {
mockReplace.mockClear();
});

it("exposes the default active section via aria-selected", () => {
render(<SettingsPage />);

Expand Down Expand Up @@ -79,4 +105,23 @@ describe("SettingsPage section navigation", () => {
expect(panel).toHaveAttribute("id", panelId);
expect(panel).toHaveAttribute("aria-labelledby", "settings-tab-profile");
});

it("renders supported locales (en, es, tl) in DisplaySection and navigates via router.replace on change", async () => {
const user = userEvent.setup();
render(<SettingsPage />);

const displayTab = screen.getByRole("tab", { name: "Display" });
await user.click(displayTab);

const languageSelect = screen.getByLabelText("Language") as HTMLSelectElement;
expect(languageSelect.value).toBe("en");

const optionValues = Array.from(languageSelect.options).map((o) => o.value);
expect(optionValues).toEqual(["en", "es", "tl"]);

expect(screen.queryByText(/Only English is fully translated/i)).not.toBeInTheDocument();

await user.selectOptions(languageSelect, "es");
expect(mockReplace).toHaveBeenCalledWith("/es/settings");
});
});
36 changes: 25 additions & 11 deletions frontend/src/app/[locale]/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";

import { useEffect, useState, type KeyboardEvent } from "react";
import { useEffect, useState, useTransition, type ChangeEvent, type KeyboardEvent } from "react";
import { useLocale } from "next-intl";
import { usePathname, useRouter } from "next/navigation";
import {
User,
Wallet,
Expand Down Expand Up @@ -562,12 +564,22 @@ function DisplaySection() {
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "es", label: "Español" },
{ code: "fr", label: "Français" },
{ code: "pt", label: "Português" },
{ code: "hi", label: "हिन्दी" },
{ code: "tl", label: "Tagalog" },
];

const [language, setLanguage] = useState("en");
const [isPending, startTransition] = useTransition();
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();

function handleLanguageChange(event: ChangeEvent<HTMLSelectElement>) {
const nextLocale = event.target.value;
startTransition(() => {
const segments = pathname.split("/");
segments[1] = nextLocale;
router.replace(segments.join("/"));
});
}

const theme = useThemeStore((s) => s.theme);
const hydrated = useThemeStore((s) => s.hydrated);
Expand Down Expand Up @@ -616,12 +628,17 @@ function DisplaySection() {
</div>

<div>
<label className="text-sm font-medium text-zinc-900 dark:text-zinc-100 block mb-2">
<label
htmlFor="settings-language-select"
className="text-sm font-medium text-zinc-900 dark:text-zinc-100 block mb-2"
>
Language
</label>
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
id="settings-language-select"
value={locale}
disabled={isPending}
onChange={handleLanguageChange}
className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-indigo-500 focus:outline-none dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-50"
>
{LANGUAGES.map((l) => (
Expand All @@ -630,9 +647,6 @@ function DisplaySection() {
</option>
))}
</select>
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-1.5">
Full i18n support is coming soon. Only English is fully translated.
</p>
</div>
</CardContent>
</Card>
Expand Down
Loading