Skip to content

Commit

Permalink
Merge 2672e4f into 56e66f1
Browse files Browse the repository at this point in the history
  • Loading branch information
lslezak committed Sep 5, 2023
2 parents 56e66f1 + 2672e4f commit 93287f8
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 22 deletions.
1 change: 1 addition & 0 deletions web/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"rfkill",
"sata",
"screenreader",
"sprintf",
"ssid",
"ssids",
"startup",
Expand Down
65 changes: 65 additions & 0 deletions web/src/L10nBackendWrapper.jsx
Original file line number Diff line number Diff line change
@@ -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;
}
98 changes: 98 additions & 0 deletions web/src/L10nBackendWrapper.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<InstallerClientProvider client={createClient}>
<L10nBackendWrapper>Testing content</L10nBackendWrapper>
</InstallerClientProvider>
);

// 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(
<InstallerClientProvider client={createClient}>
<L10nBackendWrapper>Testing content</L10nBackendWrapper>
</InstallerClientProvider>
);

await waitFor(() => expect(window.location.reload).toHaveBeenCalled());
// it uses the usual Linux locale format
expect(setLanguageFn).toHaveBeenCalledWith("pt_BR");
});
});
});
21 changes: 21 additions & 0 deletions web/src/client/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ class LanguageClient {
proxy.Locales = langIDs;
}

/**
* Returns the current backend locale
*
* @return {Promise<String>} 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<void>}
*/
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
*
Expand Down
47 changes: 25 additions & 22 deletions web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -75,29 +76,31 @@ root.render(
<L10nWrapper>
<LoginWrapper>
<InstallerClientProvider client={createClient}>
<SoftwareProvider>
<NotificationProvider>
<HashRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="/" element={<Main />}>
<Route index element={<Overview />} />
<Route path="/overview" element={<Overview />} />
<Route path="/l10n" element={<L10nPage />} />
<Route path="/storage" element={<StoragePage />} />
<Route path="/storage/iscsi" element={<ISCSIPage />} />
<Route path="/storage/dasd" element={<DASDPage />} />
<Route path="/storage/zfcp" element={<ZFCPPage />} />
<Route path="/network" element={<NetworkPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/issues" element={<IssuesPage />} />
<L10nBackendWrapper>
<SoftwareProvider>
<NotificationProvider>
<HashRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="/" element={<Main />}>
<Route index element={<Overview />} />
<Route path="/overview" element={<Overview />} />
<Route path="/l10n" element={<L10nPage />} />
<Route path="/storage" element={<StoragePage />} />
<Route path="/storage/iscsi" element={<ISCSIPage />} />
<Route path="/storage/dasd" element={<DASDPage />} />
<Route path="/storage/zfcp" element={<ZFCPPage />} />
<Route path="/network" element={<NetworkPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/issues" element={<IssuesPage />} />
</Route>
<Route path="products" element={<ProductSelectionPage />} />
</Route>
<Route path="products" element={<ProductSelectionPage />} />
</Route>
</Routes>
</HashRouter>
</NotificationProvider>
</SoftwareProvider>
</Routes>
</HashRouter>
</NotificationProvider>
</SoftwareProvider>
</L10nBackendWrapper>
</InstallerClientProvider>
</LoginWrapper>
</L10nWrapper>
Expand Down

0 comments on commit 93287f8

Please sign in to comment.