diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 4ec6144bd9..5d8009eadf 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Feb 20 15:00:44 UTC 2026 - Imobach Gonzalez Sosa + +- Fix the language change (gh#agama-project/agama#3197). + ------------------------------------------------------------------- Thu Feb 19 13:35:30 UTC 2026 - Imobach Gonzalez Sosa diff --git a/web/src/agama.ts b/web/src/agama.ts index a8714621c2..df63129b53 100644 --- a/web/src/agama.ts +++ b/web/src/agama.ts @@ -35,9 +35,9 @@ const agama = { language: "en", // set the current translations, called from po..js - locale: (po) => { + locale: (po: object) => { if (po) { - Object.assign(translations, po); + translations = { ...po }; const header = po[""]; if (header) { diff --git a/web/src/components/core/InstallerL10nOptions.test.tsx b/web/src/components/core/InstallerL10nOptions.test.tsx index f270da1d02..5c8af6a8df 100644 --- a/web/src/components/core/InstallerL10nOptions.test.tsx +++ b/web/src/components/core/InstallerL10nOptions.test.tsx @@ -93,7 +93,7 @@ jest.mock("~/hooks/model/status", () => ({ })); jest.mock("~/context/installerL10n", () => ({ - ...jest.requireActual("~/context/installerL10n"), + InstallerL10nProvider: ({ children }) => <>{children}, useInstallerL10n: () => ({ keymap: "us", language: "de-DE", diff --git a/web/src/components/product/LicenseDialog.test.tsx b/web/src/components/product/LicenseDialog.test.tsx index 18a481d7f8..4eb257ce7c 100644 --- a/web/src/components/product/LicenseDialog.test.tsx +++ b/web/src/components/product/LicenseDialog.test.tsx @@ -119,7 +119,7 @@ describe("LicenseDialog", () => { const { user } = installerRender(, { withL10n: true, }); - const closeButton = screen.getByRole("button", { name: "Schließen" }); + const closeButton = await screen.findByRole("button", { name: "Schließen" }); await user.click(closeButton); expect(onCloseFn).toHaveBeenCalled(); }); diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index 226abb0ff1..848a68d4b4 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -87,7 +87,7 @@ jest.mock("~/hooks/model/config/product", () => ({ })); jest.mock("~/context/installerL10n", () => ({ - ...jest.requireActual("~/context/installerL10n"), + InstallerL10nProvider: ({ children }) => <>{children}, useInstallerL10n: () => ({ keymap: "us", language: "de-DE", diff --git a/web/src/context/__mocks__/installerL10n.tsx b/web/src/context/__mocks__/installerL10n.tsx new file mode 100644 index 0000000000..556e878cf3 --- /dev/null +++ b/web/src/context/__mocks__/installerL10n.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) [2025-2026] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; + +const L10nContext = React.createContext(null); + +export const useInstallerL10n = () => { + const context = React.useContext(L10nContext); + if (!context) { + throw new Error("useInstallerL10n must be used within a InstallerL10nContext"); + } + return context; +}; + +export const InstallerL10nProvider = ({ children }) => { + const value = { + language: "en-US", + loadedLanguage: "en-US", + keymap: "us", + changeLanguage: jest.fn(), + changeKeymap: jest.fn(), + }; + + return {children}; +}; diff --git a/web/src/context/installerL10n.test.tsx b/web/src/context/installerL10n.test.tsx index 90c3f3fb41..7c598be602 100644 --- a/web/src/context/installerL10n.test.tsx +++ b/web/src/context/installerL10n.test.tsx @@ -20,11 +20,14 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { Suspense } from "react"; import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { InstallerL10nProvider, useInstallerL10n } from "~/context/installerL10n"; import { InstallerClientProvider } from "./installer"; +jest.unmock("~/context/installerL10n"); + const mockUseSystemFn = jest.fn(); const mockConfigureL10nFn = jest.fn(); @@ -81,12 +84,20 @@ describe("InstallerL10nProvider", () => { }); it("sets the language from the backend", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + render( - - - - - , + + + + + + + + + , ); await waitFor(() => screen.getByText("hola")); diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index c9b9d4a17f..ae446275eb 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback } from "react"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import agama from "~/agama"; import supportedLanguages from "~/languages.json"; import { useSystem } from "~/hooks/model/system"; @@ -115,13 +116,16 @@ function findSupportedLanguage(languages: Array): string | undefined { * @param locale requested locale * @returns Promise with a dynamic import */ -async function loadTranslations(locale: string) { +async function loadTranslations(locale: string): Promise { // load the translations dynamically, first try the language + territory return import( /* webpackChunkName: "[request]" */ `../po/po.${locale}` ) - .then((m) => agama.locale(m.default)) + .then((m) => { + agama.locale(m.default); + return agama.language.replace("-", "_"); + }) .catch(async () => { // if it fails try the language only const po = locale.split("-")[0]; @@ -129,13 +133,17 @@ async function loadTranslations(locale: string) { /* webpackChunkName: "[request]" */ `../po/po.${po}` ) - .then((m) => agama.locale(m.default)) + .then((m) => { + agama.locale(m.default); + return agama.language.replace("-", "_"); + }) .catch(() => { - if (locale !== "en-US") { + if (locale && locale !== "en-US") { console.error("Cannot load frontend translations for", locale); } // reset the current translations (use the original English texts) agama.locale(null); + return agama.language.replace("-", "_"); }); }); } @@ -162,13 +170,19 @@ function InstallerL10nProvider({ initialLanguage?: string; children?: React.ReactNode; }) { - const { l10n } = useSystem(); - const [loadedLanguage, setLoadedLanguage] = useState(initialLanguage); + const queryClient = useQueryClient(); + const system = useSystem(); + const l10n = system?.l10n; const locale = l10n?.locale; - const language = locale ? languageFromLocale(locale) : initialLanguage; + const language = locale ? languageFromLocale(locale) : initialLanguage || "en-US"; const keymap = l10n?.keymap; + const { data: loadedLanguage } = useSuspenseQuery({ + queryKey: ["translations", language], + queryFn: () => loadTranslations(language), + }); + const changeLanguage = useCallback( async (lang: string) => { const candidateLanguages = [ @@ -178,26 +192,32 @@ function InstallerL10nProvider({ ].filter((l) => l); const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US"; document.documentElement.lang = newLanguage.split("-")[0]; + await configureL10nAction({ locale: languageToLocale(newLanguage) }); + await queryClient.invalidateQueries({ queryKey: ["translations"] }); }, - [language], + [language, queryClient], ); const changeKeymap = useCallback(async (id: string) => { await configureL10nAction({ keymap: id }); }, []); - useEffect(() => { - if (!language) return; - - loadTranslations(language).then(() => setLoadedLanguage(agama.language.replace("-", "_"))); - }, [language, setLoadedLanguage]); - - if (!loadedLanguage) return null; - - const value = { loadedLanguage, language, changeLanguage, keymap, changeKeymap }; - - return {children}; + const value = { + loadedLanguage, + language, + changeLanguage, + keymap, + changeKeymap, + }; + + // Setting the key forces to reload the children when the language changes + // (see https://react.dev/learn/preserving-and-resetting-state). + return ( + + {children} + + ); } export { InstallerL10nProvider, useInstallerL10n }; diff --git a/web/src/setupTests.ts b/web/src/setupTests.ts index 1f940b3cd9..f9837a1ede 100644 --- a/web/src/setupTests.ts +++ b/web/src/setupTests.ts @@ -11,3 +11,5 @@ if (!globalThis.TextEncoder || !globalThis.TextDecoder) { globalThis.TextEncoder = NodeTextEncoder as typeof TextEncoder; globalThis.TextDecoder = NodeTextDecoder as typeof TextDecoder; } + +jest.mock("~/context/installerL10n");