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
35 changes: 35 additions & 0 deletions apps/web/app/api/queries/useCategories.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SUPPORTED_LANGUAGES } from "@repo/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { ApiClient } from "../api-client";

import { categoriesQueryOptions } from "./useCategories";

vi.mock("../api-client", () => ({
ApiClient: {
api: {
categoryControllerGetAllCategories: vi.fn(),
},
},
}));

describe("categoriesQueryOptions", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(ApiClient.api.categoryControllerGetAllCategories).mockResolvedValue({
data: { data: [], pagination: { page: 1, perPage: 100, totalItems: 0 } },
} as unknown as Awaited<ReturnType<typeof ApiClient.api.categoryControllerGetAllCategories>>);
});

it("passes the requested language to the categories endpoint", async () => {
const query = categoriesQueryOptions({ language: SUPPORTED_LANGUAGES.PL });

await query.queryFn();

expect(ApiClient.api.categoryControllerGetAllCategories).toHaveBeenCalledWith({
language: SUPPORTED_LANGUAGES.PL,
page: 1,
perPage: 100,
});
});
});
24 changes: 7 additions & 17 deletions apps/web/app/modules/Admin/Courses/Courses.page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, useLoaderData, useNavigate } from "@remix-run/react";
import { Link, useNavigate } from "@remix-run/react";
import { COURSE_ORIGIN_TYPES, COURSE_STATUSES, COURSE_TYPE } from "@repo/shared";
import {
type ColumnDef,
Expand All @@ -15,9 +15,8 @@ import React, { startTransition, useState } from "react";
import { useTranslation } from "react-i18next";

import { useDuplicateCourse } from "~/api/mutations/admin/useDuplicateCourse";
import { categoriesQueryOptions } from "~/api/queries";
import { useCategoriesSuspense } from "~/api/queries";
import { useCoursesSuspense } from "~/api/queries/useCourses";
import { queryClient } from "~/api/queryClient";
import { ButtonGroup } from "~/components/ButtonGroup/ButtonGroup";
import { PageWrapper } from "~/components/PageWrapper/PageWrapper";
import SortButton from "~/components/TableSortButton/TableSortButton";
Expand All @@ -40,6 +39,7 @@ import {
type FilterValue,
SearchFilter,
} from "~/modules/common/SearchFilter/SearchFilter";
import { useLanguageStore } from "~/modules/Dashboard/Settings/Language/LanguageStore";
import { DashboardIcon, HamburgerIcon } from "~/modules/icons/icons";
import { getCurrencyLocale } from "~/utils/getCurrencyLocale";
import { setPageTitle } from "~/utils/setPageTitle";
Expand All @@ -55,7 +55,7 @@ import {
getCourseStatus,
} from "./utils";

import type { ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
import type { MetaFunction } from "@remix-run/react";
import type { CourseType } from "@repo/shared";
import type { GetAllCoursesResponse } from "~/api/generated-api";
import type { CourseParams, CourseStatus } from "~/api/queries/useCourses";
Expand All @@ -66,23 +66,13 @@ type TCourse = GetAllCoursesResponse["data"][number] & {

export const meta: MetaFunction = ({ matches }) => setPageTitle(matches, "pages.courses");

export const clientLoader = async (_: ClientLoaderFunctionArgs) => {
try {
const { data } = await queryClient.fetchQuery(categoriesQueryOptions());
return data;
} catch (error) {
console.error("Error fetching categories:", error);

throw new Error("Failed to load categories.");
}
};

const Courses = () => {
const categories = useLoaderData<typeof clientLoader>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useState<CourseParams>({});
const { language } = useLanguageStore();

const { data } = useCoursesSuspense(searchParams);
const { data: categories } = useCategoriesSuspense({ language });
const { data } = useCoursesSuspense({ ...searchParams, language });
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const { t } = useTranslation();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/modules/Courses/LegacyCoursesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const LegacyCoursesView = () => {
userId: currentUser?.id,
});

const { data: categories, isLoading: isCategoriesLoading } = useCategories();
const { data: categories, isLoading: isCategoriesLoading } = useCategories({ language });

const steps = useMemo(
() => (canUpdateLearningProgress ? studentCoursesSteps(t) : []),
Expand Down
3 changes: 3 additions & 0 deletions docs/specs/language-localization-business-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The main workflow is simple for learners: choose a preferred interface language
- Resolve localized content with fallback behavior when the requested language is unavailable.
- Lock structural curriculum edits to the base language while allowing translation edits in other languages.
- Keep localized admin objects such as groups and categories usable in filters and selection lists.
- Show course category filters and category-based course lists in the active interface language when translations exist.

## End-User Value

Expand All @@ -47,10 +48,12 @@ Curriculum has an extra safeguard: chapter and lesson structure is controlled fr
- Backend localized-field behavior is centralized in `apps/api/src/localization/localization.service.ts`.
- User language persistence uses the settings API and `PERMISSIONS.SETTINGS_UPDATE_SELF`.
- Many content endpoints require an explicit `language` parameter to return localized fields.
- Course and category list screens pass the active UI language into category/course queries so localized database entries can be displayed and filtered consistently.

## Test Evidence

- Web E2E coverage verifies that users can change interface language and keep it after reload.
- Web E2E coverage verifies localized auth copy for visitor-facing pages.
- Curriculum E2E coverage verifies that non-base-language curriculum structure is locked while translations can still be edited.
- API E2E coverage verifies user settings language updates and invalid language-setting rejection.
- API E2E coverage verifies category and group list localization by requested language; web query coverage verifies category requests include the selected language.
Loading