fix(i18n): localize singular matching job counts
This commit is contained in:
@@ -458,6 +458,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const statusOptions = ["Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const;
|
||||
const visibleDesktopColumns = 6 + Number(columns.status) + Number(columns.dateApplied) + Number(columns.daysSince) + Number(columns.jobUrl);
|
||||
const selectedCompanyIsLoading = companyFilterId !== "All" && !companies.some((company) => company.id === companyFilterId);
|
||||
const matchingCount = boardView ? boardCount
|
||||
: jobsResource.loading || jobsResource.refreshing || jobsResource.error ? null : total;
|
||||
const densityControl = (
|
||||
<FormControl size="small" sx={{ minWidth: 150, width: { xs: "100%", sm: "auto" } }}>
|
||||
<InputLabel id="job-table-density-label">{t("listDensity")}</InputLabel>
|
||||
@@ -484,9 +486,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
))}
|
||||
</Box>
|
||||
<Typography variant="body2" role="status" sx={{ color: "text.secondary" }}>
|
||||
{boardView
|
||||
? boardCount !== null ? t("jobsMatchingCount", { count: boardCount }) : ""
|
||||
: jobsResource.loading || jobsResource.refreshing || jobsResource.error ? "" : t("jobsMatchingCount", { count: total })}
|
||||
{matchingCount === null ? "" : matchingCount === 1
|
||||
? t("jobsMatchingSingle", { count: matchingCount })
|
||||
: t("jobsMatchingCount", { count: matchingCount })}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
@@ -219,6 +219,7 @@ export const translations = {
|
||||
jobsTableView: "Table",
|
||||
jobsBoardView: "Board",
|
||||
jobsMatchingCount: "{count} matching jobs",
|
||||
jobsMatchingSingle: "{count} matching job",
|
||||
companies: "Companies",
|
||||
trash: "Trash",
|
||||
settings: "Settings",
|
||||
@@ -3436,6 +3437,7 @@ export const translations = {
|
||||
jobsTableView: "Tabell",
|
||||
jobsBoardView: "Tavle",
|
||||
jobsMatchingCount: "{count} jobber samsvarer",
|
||||
jobsMatchingSingle: "{count} jobb samsvarer",
|
||||
companies: "Selskaper",
|
||||
trash: "Papirkurv",
|
||||
settings: "Innstillinger",
|
||||
|
||||
@@ -7,7 +7,7 @@ import JobTable from "./components/JobTable";
|
||||
import { ConfirmProvider } from "./confirm";
|
||||
import { PromptProvider } from "./prompt";
|
||||
import { ToastProvider } from "./toast";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
||||
import { translations } from "./i18n/translations";
|
||||
import { getUserScopedStorageKey } from "./auth";
|
||||
const t = translations.en;
|
||||
@@ -28,6 +28,10 @@ function show(query = "", mode: "jobs" | "trash" = "jobs") {
|
||||
return render(<MemoryRouter initialEntries={[`/${mode}${query}`]}><I18nProvider><ToastProvider><ConfirmProvider><PromptProvider><Consumer mode={mode} /></PromptProvider></ConfirmProvider></ToastProvider></I18nProvider></MemoryRouter>);
|
||||
}
|
||||
const reads = (path: string) => mockedApi.get.mock.calls.filter(([url]) => url === path);
|
||||
function LanguageSwitch() {
|
||||
const { language, setLocalLanguage } = useI18n();
|
||||
return <button onClick={() => setLocalLanguage(language === 'en' ? 'nb' : 'en')}>Change language</button>;
|
||||
}
|
||||
const ids = (selector: string, attribute: string) => Array.from(document.querySelectorAll(selector), el => Number(el.getAttribute(attribute))).sort((a, b) => a - b);
|
||||
beforeEach(() => {
|
||||
localStorage.clear(); jest.clearAllMocks();
|
||||
@@ -38,6 +42,18 @@ beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
test.each((['en', 'nb'] as const).flatMap(language => ['table', 'board'].flatMap(view => [0, 1, 2].map(count => [language, view, count] as const))))('localised matching count: %s %s %s retains membership when language changes', async (language, view, count) => {
|
||||
localStorage.setItem('uiLanguage', language);
|
||||
mockedApi.get.mockImplementation(url => Promise.resolve({ data: url === '/companies' ? [jobs[0].company] : url === '/jobapplications/board' ? jobs.slice(0, count) : { items: jobs.slice(0, count), total: count, page: 1, pageSize: 15 } } as any));
|
||||
render(<MemoryRouter initialEntries={[`/jobs?view=${view}`]}><I18nProvider><LanguageSwitch /><ToastProvider><ConfirmProvider><PromptProvider><Consumer /></PromptProvider></ConfirmProvider></ToastProvider></I18nProvider></MemoryRouter>);
|
||||
const label = (locale: 'en' | 'nb') => locale === 'en' ? `${count} matching ${count === 1 ? 'job' : 'jobs'}` : `${count} ${count === 1 ? 'jobb' : 'jobber'} samsvarer`;
|
||||
await screen.findByText(label(language));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Change language' }));
|
||||
await screen.findByText(label(language === 'en' ? 'nb' : 'en'));
|
||||
expect(reads(view === 'board' ? '/jobapplications' : '/jobapplications/board')).toHaveLength(0);
|
||||
expect(ids(view === 'board' ? '[data-kanban-card-id]' : '[data-job-row-id]', view === 'board' ? 'data-kanban-card-id' : 'data-job-row-id')).toEqual(jobs.slice(0, count).map(job => job.id));
|
||||
});
|
||||
|
||||
test("Table/Board switches preserve query, complete membership, page, density and history without hidden reads", async () => {
|
||||
show("?q=Engineer&page=2&sortBy=jobTitle&sortDir=asc");
|
||||
await screen.findByText("16–26 of 26");
|
||||
@@ -72,14 +88,14 @@ test("Table/Board switches preserve query, complete membership, page, density an
|
||||
test.each(["?q=Engineer&status=Custom+review", "?q=absent"])("direct Board and both switch directions retain exact result membership: %s", async query => {
|
||||
show(`${query}&view=board`);
|
||||
const expected = query.includes("absent") ? [] : [2];
|
||||
await screen.findByText(`${expected.length} matching jobs`);
|
||||
await screen.findByText(expected.length === 1 ? "1 matching job" : `${expected.length} matching jobs`);
|
||||
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual(expected);
|
||||
expect(reads("/jobapplications")).toHaveLength(0);
|
||||
fireEvent.click(screen.getByRole("link", { name: "Table" }));
|
||||
await waitFor(() => expect(reads("/jobapplications")).toHaveLength(1));
|
||||
await waitFor(() => expect(ids("[data-job-row-id]", "data-job-row-id")).toEqual(expected));
|
||||
fireEvent.click(screen.getByRole("link", { name: "Board" }));
|
||||
await screen.findByText(`${expected.length} matching jobs`);
|
||||
await screen.findByText(expected.length === 1 ? "1 matching job" : `${expected.length} matching jobs`);
|
||||
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual(expected);
|
||||
expect(reads("/jobapplications/board").every(([, config]) => (config?.params as { q?: string }).q === (expected.length ? "Engineer" : "absent"))).toBe(true);
|
||||
});
|
||||
@@ -111,7 +127,7 @@ test("saved filters applied in Board reset the Table page and refresh only the a
|
||||
await screen.findByText("27 matching jobs");
|
||||
fireEvent.click(screen.getByRole("button", { name: t.savedViewsTooltip }));
|
||||
fireEvent.click(await screen.findByText("Review shortlist"));
|
||||
await screen.findByText("1 matching jobs");
|
||||
await screen.findByText("1 matching job");
|
||||
expect(screen.getByLabelText("Route")).not.toHaveTextContent("page=");
|
||||
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual([2]);
|
||||
const before = reads("/jobapplications/board").length;
|
||||
|
||||
Reference in New Issue
Block a user