From e6001d62a9c1657af8593eaa6700e9de2ed4d654 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Wed, 7 Oct 2026 18:37:46 +0200 Subject: [PATCH] =?UTF-8?q?Corregir=20sincronizaci=C3=B3n=20de=20idioma=20?= =?UTF-8?q?tras=20login=20y=20reactividad=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CatalogPickerDataTable.tsx | 11 +++--- .../app/quotes/components/QuotesDataTable.tsx | 5 +-- .../src/app/quotes/hooks/useDetailColumns.tsx | 5 +-- .../ProtectedRoute/ProtectedRoute.test.tsx | 35 +++++++++++++++---- .../ProtectedRoute/ProtectedRoute.tsx | 21 +++++++---- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/client/src/app/quotes/components/CatalogPickerDataTable.tsx b/client/src/app/quotes/components/CatalogPickerDataTable.tsx index ee41709..07a88ec 100644 --- a/client/src/app/quotes/components/CatalogPickerDataTable.tsx +++ b/client/src/app/quotes/components/CatalogPickerDataTable.tsx @@ -7,15 +7,16 @@ import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { Button, Input } from "@/ui"; import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts"; import { CellContext, ColumnDef, createColumnHelper, Row, RowData } from "@tanstack/react-table"; -import { t } from "i18next"; import { PackagePlusIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; type QuantityValue = number | ""; type CatalogPickerTableMeta = { quantities: QuantityValue[]; + t: (key: string) => string; setQuantity: (index: number, quantity: QuantityValue) => void; selectArticle: (article: IListArticles_Response_DTO, index: number) => void; }; @@ -68,7 +69,7 @@ function AddArticleCell({ row, table }: CellContext - {t("common.add")} + {catalogPicker?.t("common.add")} ); } @@ -79,6 +80,7 @@ export const CatalogPickerDataTable = ({ onSelect: (data: unknown, quantity: number) => void; }) => { const navigate = useNavigate(); + const { t, i18n } = useTranslation(); const { pagination, globalFilter, isFiltered } = useDataTableContext(); const [quantities, setQuantities] = useState([]); @@ -111,12 +113,13 @@ export const CatalogPickerDataTable = ({ () => ({ catalogPicker: { quantities, + t, setQuantity: handleQuantity, selectArticle: (article, index) => onSelect(article, quantities[index] === "" ? 1 : (quantities[index] ?? 1)), }, }), - [handleQuantity, onSelect, quantities] + [handleQuantity, onSelect, quantities, t] ); const columns = useMemo>>( @@ -155,7 +158,7 @@ export const CatalogPickerDataTable = ({ cell: AddArticleCell, }), ], - [] + [t, i18n.resolvedLanguage] ); /*const columns2 = useMemo[]>(() => { diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index 75e14a9..192c9ac 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -12,9 +12,9 @@ import { Button, Card, CardContent, Tooltip, TooltipContent, TooltipTrigger } fr import { useToast } from "@/ui/use-toast"; import { IListQuotes_Response_DTO, UTCDateValue } from "@shared/contexts"; import { ColumnDef, Row } from "@tanstack/react-table"; -import { t } from "i18next"; import { ArchiveIcon, CopyIcon, DownloadIcon, EditIcon } from "lucide-react"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useQuotes } from "../hooks"; import { DownloadQuoteDialog } from "./DownloadQuoteDialog"; @@ -30,6 +30,7 @@ export const QuotesDataTable = ({ }) => { const navigate = useNavigate(); const { toast } = useToast(); + const { t, i18n } = useTranslation(); const tableId = useId(); const previewId = useId(); @@ -356,7 +357,7 @@ export const QuotesDataTable = ({ ]; return preview ? columns : [...columns, ...actionColumn]; - }, [preview]); + }, [preview, t, i18n.resolvedLanguage]); const { table } = useDataTable({ data: data?.items ?? [], diff --git a/client/src/app/quotes/hooks/useDetailColumns.tsx b/client/src/app/quotes/hooks/useDetailColumns.tsx index b8ce014..ff90c4c 100644 --- a/client/src/app/quotes/hooks/useDetailColumns.tsx +++ b/client/src/app/quotes/hooks/useDetailColumns.tsx @@ -6,7 +6,7 @@ import { Checkbox } from "@/ui"; import { ColumnDef, Row } from "@tanstack/react-table"; import { useMemo } from "react"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell"; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -19,6 +19,7 @@ export function useDetailColumns( rowActionFn?: DataTablaRowActionFunction; } = {} ): ColumnDef[] { + const { t, i18n } = useTranslation(); const { enableDragHandleColumn = false, enableSelectionColumn = false, @@ -94,5 +95,5 @@ export function useDetailColumns( } return columns; - }, [enableActionsColumn, enableDragHandleColumn, enableSelectionColumn]); + }, [enableActionsColumn, enableDragHandleColumn, enableSelectionColumn, t, i18n.resolvedLanguage]); } diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.test.tsx b/client/src/components/ProtectedRoute/ProtectedRoute.test.tsx index 7f5efb7..e723c4f 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.test.tsx +++ b/client/src/components/ProtectedRoute/ProtectedRoute.test.tsx @@ -6,6 +6,7 @@ import { ProtectedRoute } from "./ProtectedRoute"; const useIsLoggedIn = jest.fn(); const useGetProfile = jest.fn(); const changeLanguage = jest.fn(); +const i18n = { language: "en", resolvedLanguage: "en", changeLanguage }; jest.mock("@/lib/hooks", () => ({ useIsLoggedIn: () => useIsLoggedIn(), @@ -18,12 +19,11 @@ jest.mock("@/components", () => ({ })); jest.mock("react-i18next", () => ({ - useTranslation: () => ({ i18n: { language: "en", changeLanguage } }), + useTranslation: () => ({ i18n }), })); -const renderProtectedRoute = () => { - const container = document.createElement("div"); - document.body.appendChild(container); +const renderProtectedRoute = (container = document.createElement("div")) => { + if (!container.parentElement) document.body.appendChild(container); act(() => { renderDom( @@ -54,6 +54,8 @@ describe("ProtectedRoute", () => { beforeEach(() => { changeLanguage.mockClear(); + i18n.language = "en"; + i18n.resolvedLanguage = "en"; useIsLoggedIn.mockReturnValue({ isSuccess: true, isLoading: false, @@ -75,7 +77,7 @@ describe("ProtectedRoute", () => { expect(container.querySelector("[data-testid='login']")).toBeNull(); }); - it("renders the protected route after resolving a valid profile", () => { + it("waits for the profile language before mounting the protected route", () => { useGetProfile.mockReturnValue({ isSuccess: true, isLoading: false, @@ -85,8 +87,29 @@ describe("ProtectedRoute", () => { const container = renderProtectedRoute(); - expect(container.querySelector("[data-testid='quotes']")).not.toBeNull(); + expect(container.querySelector("[data-testid='loading']")).not.toBeNull(); + expect(container.querySelector("[data-testid='quotes']")).toBeNull(); expect(changeLanguage).toHaveBeenCalledWith("es"); + + i18n.language = "es"; + i18n.resolvedLanguage = "es"; + renderProtectedRoute(container); + + expect(container.querySelector("[data-testid='quotes']")).not.toBeNull(); + }); + + it("mounts immediately when the active language already matches the profile", () => { + useGetProfile.mockReturnValue({ + isSuccess: true, + isLoading: false, + isFetching: false, + data: { id: "current-user", lang_code: "en" }, + }); + + const container = renderProtectedRoute(); + + expect(container.querySelector("[data-testid='quotes']")).not.toBeNull(); + expect(changeLanguage).not.toHaveBeenCalled(); }); it("renders an error instead of treating a profile request failure as a logout", () => { diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.tsx b/client/src/components/ProtectedRoute/ProtectedRoute.tsx index fd863e3..bbf2c10 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute/ProtectedRoute.tsx @@ -29,13 +29,22 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => { enabled: isLoggedInSuccess && authenticated, }); - React.useEffect(() => { - if (isProfileSuccess && profile?.lang_code && i18n.language !== profile.lang_code) { - void i18n.changeLanguage(profile.lang_code); - } - }, [isProfileSuccess, profile?.lang_code, i18n]); + const profileLanguage = profile?.lang_code; + const activeLanguage = i18n.resolvedLanguage ?? i18n.language; + const isLanguageSynchronized = !profileLanguage || activeLanguage === profileLanguage; - if (isLoggedInLoading || isProfileLoading || (isProfileSuccess && profile === null && isProfileFetching)) { + React.useEffect(() => { + if (isProfileSuccess && profileLanguage && !isLanguageSynchronized) { + void i18n.changeLanguage(profileLanguage); + } + }, [isProfileSuccess, profileLanguage, isLanguageSynchronized, i18n]); + + if ( + isLoggedInLoading || + isProfileLoading || + (isProfileSuccess && profile === null && isProfileFetching) || + (isProfileSuccess && profile?.id && !isLanguageSynchronized) + ) { return ; }