diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 0d7c9a0ba0..b4ef3b3fc6 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -20,8 +20,9 @@ }, "sourceType": "module" }, - "plugins": ["flowtype", "react", "react-hooks", "@typescript-eslint"], + "plugins": ["flowtype", "i18next", "react", "react-hooks", "@typescript-eslint"], "rules": { + "i18next/no-literal-string": "error", "indent": ["error", 2, { "ObjectExpression": "first", @@ -58,6 +59,15 @@ "space-before-function-paren": "off", "n/no-callback-literal": "off" }, + "overrides": [ + { + // do not check translations in the testing or development files + "files": ["*.test.*", "test-utils.js", "DevServerWrapper.jsx"], + "rules": { + "i18next/no-literal-string": "off" + } + } + ], "globals": { "require": false, "module": false, diff --git a/web/package-lock.json b/web/package-lock.json index 7e30ec6a77..79139409e5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -55,6 +55,7 @@ "eslint-config-standard-jsx": "^11.0.0", "eslint-config-standard-react": "^13.0.0", "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-i18next": "^6.0.3", "eslint-plugin-import": "^2.22.1", "eslint-plugin-n": "^15.5.1", "eslint-plugin-node": "^11.1.0", @@ -8152,6 +8153,19 @@ "eslint": "^8.1.0" } }, + "node_modules/eslint-plugin-i18next": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-6.0.3.tgz", + "integrity": "sha512-RtQXYfg6PZCjejIQ/YG+dUj/x15jPhufJ9hUDGH0kCpJ6CkVMAWOQ9exU1CrbPmzeykxLjrXkjAaOZF/V7+DOA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-plugin-import": { "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", @@ -15331,6 +15345,15 @@ "node": ">=0.10.0" } }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -24305,6 +24328,16 @@ "string-natural-compare": "^3.0.1" } }, + "eslint-plugin-i18next": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-6.0.3.tgz", + "integrity": "sha512-RtQXYfg6PZCjejIQ/YG+dUj/x15jPhufJ9hUDGH0kCpJ6CkVMAWOQ9exU1CrbPmzeykxLjrXkjAaOZF/V7+DOA==", + "dev": true, + "requires": { + "lodash": "^4.17.21", + "requireindex": "~1.1.0" + } + }, "eslint-plugin-import": { "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", @@ -29431,6 +29464,12 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", diff --git a/web/package.json b/web/package.json index 321c55f518..7ffb78a8cf 100644 --- a/web/package.json +++ b/web/package.json @@ -56,6 +56,7 @@ "eslint-config-standard-jsx": "^11.0.0", "eslint-config-standard-react": "^13.0.0", "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-i18next": "^6.0.3", "eslint-plugin-import": "^2.22.1", "eslint-plugin-n": "^15.5.1", "eslint-plugin-node": "^11.1.0", diff --git a/web/src/App.jsx b/web/src/App.jsx index d41c84b713..34fec4778d 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -96,6 +96,8 @@ function App() { + {/* this is the name of the tool, do not translate it */} + {/* eslint-disable-next-line i18next/no-literal-string */} Agama diff --git a/web/src/components/core/Sidebar.jsx b/web/src/components/core/Sidebar.jsx index d51629b172..c55c28fa90 100644 --- a/web/src/components/core/Sidebar.jsx +++ b/web/src/components/core/Sidebar.jsx @@ -115,6 +115,8 @@ export default function Sidebar ({ children }) { } targetInfo = ( + /* this is only displayed in the development mode, not in production, do not translate it */ + /* eslint-disable-next-line i18next/no-literal-string */ Target server: { " " } diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 24cbc016b7..f903001b5e 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -20,6 +20,9 @@ */ import React from 'react'; +import format from "format-util"; + +import { _ } from "~/i18n"; // NOTE: "@icons" is an alias to use a shorter path to real @material-symbols // icons location. Check the tsconfig.json file to see its value. @@ -149,5 +152,5 @@ export default function Icon({ name, className = "", size = 32, ...otherProps }) return (IconComponent) ? - : icon {name} not found!; + : {format(_("Icon %s not found!"), name)}; } diff --git a/web/src/components/layout/Icon.test.jsx b/web/src/components/layout/Icon.test.jsx index d4ed6eaf79..b775a23470 100644 --- a/web/src/components/layout/Icon.test.jsx +++ b/web/src/components/layout/Icon.test.jsx @@ -35,6 +35,6 @@ describe("when given a known name", () => { describe("when given an unknown name", () => { it("renders an informative text", async () => { plainRender(); - await screen.findByText("icon apsens not found!", { name: /options/i }); + await screen.findByText("Icon apsens not found!", { name: /options/i }); }); }); diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index 74f10af0e4..8d7fa17c8c 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -35,7 +35,7 @@ import { _ } from "~/i18n"; const NoWiredConnections = () => { return ( - No wired connections found + {_("No wired connections found")} ); }; diff --git a/web/src/components/storage/DeviceSelector.jsx b/web/src/components/storage/DeviceSelector.jsx index bce1190af2..7128e7873b 100644 --- a/web/src/components/storage/DeviceSelector.jsx +++ b/web/src/components/storage/DeviceSelector.jsx @@ -131,7 +131,8 @@ const ItemContent = ({ device }) => { const members = device.members.map(m => m.split("/").at(-1)); - return Members: {members.sort().join(", ")}; + // TRANSLATORS: RAID details, %s is replaced by list of devices used by the array + return {format(_("Members: %s"), members.sort().join(", "))}; }; const RAIDInfo = () => { @@ -139,7 +140,8 @@ const ItemContent = ({ device }) => { const devices = device.devices.map(m => m.split("/").at(-1)); - return Devices: {devices.sort().join(", ")}; + // TRANSLATORS: RAID details, %s is replaced by list of devices used by the array + return {format(_("Devices: %s"), devices.sort().join(", "))}; }; const MultipathInfo = () => { @@ -147,7 +149,8 @@ const ItemContent = ({ device }) => { const wires = device.wires.map(m => m.split("/").at(-1)); - return Wires: {wires.sort().join(", ")}; + // TRANSLATORS: multipath details, %s is replaced by list of connections used by the device + return {format(_("Wires: %s"), wires.sort().join(", "))}; }; return ( diff --git a/web/src/components/storage/ProposalPageOptions.jsx b/web/src/components/storage/ProposalPageOptions.jsx index 447315c4fe..e22a7196ec 100644 --- a/web/src/components/storage/ProposalPageOptions.jsx +++ b/web/src/components/storage/ProposalPageOptions.jsx @@ -57,7 +57,7 @@ const ZFCPLink = () => { href={href} description={_("Activate disks")} > - zFCP + {_("zFCP")} ); }; @@ -75,7 +75,7 @@ const ISCSILink = () => { href={href} description={_("Connect to iSCSI targets")} > - iSCSI + {_("iSCSI")} ); };