diff --git a/web/cspell.json b/web/cspell.json index 940b7fae66..165130dd04 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -44,6 +44,7 @@ "mgmt", "mmcblk", "multipath", + "ngettext", "onboot", "partitioner", "patternfly", diff --git a/web/src/components/core/FileViewer.jsx b/web/src/components/core/FileViewer.jsx index f23aab4f00..97370e1f22 100644 --- a/web/src/components/core/FileViewer.jsx +++ b/web/src/components/core/FileViewer.jsx @@ -23,12 +23,10 @@ import React, { useState, useEffect } from "react"; import { Popup } from "~/components/core"; import { Alert } from "@patternfly/react-core"; import { Loading } from "~/components/layout"; +import { _ } from "~/i18n"; import cockpit from "../../lib/cockpit"; -// FIXME: replace by a wrapper, this is just for testing -const _ = cockpit.gettext; - export default function FileViewer({ file, title, onCloseCallback }) { // the popup is visible const [isOpen, setIsOpen] = useState(true); diff --git a/web/src/i18n.js b/web/src/i18n.js new file mode 100644 index 0000000000..275edb6663 --- /dev/null +++ b/web/src/i18n.js @@ -0,0 +1,103 @@ +/* + * 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. + */ + +/** + * This is a wrapper module for i18n functions. Currently it uses the cockpit + * implementation but the wrapper allows easy transition to another backend if + * needed. + */ + +import cockpit from "./lib/cockpit"; + +/** + * Returns a translated text in the current locale or the original text if the + * translation is not found. + * + * @param {string} str the input string to translate + * @return {string} translated or original text + */ +const _ = (str) => cockpit.gettext(str); + +/** + * Similar to the _() function. This variant returns singular or plural form + * depending on an additional "num" argument. + * + * @see {@link _} for further information + * @param {string} str1 the input string in the singular form + * @param {string} strN the input string in the plural form + * @param {number} n the actual number which decides whether to use the + * singular or plural form + * @return {string} translated or original text + */ +const n_ = (str1, strN, n) => cockpit.ngettext(str1, strN, n); + +/** + * This is a no-op function, it can be used only for marking the text for + * translation so it is extracted to the POT file but the text itself is not + * translated. It needs to be translated by the _() function later. + * + * @example Error messages + * try { + * ... + * // the exception contains untranslated string + * throw(N_("Download failed")); + * } catch (error) { + * // log the untranslated error + * console.log(error); + * // for users display the translated error + * return
Error: {_(error)}
; + * } + * + * @example Constants + * // ERROR_MSG will not be translated, but the string will be found + * // by gettext when creating the POT file + * const ERROR_MSG = N_("Download failed"); + * const OK_MSG = N_("Success"); + * + * // assume that "result" contains one of the constants above + * const result = ...; + * // here the string will be translated using the current locale + * return
Result: {_(result)}
; + * + * @param {string} str the input string + * @return {string} the input string + */ +const N_ = (str) => str; + +/** + * Similar to the N_() function, but for the singular and plural form. + * + * @see {@link N_} for further information + * @param {string} str1 the input string in the singular form + * @param {string} strN the input string in the plural form + * @param {number} n the actual number which decides whether to use the + * singular or plural form + * @return {string} the original text, either "string1" or "stringN" depending + * on the value "num" + */ +const Nn_ = (str1, strN, n) => (n === 1) ? str1 : strN; + +export { + _, + n_, + N_, + Nn_ +}; diff --git a/web/src/i18n.test.js b/web/src/i18n.test.js new file mode 100644 index 0000000000..6cf3513155 --- /dev/null +++ b/web/src/i18n.test.js @@ -0,0 +1,78 @@ +/* + * 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 { _, n_, N_, Nn_ } from "~/i18n"; +import cockpit from "./lib/cockpit"; + +// mock the cockpit gettext functions +jest.mock("./lib/cockpit"); +const gettextFn = jest.fn(); +cockpit.gettext.mockImplementation(gettextFn); +const ngettextFn = jest.fn(); +cockpit.ngettext.mockImplementation(ngettextFn); + +// some testing texts +const text = "text to translate"; +const singularText = "singular text to translate"; +const pluralText = "plural text to translate"; + +describe("i18n", () => { + describe("_", () => { + it("calls the cockpit.gettext() implementation", () => { + _(text); + + expect(gettextFn).toHaveBeenCalledWith(text); + }); + }); + + describe("n_", () => { + it("calls the cockpit.ngettext() implementation", () => { + n_(singularText, pluralText, 1); + + expect(ngettextFn).toHaveBeenCalledWith(singularText, pluralText, 1); + }); + }); + + describe("N_", () => { + it("returns the original text and does not translate it", () => { + const val = N_(text); + + // test the object identity + expect(Object.is(val, text)).toBe(true); + }); + }); + + describe("Nn_", () => { + it("returns the singular form for value 1 and does not translate it", () => { + const val = Nn_(singularText, pluralText, 1); + + // test the object identity + expect(Object.is(val, singularText)).toBe(true); + }); + + it("returns the plural form for value 42 and does not translate it", () => { + const val = Nn_(singularText, pluralText, 42); + + // test the object identity + expect(Object.is(val, pluralText)).toBe(true); + }); + }); +});