Skip to content

Commit

Permalink
Improve translation handling
Browse files Browse the repository at this point in the history
- Use C placeholder format (%s, %d) in the texts
- Use the C language in xgettext (surprisingly handles JSX files better
  than the JavaScript language)
  • Loading branch information
lslezak committed Aug 11, 2023
1 parent ab2e5c9 commit 75a4ef6
Show file tree
Hide file tree
Showing 11 changed files with 61 additions and 39 deletions.
9 changes: 2 additions & 7 deletions .github/workflows/weblate-update-pot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions web/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions web/build_pot
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 10 additions & 10 deletions web/src/components/core/About.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -50,19 +50,19 @@ export default function About() {
<Text>
{
// 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.")
}
</Text>
<Text>
{
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"
)
}
Expand Down
9 changes: 4 additions & 5 deletions web/src/components/core/ValidationErrors.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
}
Expand Down
16 changes: 8 additions & 8 deletions web/src/components/network/ConnectionsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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: <Icon name="delete" size="24" />,
onClick: () => onForget(connection)
Expand All @@ -88,8 +88,8 @@ export default function ConnectionsTable ({
<Td isActionCell>
<RowActions
id={`actions-for-connection-${connection.id}`}
// TRANSLATORS: $0 is replaced by a network connection name
aria-label={cockpit.format(_("Actions for connection $0"), connection.name)}
// TRANSLATORS: %s is replaced by a network connection name
aria-label={format(_("Actions for connection %s"), connection.name)}
actions={actions}
connection={connection}
/>
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/network/IpSettingsForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 (
<Popup isOpen title={cockpit.format(_("Edit $0 connection"), connection.name)}>
<Popup isOpen title={format(_("Edit %s connection"), connection.name)}>
<Form id="edit-connection" onSubmit={onSubmit}>
<FormGroup fieldId="method" label={_("Mode")} isRequired>
<FormSelect
Expand Down
3 changes: 1 addition & 2 deletions web/src/components/network/WifiConnectionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback
<FormGroup fieldId="security" label={_("Security")}>
<FormSelect
id="security"
// TRANSLATORS: Wifi security configuration (password protected or not)
aria-label={_("security")}
aria-label={_("Security")}
value={security}
onChange={setSecurity}
>
Expand Down
7 changes: 3 additions & 4 deletions web/src/components/network/WifiNetworkListItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import {
Spinner,
Text
} from "@patternfly/react-core";
import { format } from "format-util";

import { ConnectionState } from "~/client/network/model";

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:
Expand Down Expand Up @@ -90,8 +89,8 @@ function WifiNetworkListItem ({ network, isSelected, isActive, onSelect, onCance
onClick={onSelect}
/>
<div className="split">
{/* TRANSLATORS: $0 is replaced by a WiFi network name */}
{showSpinner && <Spinner isSVG size="md" aria-label={cockpit.format(_("$0 connection is waiting for an state change"), network.ssid)} /> }
{/* TRANSLATORS: %s is replaced by a WiFi network name */}
{showSpinner && <Spinner isSVG size="md" aria-label={format(_("%s connection is waiting for an state change"), network.ssid)} /> }
<Text component="small" className="keep-words">
{ showSpinner && !network.connection && _("Connecting") }
{ networkState(network.connection?.state)}
Expand Down

0 comments on commit 75a4ef6

Please sign in to comment.