diff --git a/.github/workflows/weblate-update-pot.yml b/.github/workflows/weblate-update-pot.yml index 8402c76d1e..a833ce1284 100644 --- a/.github/workflows/weblate-update-pot.yml +++ b/.github/workflows/weblate-update-pot.yml @@ -32,15 +32,10 @@ jobs: path: agama - name: Generate POT file - # TODO: use a shared script for this run: | cd agama/web - xgettext --default-domain=agama --output=agama.pot --language=JavaScript --keyword= \ - --keyword=_:1 --keyword=N_:1 --keyword=n_:1,2,3t --keyword=Nn_:1,2,3t \ - --foreign-user --copyright-holder="SuSE Linux Products GmbH, Nuernberg" \ - --from-code=UTF-8 --add-comments=TRANSLATORS --sort-by-file \ - $(find . ! -name cockpit.js -name '*.js' -o ! -name '*.test.jsx' -name '*.jsx') - msgfmt --statistics agama.pot + ./build_pot + msgfmt --statistics agama.pot - name: Validate the generated POT file run: msgfmt --check-format agama/web/agama.pot diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 32965826ff..0d7c9a0ba0 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -32,6 +32,7 @@ }], "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], "no-var": "error", + "no-multi-str": "off", "no-use-before-define": "off", "@typescript-eslint/no-unused-vars": ["warn", { "ignoreRestSiblings": true }], "@typescript-eslint/no-use-before-define": "warn", diff --git a/web/build_pot b/web/build_pot new file mode 100755 index 0000000000..cfc334b9ce --- /dev/null +++ b/web/build_pot @@ -0,0 +1,17 @@ +#! /bin/sh + +# This script builds the POT file with the translatable texts from the web UI. + +# always run in the directory of this script to ensure the file paths +# saved in the POT file are always the same +cd "$(dirname "$0")" + +OUTPUT="${1:-agama.pot}" + +find . ! -name cockpit.js ! -name '*.test.js' -name '*.js' -o ! -name '*.test.jsx' -name '*.jsx' | +grep -vE "node_modules/|dist/|coverage/" | \ +xgettext --default-domain=agama --output=- --language=C --files-from=- \ + --keyword= --keyword=_:1 --keyword=N_:1 --keyword=n_:1,2,3t --keyword=Nn_:1,2,3t \ + --foreign-user --copyright-holder="SuSE Linux Products GmbH, Nuernberg" \ + --from-code=UTF-8 --add-comments=TRANSLATORS --sort-by-file | \ + sed '/^#/ s/, c-format/, javascript-format/' > "$OUTPUT" diff --git a/web/package-lock.json b/web/package-lock.json index 320f05d1e1..7e30ec6a77 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "@patternfly/react-table": "^4.113.0", "core-js": "^3.21.1", "fast-sort": "^3.2.1", + "format-util": "^1.0.5", "ipaddr.js": "^2.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -9174,6 +9175,11 @@ "node": ">= 6" } }, + "node_modules/format-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", + "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -24939,6 +24945,11 @@ "mime-types": "^2.1.12" } }, + "format-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", + "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", diff --git a/web/package.json b/web/package.json index 6d617e9ff8..321c55f518 100644 --- a/web/package.json +++ b/web/package.json @@ -102,6 +102,7 @@ "@patternfly/react-table": "^4.113.0", "core-js": "^3.21.1", "fast-sort": "^3.2.1", + "format-util": "^1.0.5", "ipaddr.js": "^2.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/web/src/components/core/About.jsx b/web/src/components/core/About.jsx index e4cf5efe3b..3f48059b85 100644 --- a/web/src/components/core/About.jsx +++ b/web/src/components/core/About.jsx @@ -21,12 +21,12 @@ import React, { useState } from "react"; import { Button, Text } from "@patternfly/react-core"; +import format from "format-util"; + import { Icon } from "~/components/layout"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; -import cockpit from "~/lib/cockpit"; - export default function About() { const [isOpen, setIsOpen] = useState(false); @@ -50,19 +50,19 @@ export default function About() { { // TRANSLATORS: content of the "About" popup (1/2) - _(`Agama is an experimental installer for (open)SUSE systems. It -still under development so, please, do not use it in -production environments. If you want to give it a try, we -recommend to use a virtual machine to prevent any possible -data loss.`) + _("Agama is an experimental installer for (open)SUSE systems. It \ +still under development so, please, do not use it in \ +production environments. If you want to give it a try, we \ +recommend to use a virtual machine to prevent any possible \ +data loss.") } { - cockpit.format( + format( // TRANSLATORS: content of the "About" popup (2/2) - // $0 is replaced by the project URL - _("For more information, please visit the project's repository at $0."), + // %s is replaced by the project URL + _("For more information, please visit the project's repository at %s."), "https://github.com/openSUSE/agama" ) } diff --git a/web/src/components/core/ValidationErrors.jsx b/web/src/components/core/ValidationErrors.jsx index 9342a18e68..c34aba1bec 100644 --- a/web/src/components/core/ValidationErrors.jsx +++ b/web/src/components/core/ValidationErrors.jsx @@ -29,12 +29,11 @@ import { ListItem, Popover } from "@patternfly/react-core"; +import format from "format-util"; import { Icon } from '~/components/layout'; import { _, n_ } from "~/i18n"; -import cockpit from "~/lib/cockpit"; - /** * @param {import("~/client/mixins").ValidationError[]} errors - Validation errors * @return React.JSX @@ -82,9 +81,9 @@ const ValidationErrors = ({ title = _("Errors"), errors }) => { onClick={() => setPopoverVisible(true)} > { warningIcon } { - cockpit.format( - // TRANSLATORS: $0 is replaced with the number of errors found - n_("$0 error found", "$0 errors found", errors.length), + format( + // TRANSLATORS: %d is replaced with the number of errors found + n_("%d error found", "%d errors found", errors.length), errors.length ) } diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.jsx index f795fc6265..80e2ff7aa7 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.jsx @@ -21,13 +21,13 @@ import React from "react"; import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import format from "format-util"; + import { RowActions } from "~/components/core"; import { Icon } from "~/components/layout"; import { formatIp } from "~/client/network/utils"; import { _ } from "~/i18n"; -import cockpit from "~/lib/cockpit"; - /** * @typedef {import("~/client/network/model").Connection} Connection */ @@ -66,15 +66,15 @@ export default function ConnectionsTable ({ { title: _("Edit"), "aria-label": - // TRANSLATORS: $0 is replaced by a network connection name - cockpit.format(_("Edit connection $0"), connection.name), + // TRANSLATORS: %s is replaced by a network connection name + format(_("Edit connection %s"), connection.name), onClick: () => onEdit(connection) }, typeof onForget === 'function' && { title: _("Forget"), "aria-label": - // TRANSLATORS: $0 is replaced by a network connection name - cockpit.format(_("Forget connection $0"), connection.name), + // TRANSLATORS: %s is replaced by a network connection name + format(_("Forget connection %s"), connection.name), className: "danger-action", icon: , onClick: () => onForget(connection) @@ -88,8 +88,8 @@ export default function ConnectionsTable ({ diff --git a/web/src/components/network/IpSettingsForm.jsx b/web/src/components/network/IpSettingsForm.jsx index 42b6d1db45..2a61cf8c5e 100644 --- a/web/src/components/network/IpSettingsForm.jsx +++ b/web/src/components/network/IpSettingsForm.jsx @@ -21,14 +21,13 @@ import React, { useState } from "react"; import { HelperText, HelperTextItem, Form, FormGroup, FormSelect, FormSelectOption, TextInput } from "@patternfly/react-core"; +import format from "format-util"; import { useInstallerClient } from "~/context/installer"; import { Popup } from "~/components/core"; import { AddressesDataList, DnsDataList } from "~/components/network"; import { _ } from "~/i18n"; -import cockpit from "~/lib/cockpit"; - const METHODS = { MANUAL: "manual", AUTO: "auto" @@ -127,8 +126,9 @@ export default function IpSettingsForm({ connection, onClose }) { }; // TRANSLATORS: manual network configuration mode with a static IP address + // %s is replaced by the connection name return ( - +
diff --git a/web/src/components/network/WifiNetworkListItem.jsx b/web/src/components/network/WifiNetworkListItem.jsx index 73374efdb4..085484bf93 100644 --- a/web/src/components/network/WifiNetworkListItem.jsx +++ b/web/src/components/network/WifiNetworkListItem.jsx @@ -26,6 +26,7 @@ import { Spinner, Text } from "@patternfly/react-core"; +import format from "format-util"; import { ConnectionState } from "~/client/network/model"; @@ -33,8 +34,6 @@ import { Icon } from "~/components/layout"; import { WifiNetworkMenu, WifiConnectionForm } from "~/components/network"; import { _ } from "~/i18n"; -import cockpit from "~/lib/cockpit"; - const networkState = (state) => { switch (state) { case ConnectionState.ACTIVATING: @@ -90,8 +89,8 @@ function WifiNetworkListItem ({ network, isSelected, isActive, onSelect, onCance onClick={onSelect} />
- {/* TRANSLATORS: $0 is replaced by a WiFi network name */} - {showSpinner && } + {/* TRANSLATORS: %s is replaced by a WiFi network name */} + {showSpinner && } { showSpinner && !network.connection && _("Connecting") } { networkState(network.connection?.state)}