From 2672e4f5873edad9f152f7542d9118ee2a604693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 5 Sep 2023 13:56:50 +0200 Subject: [PATCH] Switch also the backend locale --- web/cspell.json | 1 + web/src/L10nBackendWrapper.jsx | 65 +++++++++++++++++++ web/src/L10nBackendWrapper.test.jsx | 98 +++++++++++++++++++++++++++++ web/src/client/language.js | 21 +++++++ web/src/index.js | 47 +++++++------- 5 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 web/src/L10nBackendWrapper.jsx create mode 100644 web/src/L10nBackendWrapper.test.jsx diff --git a/web/cspell.json b/web/cspell.json index 165130dd04..12cf6761e3 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -51,6 +51,7 @@ "rfkill", "sata", "screenreader", + "sprintf", "ssid", "ssids", "startup", diff --git a/web/src/L10nBackendWrapper.jsx b/web/src/L10nBackendWrapper.jsx new file mode 100644 index 0000000000..328a25abf5 --- /dev/null +++ b/web/src/L10nBackendWrapper.jsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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, { useEffect, useState } from "react"; +import { useCancellablePromise } from "~/utils"; +import { useInstallerClient } from "~/context/installer"; + +import cockpit from "./lib/cockpit"; + +/** + * This is a helper component to set the language used in the backend service. + * It ensures the backend service uses the same language as the web frontend. + * To activate a new language it reloads the whole page. + * + * It behaves like a wrapper, it just wraps the children components, it does + * not render any real content. + * + * @param {React.ReactNode} [props.children] - content to display within the + * wrapper + */ +export default function L10nBackendWrapper({ children }) { + const { language: client } = useInstallerClient(); + const { cancellablePromise } = useCancellablePromise(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const syncBackendLanguage = async () => { + // cockpit uses "pt-br" format, convert that to the usual Linux locale "pt_BR" style + let [lang, country] = cockpit.language.split("-"); + country = country?.toUpperCase(); + const cockpitLocale = lang + (country ? "_" + country : ""); + const currentLang = await cancellablePromise(client.getUILanguage()); + + if (currentLang !== cockpitLocale) { + await cancellablePromise(client.setUILanguage(cockpitLocale)); + // reload the whole page to force retranslation of all texts + window.location.reload(true); + } + }; + + syncBackendLanguage().catch(console.error) + .finally(() => setLoading(false)); + }, [client, cancellablePromise]); + + // display empty page while loading + return loading ? <> : children; +} diff --git a/web/src/L10nBackendWrapper.test.jsx b/web/src/L10nBackendWrapper.test.jsx new file mode 100644 index 0000000000..a05498db09 --- /dev/null +++ b/web/src/L10nBackendWrapper.test.jsx @@ -0,0 +1,98 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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"; +import { render, waitFor, screen } from "@testing-library/react"; + +import cockpit from "~/lib/cockpit"; + +import { createClient } from "~/client"; +import { InstallerClientProvider } from "~/context/installer"; +import L10nBackendWrapper from "~/L10nBackendWrapper"; + +jest.mock("~/client"); + +const backendLang = "en"; +const setLanguageFn = jest.fn(); + +beforeEach(() => { + // if defined outside, the mock is cleared automatically + createClient.mockImplementation(() => { + return { + language: { + getUILanguage: () => Promise.resolve(backendLang), + setUILanguage: (lang) => new Promise((resolve) => resolve(setLanguageFn(lang))) + } + }; + }); +}); + +describe("L10nBackendWrapper", () => { + // remember the original location object, we need to temporarily replace it with a mock + const origLocation = window.location; + const origLang = cockpit.language; + + // mock window.location.reload + beforeAll(() => { + delete window.location; + window.location = { + reload: jest.fn(), + }; + }); + + afterAll(() => { + window.location = origLocation; + cockpit.language = origLang; + }); + + describe("when the backend language is the same as in the frontend", () => { + it("displays the children content and does not reload", async () => { + cockpit.language = backendLang; + render( + + Testing content + + ); + + // children are displayed + await screen.findByText("Testing content"); + + expect(setLanguageFn).not.toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + }); + + describe("when the backend language is different as in the frontend", () => { + it("sets the backend language and reloads", async () => { + cockpit.language = "pt-br"; + + render( + + Testing content + + ); + + await waitFor(() => expect(window.location.reload).toHaveBeenCalled()); + // it uses the usual Linux locale format + expect(setLanguageFn).toHaveBeenCalledWith("pt_BR"); + }); + }); +}); diff --git a/web/src/client/language.js b/web/src/client/language.js index aaf277bfa1..e40ab1164c 100644 --- a/web/src/client/language.js +++ b/web/src/client/language.js @@ -80,6 +80,27 @@ class LanguageClient { proxy.Locales = langIDs; } + /** + * Returns the current backend locale + * + * @return {Promise} the locale string + */ + async getUILanguage() { + const proxy = await this.client.proxy(LANGUAGE_IFACE); + return proxy.UILocale; + } + + /** + * Set the backend language + * + * @param {String} lang the locale string + * @return {Promise} + */ + async setUILanguage(lang) { + const proxy = await this.client.proxy(LANGUAGE_IFACE); + proxy.UILocale = lang; + } + /** * Register a callback to run when properties in the Language object change * diff --git a/web/src/index.js b/web/src/index.js index 80f29d5630..e8b0b3e969 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -40,6 +40,7 @@ import App from "~/App"; import Main from "~/Main"; import DevServerWrapper from "~/DevServerWrapper"; import L10nWrapper from "~/L10nWrapper"; +import L10nBackendWrapper from "~/L10nBackendWrapper"; import { Overview } from "~/components/overview"; import { ProductSelectionPage } from "~/components/software"; import { ProposalPage as StoragePage, ISCSIPage, DASDPage, ZFCPPage } from "~/components/storage"; @@ -75,29 +76,31 @@ root.render( - - - - - }> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> - } /> - - - - - + + + + +