From 6afbd21fd1571594ae373911331cc5e1f2ea4006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Wed, 3 Jan 2024 20:58:40 +0100 Subject: [PATCH] feat(web): convert most strings to translation keys --- .../apps/mobile/src/{ => @types}/i18next.d.ts | 0 .../apps/web/src/{ => @types}/i18next.d.ts | 5 +- .../AuthenticatedLayout.tsx | 46 +- .../authenticated-layout/SidebarGroupList.tsx | 2 - .../UnauthenticatedLayout.tsx | 8 +- .../apps/web/src/components/AccountSelect.tsx | 2 - .../web/src/components/AddNewTagDialog.tsx | 10 +- .../apps/web/src/components/DateInput.tsx | 6 +- .../apps/web/src/components/RequireAuth.tsx | 2 - .../apps/web/src/components/ShareSelect.tsx | 28 +- .../apps/web/src/components/TagSelector.tsx | 5 +- .../accounts/AccountClearingListEntry.tsx | 15 +- .../accounts/AccountTransactionList.tsx | 6 +- .../accounts/AccountTransactionListEntry.tsx | 13 +- .../accounts/BalanceHistoryGraph.tsx | 2 - .../src/components/accounts/BalanceTable.tsx | 10 +- .../accounts/ClearingAccountDetail.tsx | 14 +- .../accounts/DeleteAccountModal.tsx | 14 +- frontend/apps/web/src/core/index.ts | 0 frontend/apps/web/src/hooks/index.ts | 1 + .../apps/web/src/hooks/useFormatCurrency.ts | 7 + .../accounts/AccountDetail/AccountDetail.tsx | 23 +- .../accounts/AccountDetail/AccountInfo.tsx | 28 +- .../apps/web/src/pages/accounts/Balances.tsx | 28 +- .../ClearingAccountList.tsx | 24 +- .../ClearingAccountListItem.tsx | 2 - .../PersonalAccountList.tsx | 14 +- .../PersonalAccountListItem.tsx | 2 - .../pages/accounts/SettlementPlanDisplay.tsx | 20 +- .../web/src/pages/auth/ConfirmEmailChange.tsx | 22 +- .../pages/auth/ConfirmPasswordRecovery.tsx | 30 +- .../src/pages/auth/ConfirmRegistration.tsx | 36 +- frontend/apps/web/src/pages/auth/Login.tsx | 20 +- frontend/apps/web/src/pages/auth/Logout.tsx | 2 - frontend/apps/web/src/pages/auth/Register.tsx | 28 +- .../pages/auth/RequestPasswordRecovery.tsx | 17 +- frontend/apps/web/src/pages/groups/Group.tsx | 12 +- .../apps/web/src/pages/groups/GroupInvite.tsx | 30 +- .../web/src/pages/groups/GroupInvites.tsx | 28 +- .../apps/web/src/pages/groups/GroupList.tsx | 28 +- .../apps/web/src/pages/groups/GroupLog.tsx | 32 +- .../web/src/pages/groups/GroupMemberList.tsx | 52 +- .../web/src/pages/groups/GroupSettings.tsx | 39 +- .../web/src/pages/profile/ChangeEmail.tsx | 8 +- .../web/src/pages/profile/ChangePassword.tsx | 11 +- .../web/src/pages/profile/SessionList.tsx | 32 +- .../apps/web/src/pages/profile/Settings.tsx | 2 - .../TransactionDetail/FileGallery.tsx | 6 +- .../TransactionDetail/ImageUploadDialog.tsx | 14 +- .../TransactionDetail/TransactionActions.tsx | 16 +- .../TransactionDetail/TransactionDetail.tsx | 4 +- .../TransactionDetail/TransactionMetadata.tsx | 50 +- .../purchase/TransactionPositions.tsx | 46 +- .../TransactionList/TransactionList.tsx | 34 +- .../TransactionList/TransactionListItem.tsx | 19 +- frontend/libs/translations/src/lib/de.ts | 118 +- frontend/libs/translations/src/lib/en.ts | 222 +++- frontend/libs/translations/src/lib/util.ts | 7 + frontend/package-lock.json | 1037 +---------------- frontend/package.json | 3 +- 60 files changed, 872 insertions(+), 1470 deletions(-) rename frontend/apps/mobile/src/{ => @types}/i18next.d.ts (100%) rename frontend/apps/web/src/{ => @types}/i18next.d.ts (57%) create mode 100644 frontend/apps/web/src/core/index.ts create mode 100644 frontend/apps/web/src/hooks/index.ts create mode 100644 frontend/apps/web/src/hooks/useFormatCurrency.ts create mode 100644 frontend/libs/translations/src/lib/util.ts diff --git a/frontend/apps/mobile/src/i18next.d.ts b/frontend/apps/mobile/src/@types/i18next.d.ts similarity index 100% rename from frontend/apps/mobile/src/i18next.d.ts rename to frontend/apps/mobile/src/@types/i18next.d.ts diff --git a/frontend/apps/web/src/i18next.d.ts b/frontend/apps/web/src/@types/i18next.d.ts similarity index 57% rename from frontend/apps/web/src/i18next.d.ts rename to frontend/apps/web/src/@types/i18next.d.ts index 0d806f61..631d0de5 100644 --- a/frontend/apps/web/src/i18next.d.ts +++ b/frontend/apps/web/src/@types/i18next.d.ts @@ -4,7 +4,8 @@ import type { resources, defaultNS } from "@abrechnung/translations"; declare module "i18next" { interface CustomTypeOptions { defaultNS: typeof defaultNS; - nsSeparator: ""; - resources: { "": (typeof resources)["en"]["translations"] }; + // nsSeparator: ""; + // resources: { "": (typeof resources)["en"]["translations"] }; + resources: (typeof resources)["en"]; } } diff --git a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx index 2900f28b..56ee4b73 100644 --- a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx @@ -1,10 +1,10 @@ import * as React from "react"; import { selectIsAuthenticated } from "@abrechnung/redux"; import { Link as RouterLink, Navigate, Outlet, useLocation, useParams } from "react-router-dom"; -import { selectAuthSlice, useAppSelector } from "../../store"; +import { selectAuthSlice, useAppSelector } from "@/store"; import { useRecoilValue } from "recoil"; -import ListItemLink from "../../components/style/ListItemLink"; -import SidebarGroupList from "../../app/authenticated-layout/SidebarGroupList"; +import { ListItemLink } from "@/components/style/ListItemLink"; +import { SidebarGroupList } from "@/app/authenticated-layout/SidebarGroupList"; import { AppBar, Box, @@ -40,17 +40,19 @@ import { Paid, People, } from "@mui/icons-material"; -import { config } from "../../state/config"; +import { config } from "@/state/config"; import { useTheme } from "@mui/material/styles"; -import { Banner } from "../../components/style/Banner"; -import Loading from "../../components/style/Loading"; +import { Banner } from "@/components/style/Banner"; +import { Loading } from "@/components/style/Loading"; import styles from "./AuthenticatedLayout.module.css"; import { LanguageSelect } from "@/components/LanguageSelect"; +import { useTranslation } from "react-i18next"; const drawerWidth = 240; const AUTH_FALLBACK = "/login"; export const AuthenticatedLayout: React.FC = () => { + const { t } = useTranslation(); const authenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); const location = useLocation(); const params = useParams(); @@ -94,7 +96,7 @@ export const AuthenticatedLayout: React.FC = () => { - + { - + { - + { - + { - + { - + { - + { - + @@ -173,7 +175,7 @@ export const AuthenticatedLayout: React.FC = () => { > {cfg.imprintURL && ( - imprint + {t("navbar.imprint")} )} @@ -213,7 +215,7 @@ export const AuthenticatedLayout: React.FC = () => { - Abrechnung + {t("app.name")}
@@ -243,26 +245,26 @@ export const AuthenticatedLayout: React.FC = () => { onClose={handleDotsMenuClose} > - Profile + {t("navbar.profile")} - Settings + {t("navbar.settings")} - Sessions + {t("navbar.sessions")} - Change E-Mail + {t("navbar.changeEmail")} - Change Password + {t("navbar.changePassword")} - Sign out + {t("navbar.signOut")}
diff --git a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx index 06300612..ad92d0ab 100644 --- a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx @@ -60,5 +60,3 @@ export const SidebarGroupList: React.FC = ({ activeGroupId }) => { ); }; - -export default SidebarGroupList; diff --git a/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx b/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx index 41d829ad..42dc621d 100644 --- a/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/unauthenticated-layout/UnauthenticatedLayout.tsx @@ -1,12 +1,14 @@ import * as React from "react"; import { Link as RouterLink, Outlet, Navigate } from "react-router-dom"; import { AppBar, Box, Button, Container, CssBaseline, Toolbar, Typography } from "@mui/material"; -import { Banner } from "../../components/style/Banner"; +import { Banner } from "@/components/style/Banner"; import { selectIsAuthenticated } from "@abrechnung/redux"; import { useAppSelector, selectAuthSlice } from "../../store"; import { LanguageSelect } from "@/components/LanguageSelect"; +import { useTranslation } from "react-i18next"; export const UnauthenticatedLayout: React.FC = () => { + const { t } = useTranslation(); const authenticated = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); if (authenticated) { @@ -25,12 +27,12 @@ export const UnauthenticatedLayout: React.FC = () => { - Abrechnung + {t("app.name")} diff --git a/frontend/apps/web/src/components/AccountSelect.tsx b/frontend/apps/web/src/components/AccountSelect.tsx index 6731430c..6e69a470 100644 --- a/frontend/apps/web/src/components/AccountSelect.tsx +++ b/frontend/apps/web/src/components/AccountSelect.tsx @@ -77,5 +77,3 @@ export const AccountSelect: React.FC = ({ /> ); }; - -export default AccountSelect; diff --git a/frontend/apps/web/src/components/AddNewTagDialog.tsx b/frontend/apps/web/src/components/AddNewTagDialog.tsx index 2e659fe7..10cc1f0b 100644 --- a/frontend/apps/web/src/components/AddNewTagDialog.tsx +++ b/frontend/apps/web/src/components/AddNewTagDialog.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button } from "@mui/material"; +import { useTranslation } from "react-i18next"; interface Props { open: boolean; @@ -8,6 +9,7 @@ interface Props { } export const AddNewTagDialog: React.FC = ({ open, onCreate, onClose }) => { + const { t } = useTranslation(); const [tag, setTag] = React.useState(""); const [error, setError] = React.useState(false); @@ -43,12 +45,12 @@ export const AddNewTagDialog: React.FC = ({ open, onCreate, onClose }) => return ( - Add new tag + {t("common.addNewTag")} = ({ open, onCreate, onClose }) => diff --git a/frontend/apps/web/src/components/DateInput.tsx b/frontend/apps/web/src/components/DateInput.tsx index 9c2d458f..a1571395 100644 --- a/frontend/apps/web/src/components/DateInput.tsx +++ b/frontend/apps/web/src/components/DateInput.tsx @@ -2,6 +2,7 @@ import { DatePicker } from "@mui/x-date-pickers"; import { DateTime } from "luxon"; import * as React from "react"; import { DisabledTextField } from "./style/DisabledTextField"; +import { useTranslation } from "react-i18next"; interface Props { value: string; @@ -12,6 +13,7 @@ interface Props { } export const DateInput: React.FC = ({ value, onChange, helperText, error, disabled = false }) => { + const { t } = useTranslation(); const handleChange = (value: DateTime) => { if (value.toISODate()) { onChange(value.toISODate()); @@ -21,7 +23,7 @@ export const DateInput: React.FC = ({ value, onChange, helperText, error, if (disabled) { return ( = ({ value, onChange, helperText, error, return ( = ({ authFallback = "/login", children return children; }; - -export default RequireAuth; diff --git a/frontend/apps/web/src/components/ShareSelect.tsx b/frontend/apps/web/src/components/ShareSelect.tsx index b6f02855..7b0503e4 100644 --- a/frontend/apps/web/src/components/ShareSelect.tsx +++ b/frontend/apps/web/src/components/ShareSelect.tsx @@ -29,6 +29,7 @@ import { getAccountLink } from "../utils"; import { NumericInput } from "./NumericInput"; import { getAccountIcon } from "./style/AbrechnungIcons"; import { getAccountSortFunc } from "@abrechnung/core"; +import { useTranslation } from "react-i18next"; interface RowProps { account: Account; @@ -127,6 +128,7 @@ export const ShareSelect: React.FC = ({ helperText, editable = false, }) => { + const { t } = useTranslation(); const theme = useTheme(); const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down("sm")); @@ -214,8 +216,20 @@ export const ShareSelect: React.FC = ({ {label} - {nSelectedPeople > 0 && } - {nSelectedEvents > 0 && } + {nSelectedPeople > 0 && ( + + )} + {nSelectedEvents > 0 && ( + + )} {editable && ( @@ -225,7 +239,7 @@ export const ShareSelect: React.FC = ({ onChange={(event: React.ChangeEvent) => setShowEvents(event.target.checked) } - label="Show Events" + label={t("shareSelect.showEvents")} /> } @@ -233,7 +247,7 @@ export const ShareSelect: React.FC = ({ onChange={(event: React.ChangeEvent) => setShowAdvanced(event.target.checked) } - label="Advanced" + label={t("common.advanced")} /> )} @@ -257,10 +271,10 @@ export const ShareSelect: React.FC = ({ {!showSearch ? ( - "Account / Event" + t("shareSelect.accountSlashEvent") ) : ( = ({ /> )} - Shares + {t("common.shares")} {additionalShareInfoHeader ?? null} diff --git a/frontend/apps/web/src/components/TagSelector.tsx b/frontend/apps/web/src/components/TagSelector.tsx index e9f30be8..ebe2b5a5 100644 --- a/frontend/apps/web/src/components/TagSelector.tsx +++ b/frontend/apps/web/src/components/TagSelector.tsx @@ -5,6 +5,7 @@ import * as React from "react"; import { useAppSelector } from "@/store"; import { AddNewTagDialog } from "./AddNewTagDialog"; import { DisabledTextField } from "./style/DisabledTextField"; +import { useTranslation } from "react-i18next"; interface Props extends Omit { groupId: number; @@ -26,6 +27,7 @@ export const TagSelector: React.FC = ({ addCreateNewOption = true, ...props }) => { + const { t } = useTranslation(); const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false); const possibleTags = useAppSelector((state) => selectTagsInGroup({ state, groupId })); @@ -36,7 +38,6 @@ export const TagSelector: React.FC = ({ } const newTags = event.target.value; if (newTags.indexOf(CREATE_TAG) > -1) { - console.log("add new tag"); openAddTagDialog(); return; } @@ -86,7 +87,7 @@ export const TagSelector: React.FC = ({ - Add new tag + {t("common.addNewTag")} )} diff --git a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx index 29074856..1955eeda 100644 --- a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx +++ b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx @@ -7,6 +7,8 @@ import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store"; import { getAccountLink } from "@/utils"; import { ClearingAccountIcon } from "../style/AbrechnungIcons"; import ListItemLink from "../style/ListItemLink"; +import { useTranslation } from "react-i18next"; +import { useFormatCurrency } from "@/hooks"; interface Props { groupId: number; @@ -15,6 +17,8 @@ interface Props { } export const AccountClearingListEntry: React.FC = ({ groupId, accountId, clearingAccountId }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const balances = useAppSelector((state) => selectAccountBalances({ state, groupId })); const currency_symbol = useAppSelector((state) => selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId }) @@ -56,17 +60,18 @@ export const AccountClearingListEntry: React.FC = ({ groupId, accountId, balanceColor(balances[clearingAccount.id]?.clearingResolution[accountId], theme), }} > - {balances[clearingAccount.id]?.clearingResolution[accountId]?.toFixed(2)} {currency_symbol} + {formatCurrency(balances[clearingAccount.id]?.clearingResolution[accountId], currency_symbol)}
- last changed:{" "} - {DateTime.fromISO(clearingAccount.last_changed).toLocaleString(DateTime.DATETIME_FULL)} + {t("common.lastChangedWithTime", { + datetime: DateTime.fromISO(clearingAccount.last_changed).toLocaleString( + DateTime.DATETIME_FULL + ), + })}
); }; - -export default AccountClearingListEntry; diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx index da910d78..72d5c86b 100644 --- a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx +++ b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx @@ -2,10 +2,10 @@ import { selectClearingAccountsInvolvingAccounts, selectTransactionsInvolvingAcc import { Account, Transaction } from "@abrechnung/types"; import { Alert, List } from "@mui/material"; import { DateTime } from "luxon"; -import React from "react"; +import * as React from "react"; import { selectAccountSlice, selectTransactionSlice, useAppSelector } from "@/store"; -import AccountClearingListEntry from "./AccountClearingListEntry"; -import AccountTransactionListEntry from "./AccountTransactionListEntry"; +import { AccountClearingListEntry } from "./AccountClearingListEntry"; +import { AccountTransactionListEntry } from "./AccountTransactionListEntry"; type ArrayAccountsAndTransactions = Array; diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx index 66b43507..2112eadc 100644 --- a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx +++ b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx @@ -7,6 +7,7 @@ import { balanceColor } from "@/core/utils"; import { selectGroupSlice, selectTransactionSlice, useAppSelector } from "@/store"; import { PurchaseIcon, TransferIcon } from "../style/AbrechnungIcons"; import ListItemLink from "../style/ListItemLink"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -15,6 +16,7 @@ interface Props { } export const AccountTransactionListEntry: React.FC = ({ groupId, transactionId, accountId }) => { + const { t } = useTranslation(); const balanceEffect = useAppSelector((state) => selectTransactionBalanceEffect({ state: selectTransactionSlice(state), groupId, transactionId }) ); @@ -29,11 +31,11 @@ export const AccountTransactionListEntry: React.FC = ({ groupId, transact {transaction.type === "purchase" ? ( - + ) : transaction.type === "transfer" ? ( - + ) : ( @@ -67,13 +69,12 @@ export const AccountTransactionListEntry: React.FC = ({ groupId, transact
- last changed:{" "} - {DateTime.fromISO(transaction.last_changed).toLocaleString(DateTime.DATETIME_FULL)} + {t("common.lastChangedWithTime", { + datetime: DateTime.fromISO(transaction.last_changed).toLocaleString(DateTime.DATETIME_FULL), + })}
); }; - -export default AccountTransactionListEntry; diff --git a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx index 5729d355..ca7e2767 100644 --- a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx +++ b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx @@ -182,5 +182,3 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) => ); }; - -export default BalanceHistoryGraph; diff --git a/frontend/apps/web/src/components/accounts/BalanceTable.tsx b/frontend/apps/web/src/components/accounts/BalanceTable.tsx index 5085a68f..3ccfadbf 100644 --- a/frontend/apps/web/src/components/accounts/BalanceTable.tsx +++ b/frontend/apps/web/src/components/accounts/BalanceTable.tsx @@ -3,12 +3,14 @@ import { selectAccountBalances, selectGroupById, selectSortedAccounts } from "@a import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"; import React from "react"; import { renderCurrency } from "../style/datagrid/renderCurrency"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; } export const BalanceTable: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const personalAccounts = useAppSelector((state) => selectSortedAccounts({ state: selectAccountSlice(state), groupId, type: "personal", sortMode: "name" }) ); @@ -32,17 +34,17 @@ export const BalanceTable: React.FC = ({ groupId }) => { { field: "description", headerName: "Description", width: 200 }, { field: "totalConsumed", - headerName: "Received / Consumed", + headerName: t("balanceTable.totalConsumed"), renderCell: renderCurrency(group.currency_symbol, "red"), }, { field: "totalPaid", - headerName: "Paid", + headerName: t("balanceTable.totalPaid"), renderCell: renderCurrency(group.currency_symbol, "green"), }, { field: "balance", - headerName: "Balance", + headerName: t("balanceTable.balance"), renderCell: renderCurrency(group.currency_symbol), }, ]; @@ -69,5 +71,3 @@ export const BalanceTable: React.FC = ({ groupId }) => { ); }; - -export default BalanceTable; diff --git a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx index fea86fbc..8c478497 100644 --- a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx +++ b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx @@ -3,6 +3,8 @@ import { TableCell } from "@mui/material"; import React from "react"; import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store"; import { ShareSelect } from "../ShareSelect"; +import { useTranslation } from "react-i18next"; +import { useFormatCurrency } from "@/hooks"; interface Props { groupId: number; @@ -10,6 +12,8 @@ interface Props { } export const ClearingAccountDetail: React.FC = ({ groupId, accountId }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const account = useAppSelector((state) => selectAccountById({ state: selectAccountSlice(state), groupId, accountId }) ); @@ -28,14 +32,16 @@ export const ClearingAccountDetail: React.FC = ({ groupId, accountId }) = value={account.clearing_shares} additionalShareInfoHeader={ - Shared + {t("common.shared")} } excludeAccounts={[account.id]} renderAdditionalShareInfo={({ account: participatingAccount }) => ( - {(balances[account.id]?.clearingResolution[participatingAccount.id] ?? 0).toFixed(2)}{" "} - {currency_symbol} + {formatCurrency( + balances[account.id]?.clearingResolution[participatingAccount.id] ?? 0, + currency_symbol + )} )} onChange={(value) => undefined} @@ -43,5 +49,3 @@ export const ClearingAccountDetail: React.FC = ({ groupId, accountId }) = /> ); }; - -export default ClearingAccountDetail; diff --git a/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx b/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx index 3fc24fc5..1312dc69 100644 --- a/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx +++ b/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx @@ -4,6 +4,7 @@ import { Dialog, DialogTitle, DialogActions, DialogContent, Button, LinearProgre import { selectAccountSlice, useAppDispatch, useAppSelector } from "@/store"; import { deleteAccount, selectAccountById } from "@abrechnung/redux"; import { toast } from "react-toastify"; +import { useTranslation } from "react-i18next"; interface Props { show: boolean; @@ -14,13 +15,14 @@ interface Props { } export const DeleteAccountModal: React.FC = ({ show, onClose, groupId, accountId, onAccountDeleted }) => { + const { t } = useTranslation(); const account = useAppSelector((state) => selectAccountById({ state: selectAccountSlice(state), groupId, accountId }) ); const dispatch = useAppDispatch(); const [showProgress, setShowProgress] = React.useState(false); - const accountTypeLabel = account?.type === "clearing" ? "event" : "account"; + const accountTypeLabel = account?.type === "clearing" ? t("accounts.event") : t("accounts.account"); const confirmDeleteAccount = () => { if (!account) { @@ -50,16 +52,18 @@ export const DeleteAccountModal: React.FC = ({ show, onClose, groupId, ac return ( {showProgress && } - Confirm delete {accountTypeLabel} + + {t("accounts.deleteConfirm", { accountType: accountTypeLabel })} + - Are you sure you want to delete the {accountTypeLabel} "{account?.name}" + {t("accounts.deleteConfirmBody", { accountType: accountTypeLabel, accountName: account?.name })} diff --git a/frontend/apps/web/src/core/index.ts b/frontend/apps/web/src/core/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/apps/web/src/hooks/index.ts b/frontend/apps/web/src/hooks/index.ts new file mode 100644 index 00000000..28a476e8 --- /dev/null +++ b/frontend/apps/web/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useFormatCurrency"; diff --git a/frontend/apps/web/src/hooks/useFormatCurrency.ts b/frontend/apps/web/src/hooks/useFormatCurrency.ts new file mode 100644 index 00000000..c06b97ca --- /dev/null +++ b/frontend/apps/web/src/hooks/useFormatCurrency.ts @@ -0,0 +1,7 @@ +import * as React from "react"; + +export const useFormatCurrency = () => { + return React.useCallback((value: number, currencySymbol: string) => { + return `${value.toFixed(2)} ${currencySymbol}`; + }, []); +}; diff --git a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx index e9c1a0a5..db5f710d 100644 --- a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx +++ b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountDetail.tsx @@ -1,15 +1,16 @@ -import AccountTransactionList from "@/components/accounts/AccountTransactionList"; -import BalanceHistoryGraph from "@/components/accounts/BalanceHistoryGraph"; -import ClearingAccountDetail from "@/components/accounts/ClearingAccountDetail"; +import { AccountTransactionList } from "@/components/accounts/AccountTransactionList"; +import { BalanceHistoryGraph } from "@/components/accounts/BalanceHistoryGraph"; +import { ClearingAccountDetail } from "@/components/accounts/ClearingAccountDetail"; import { Loading } from "@/components/style/Loading"; import { MobilePaper } from "@/components/style/mobile"; import { useQuery, useTitle } from "@/core/utils"; import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store"; import { selectAccountById, selectGroupById } from "@abrechnung/redux"; import { Grid, Typography } from "@mui/material"; -import React from "react"; +import * as React from "react"; import { Navigate, useParams } from "react-router-dom"; import { AccountInfo } from "./AccountInfo"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -28,6 +29,7 @@ const AccountEdit: React.FC<{ groupId: number; accountId: number }> = ({ groupId }; export const AccountDetail: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const params = useParams(); const accountId = Number(params["id"]); @@ -37,7 +39,12 @@ export const AccountDetail: React.FC = ({ groupId }) => { ); const query = useQuery(); - useTitle(`${group.name} - ${account?.type === "clearing" ? "Event" : "Account"} ${account?.name}`); + useTitle( + t(account?.type === "clearing" ? "accounts.detail.tabTitleEvent" : "accounts.detail.tabTitleAccount", { + group, + account, + }) + ); if (account === undefined) { if (query.get("no-redirect") === "true") { @@ -61,7 +68,7 @@ export const AccountDetail: React.FC = ({ groupId }) => { {account.type === "personal" && ( - Balance of {account.name} + {t("accounts.balanceOf", { account })} @@ -69,14 +76,14 @@ export const AccountDetail: React.FC = ({ groupId }) => { {account.type === "clearing" && ( - Clearing distribution of {account.name} + {t("accounts.clearingDistributionOf", { account })} )} - Transactions involving {account.name} + {t("accounts.transactionsInvolving", { account })} diff --git a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx index 203a2d9a..3cfa407f 100644 --- a/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx +++ b/frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx @@ -4,6 +4,7 @@ import { TagSelector } from "@/components/TagSelector"; import { TextInput } from "@/components/TextInput"; import { DeleteAccountModal } from "@/components/accounts/DeleteAccountModal"; import { api } from "@/core/api"; +import { useFormatCurrency } from "@/hooks"; import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; import { getAccountLink, getAccountListLink } from "@/utils"; import { @@ -19,7 +20,8 @@ import { import { Account, AccountValidator } from "@abrechnung/types"; import { ChevronLeft, Delete, Edit } from "@mui/icons-material"; import { Button, Chip, Divider, Grid, IconButton, LinearProgress, TableCell } from "@mui/material"; -import React from "react"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { typeToFlattenedError, z } from "zod"; @@ -32,6 +34,8 @@ interface Props { const emptyErrors = { fieldErrors: {}, formErrors: [] }; export const AccountInfo: React.FC = ({ groupId, accountId }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -128,10 +132,10 @@ export const AccountInfo: React.FC = ({ groupId, accountId }) => { {account.is_wip ? ( <> ) : ( @@ -151,7 +155,7 @@ export const AccountInfo: React.FC = ({ groupId, accountId }) => { = ({ groupId, accountId }) => { /> {!account.is_wip && account.description === "" ? null : ( = ({ groupId, accountId }) => { = ({ groupId, accountId }) => { - Shared + {t("common.shared")} } error={!!validationErrors.fieldErrors.clearing_shares} @@ -217,10 +221,10 @@ export const AccountInfo: React.FC = ({ groupId, accountId }) => { excludeAccounts={[account.id]} renderAdditionalShareInfo={({ account: participatingAccount }) => ( - {(balances[account.id]?.clearingResolution[participatingAccount.id] ?? 0).toFixed( - 2 - )}{" "} - {currencySymbol} + {formatCurrency( + balances[account.id]?.clearingResolution[participatingAccount.id] ?? 0, + currencySymbol + )} )} onChange={(value) => pushChanges({ clearing_shares: value })} diff --git a/frontend/apps/web/src/pages/accounts/Balances.tsx b/frontend/apps/web/src/pages/accounts/Balances.tsx index 8d759e9a..fd8076ee 100644 --- a/frontend/apps/web/src/pages/accounts/Balances.tsx +++ b/frontend/apps/web/src/pages/accounts/Balances.tsx @@ -1,7 +1,8 @@ -import BalanceTable from "@/components/accounts/BalanceTable"; -import ListItemLink from "@/components/style/ListItemLink"; +import { BalanceTable } from "@/components/accounts/BalanceTable"; +import { ListItemLink } from "@/components/style/ListItemLink"; import { MobilePaper } from "@/components/style/mobile"; import { useTitle } from "@/core/utils"; +import { useFormatCurrency } from "@/hooks"; import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store"; import { selectAccountBalances, @@ -25,6 +26,7 @@ import { } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { Bar, BarChart, Cell, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; @@ -33,6 +35,8 @@ interface Props { } export const Balances: React.FC = ({ groupId }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const theme: Theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); const navigate = useNavigate(); @@ -53,7 +57,7 @@ export const Balances: React.FC = ({ groupId }) => { const colorGreenInverted = theme.palette.mode === "dark" ? theme.palette.success.light : theme.palette.success.dark; const colorRedInverted = theme.palette.mode === "dark" ? theme.palette.error.light : theme.palette.error.dark; - useTitle(`${group.name} - Balances`); + useTitle(t("accounts.balances.tabTitle", { groupName: group.name })); const roundTwoDecimals = (val: number) => +val.toFixed(2); @@ -100,10 +104,10 @@ export const Balances: React.FC = ({ groupId }) => { - {personalAccounts.length === 0 && No Accounts} + {personalAccounts.length === 0 && {t("accounts.noAccounts")}} {unbalancedClearingAccounts.length !== 0 && ( - Some Clearing Accounts have remaining balances. + {t("accounts.balances.clearingAccountsRemainingBalances")} {unbalancedClearingAccounts.map((account) => ( <>{account.name}: @@ -114,7 +118,7 @@ export const Balances: React.FC = ({ groupId }) => { color: account.balance < 0 ? colorRedInverted : colorGreenInverted, }} > - {account.balance.toFixed(2)} {group.currency_symbol} + {formatCurrency(account.balance, group.currency_symbol)} ))} @@ -136,7 +140,7 @@ export const Balances: React.FC = ({ groupId }) => { : colorGreenInverted, }} > - {balances[account.id]?.balance.toFixed(2)} {group.currency_symbol} + {formatCurrency(balances[account.id]?.balance, group.currency_symbol)} @@ -170,7 +174,7 @@ export const Balances: React.FC = ({ groupId }) => { /> - parseFloat(String(label)).toFixed(2) + ` ${group.currency_symbol}` + formatCurrency(parseFloat(String(label)), group.currency_symbol) } labelStyle={{ color: theme.palette.text.primary, @@ -194,9 +198,7 @@ export const Balances: React.FC = ({ groupId }) => { ); })} - `${entry["balance"].toFixed(2)}${group.currency_symbol}` - } + dataKey={(entry) => formatCurrency(entry["balance"], group.currency_symbol)} position="insideLeft" fill={theme.palette.text.primary} /> @@ -213,11 +215,9 @@ export const Balances: React.FC = ({ groupId }) => { ); }; - -export default Balances; diff --git a/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx b/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx index 340a3e82..e53c6d78 100644 --- a/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx +++ b/frontend/apps/web/src/pages/accounts/ClearingAccountList/ClearingAccountList.tsx @@ -30,6 +30,7 @@ import { useTitle } from "@/core/utils"; import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; import { getAccountLink } from "@/utils"; import { ClearingAccountListItem } from "./ClearingAccountListItem"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -39,6 +40,7 @@ const emptyList = []; const MAX_ITEMS_PER_PAGE = 40; export const ClearingAccountList: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); @@ -70,7 +72,7 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { (currentPage + 1) * MAX_ITEMS_PER_PAGE ); - useTitle(`${group.name} - Events`); + useTitle(t("events.list.tabTitle", { groupName: group.name })); const [accountDeleteId, setAccountDeleteId] = useState(null); const showDeleteModal = accountDeleteId !== null; @@ -112,7 +114,7 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { setSearchValue(e.target.value)} - placeholder="Search…" + placeholder={t("common.search")} inputProps={{ "aria-label": "search", }} @@ -130,23 +132,23 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { } /> - Sort by + {t("common.sortBy")} = ({ groupId }) => { {!isSmallScreen && ( - + @@ -169,7 +171,7 @@ export const ClearingAccountList: React.FC = ({ groupId }) => { {paginatedAccounts.length === 0 ? ( - No Events + {t("events.noEvents")} ) : ( paginatedAccounts.map((account) => ( = ({ groupId, accountId, s ); }; - -export default ClearingAccountListItem; diff --git a/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx b/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx index ce6509f0..293823d9 100644 --- a/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx +++ b/frontend/apps/web/src/pages/accounts/PersonalAccountList/PersonalAccountList.tsx @@ -35,6 +35,7 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { PersonalAccountListItem } from "./PersonalAccountListItem"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -43,6 +44,7 @@ interface Props { const MAX_ITEMS_PER_PAGE = 40; export const PersonalAccountList: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); @@ -74,7 +76,7 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { (currentPage + 1) * MAX_ITEMS_PER_PAGE ); - useTitle(`${group.name} - Accounts`); + useTitle(t("accounts.list.tabTitle", { groupName: group.name })); const [accountDeleteId, setAccountDeleteId] = useState(null); const showDeleteModal = accountDeleteId !== null; @@ -117,7 +119,7 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { setSearchValue(e.target.value)} - placeholder="Search…" + placeholder={t("common.search")} inputProps={{ "aria-label": "search", }} @@ -143,9 +145,9 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { onChange={(evt) => setSortMode(evt.target.value as AccountSortMode)} value={sortMode} > - Name - Description - Last changed + {t("common.name")} + {t("common.description")} + {t("common.lastChanged")} @@ -162,7 +164,7 @@ export const PersonalAccountList: React.FC = ({ groupId }) => { {paginatedAccounts.length === 0 ? ( - No Accounts + {t("accounts.noAccounts")} ) : ( paginatedAccounts.map((account) => ( = ({ groupId, currentUserI ); }; - -export default PersonalAccountListItem; diff --git a/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx b/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx index bebf93d7..90da33e3 100644 --- a/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx +++ b/frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx @@ -10,12 +10,16 @@ import { import { Button, List, ListItem, ListItemSecondaryAction, ListItemText, Typography } from "@mui/material"; import * as React from "react"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useFormatCurrency } from "@/hooks"; interface Props { groupId: number; } export const SettlementPlanDisplay: React.FC = ({ groupId }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const settlementPlan = useAppSelector((state) => selectSettlementPlan({ state, groupId })); @@ -32,7 +36,7 @@ export const SettlementPlanDisplay: React.FC = ({ groupId }) => { type: "transfer", groupId, data: { - name: "Settlement", + name: t("accounts.settlement.transactionName"), value: planItem.paymentAmount, creditor_shares: { [planItem.creditorId]: 1 }, debitor_shares: { [planItem.debitorId]: 1 }, @@ -47,21 +51,25 @@ export const SettlementPlanDisplay: React.FC = ({ groupId }) => { return ( - Settle this groups balances + {t("accounts.settlement.title")} {settlementPlan.map((planItem) => ( - {accountMap[planItem.creditorId].name} pays {accountMap[planItem.debitorId].name}{" "} - {planItem.paymentAmount.toFixed(2)} - {currency_symbol} + {t("accounts.settlement.whoPaysWhom", { + from: accountMap[planItem.creditorId].name, + to: accountMap[planItem.debitorId].name, + money: formatCurrency(planItem.paymentAmount, currency_symbol), + })} } /> - + ))} diff --git a/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx b/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx index 3aa4d977..1b7d5cfa 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmEmailChange.tsx @@ -5,12 +5,14 @@ import { toast } from "react-toastify"; import { Loading } from "@/components/style/Loading"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; +import { Trans, useTranslation } from "react-i18next"; export const ConfirmEmailChange: React.FC = () => { + const { t } = useTranslation(); const [status, setStatus] = useState("idle"); const { token } = useParams(); - useTitle("Abrechnung - Confirm E-Mail Change"); + useTitle(t("auth.confirmEmailChange.tabTitle")); const confirmEmail = (e) => { e.preventDefault(); @@ -28,7 +30,7 @@ export const ConfirmEmailChange: React.FC = () => { if (status === "success") { return ( - Confirmation successful + {t("auth.confirmEmailChange.confirmSuccessful")} ); } @@ -36,21 +38,21 @@ export const ConfirmEmailChange: React.FC = () => { return (
- Confirm your new E-Mail + {t("auth.confirmEmailChange.header")} {status === "loading" ? ( ) : (

- Click{" "} - {" "} - to confirm your new email. + + Click + + to confirm your new email. +

)}
); }; - -export default ConfirmEmailChange; diff --git a/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx b/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx index fd3ef90d..59f4de8b 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmPasswordRecovery.tsx @@ -5,6 +5,9 @@ import React, { useState } from "react"; import { Link as RouterLink, useParams } from "react-router-dom"; import { z } from "zod"; import { api } from "@/core/api"; +import i18n from "@/i18n"; +import { Trans, useTranslation } from "react-i18next"; +import { useTitle } from "@/core/utils"; const validationSchema = z .object({ @@ -12,16 +15,19 @@ const validationSchema = z password2: z.string({ required_error: "please repeat your desired password" }), }) .refine((data) => data.password === data.password2, { - message: "passwords don't match", + message: i18n.t("common.passwordsDoNotMatch"), path: ["password2"], }); type FormSchema = z.infer; export const ConfirmPasswordRecovery: React.FC = () => { + const { t } = useTranslation(); const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); const { token } = useParams(); + useTitle(t("auth.confirmPasswordRecovery.tabTitle")); + const handleSubmit = (values: FormSchema, { setSubmitting, resetForm }: FormikHelpers) => { api.client.auth .confirmPasswordRecovery({ requestBody: { new_password: values.password, token } }) @@ -50,7 +56,7 @@ export const ConfirmPasswordRecovery: React.FC = () => { }} > - Confirm Password Recovery + {t("auth.confirmPasswordRecovery.header")} {error && ( @@ -59,11 +65,13 @@ export const ConfirmPasswordRecovery: React.FC = () => { )} {status === "success" ? ( - Password recovery successful, please{" "} - - login - {" "} - using your new password. + + Password recovery successful, please + + login + + using your new password. + ) : ( { autoFocus type="password" name="password" - label="Password" + label={t("common.password")} onBlur={handleBlur} onChange={handleChange} value={values.password} @@ -104,7 +112,7 @@ export const ConfirmPasswordRecovery: React.FC = () => { fullWidth type="password" name="password2" - label="Repeat Password" + label={t("common.repeatPassword")} onBlur={handleBlur} onChange={handleChange} value={values.password2} @@ -121,7 +129,7 @@ export const ConfirmPasswordRecovery: React.FC = () => { disabled={isSubmitting} sx={{ margin: "3 0 2 0" }} > - Confirm + {t("common.confirm")} )} @@ -131,5 +139,3 @@ export const ConfirmPasswordRecovery: React.FC = () => { ); }; - -export default ConfirmPasswordRecovery; diff --git a/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx b/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx index f56b7203..19ba3301 100644 --- a/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx +++ b/frontend/apps/web/src/pages/auth/ConfirmRegistration.tsx @@ -5,20 +5,22 @@ import { Loading } from "@/components/style/Loading"; import { MobilePaper } from "@/components/style/mobile"; import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; +import { Trans, useTranslation } from "react-i18next"; export const ConfirmRegistration: React.FC = () => { + const { t } = useTranslation(); const [error, setError] = useState(null); const [status, setStatus] = useState("idle"); const { token } = useParams(); - useTitle("Abrechnung - Confirm Registration"); + useTitle(t("auth.confirmRegistration.tabTitle")); const confirmEmail = (e) => { e.preventDefault(); setStatus("loading"); api.client.auth .confirmRegistration({ requestBody: { token } }) - .then((value) => { + .then(() => { setError(null); setStatus("success"); }) @@ -32,34 +34,36 @@ export const ConfirmRegistration: React.FC = () => { - Confirm Registration + {t("auth.confirmRegistration.header")} {error && {error}} {status === "success" ? ( <> - Confirmation successful + {t("auth.confirmRegistration.confirmSuccessful")}

- Please{" "} - - login - {" "} - using your credentials. + + Please + + login + + using your credentials. +

) : status === "loading" ? ( ) : (

- Click{" "} - {" "} - to confirm your registration. + + Click + + to confirm your registration. +

)}
); }; - -export default ConfirmRegistration; diff --git a/frontend/apps/web/src/pages/auth/Login.tsx b/frontend/apps/web/src/pages/auth/Login.tsx index 6b7a8802..15ffc58f 100644 --- a/frontend/apps/web/src/pages/auth/Login.tsx +++ b/frontend/apps/web/src/pages/auth/Login.tsx @@ -21,6 +21,7 @@ import { z } from "zod"; import { useAppDispatch, useAppSelector, selectAuthSlice } from "@/store"; import { selectIsAuthenticated, login } from "@abrechnung/redux"; import { toFormikValidationSchema } from "@abrechnung/utils"; +import { useTranslation } from "react-i18next"; const validationSchema = z.object({ username: z.string({ required_error: "username is required" }), @@ -30,6 +31,7 @@ const validationSchema = z.object({ type FormValues = z.infer; export const Login: React.FC = () => { + const { t } = useTranslation(); const isLoggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); const dispatch = useAppDispatch(); const query = useQuery(); @@ -37,7 +39,7 @@ export const Login: React.FC = () => { const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : ""; - useTitle("Abrechnung - Login"); + useTitle(t("auth.login.tabTitle")); useEffect(() => { if (isLoggedIn) { @@ -54,7 +56,7 @@ export const Login: React.FC = () => { dispatch(login({ username: values.username, password: values.password, sessionName, api })) .unwrap() .then((res) => { - toast.success(`Logged in...`); + toast.success(t("auth.login.loginSuccess")); setSubmitting(false); }) .catch((err) => { @@ -71,7 +73,7 @@ export const Login: React.FC = () => { - Sign in + {t("auth.login.header")} { fullWidth autoFocus type="text" - label="Username" + label={t("common.username")} name="username" onBlur={handleBlur} onChange={handleChange} @@ -102,7 +104,7 @@ export const Login: React.FC = () => { fullWidth type="password" name="password" - label="Password" + label={t("common.password")} onBlur={handleBlur} onChange={handleChange} value={values.password} @@ -117,19 +119,19 @@ export const Login: React.FC = () => { disabled={isSubmitting} sx={{ mt: 1 }} > - Login + {t("auth.login.confirmButton")} - No account? register + {t("auth.login.noAccountRegister")} - Forgot your password? + {t("auth.login.forgotPassword")} @@ -140,5 +142,3 @@ export const Login: React.FC = () => { ); }; - -export default Login; diff --git a/frontend/apps/web/src/pages/auth/Logout.tsx b/frontend/apps/web/src/pages/auth/Logout.tsx index 31019a30..0d57992c 100644 --- a/frontend/apps/web/src/pages/auth/Logout.tsx +++ b/frontend/apps/web/src/pages/auth/Logout.tsx @@ -21,5 +21,3 @@ export const Logout: React.FC = () => { return ; }; - -export default Logout; diff --git a/frontend/apps/web/src/pages/auth/Register.tsx b/frontend/apps/web/src/pages/auth/Register.tsx index 25c28904..40c5b739 100644 --- a/frontend/apps/web/src/pages/auth/Register.tsx +++ b/frontend/apps/web/src/pages/auth/Register.tsx @@ -22,6 +22,8 @@ import { api } from "@/core/api"; import { useQuery, useTitle } from "@/core/utils"; import { selectAuthSlice, useAppSelector } from "@/store"; import { toFormikValidationSchema } from "@abrechnung/utils"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; const validationSchema = z .object({ @@ -31,13 +33,14 @@ const validationSchema = z password2: z.string(), }) .refine((data) => data.password === data.password2, { - message: "Passwords do not match", + message: i18n.t("common.passwordsDoNotMatch"), path: ["password2"], }); type FormValues = z.infer; export const Register: React.FC = () => { + const { t } = useTranslation(); const loggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); const [loading, setLoading] = useState(true); const query = useQuery(); @@ -45,7 +48,7 @@ export const Register: React.FC = () => { const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : ""; - useTitle("Abrechnung - Register"); + useTitle(t("auth.register.tabTitle")); useEffect(() => { if (loggedIn) { @@ -81,8 +84,8 @@ export const Register: React.FC = () => { invite_token: inviteToken, }, }) - .then((res) => { - toast.success(`Registered successfully, please confirm your email before logging in...`, { + .then(() => { + toast.success(t("auth.register.registrationSuccess"), { autoClose: 20000, }); setSubmitting(false); @@ -112,7 +115,7 @@ export const Register: React.FC = () => { - Register a new account + {t("auth.register.header")} { fullWidth autoFocus type="text" - label="Username" + label={t("common.username")} name="username" onBlur={handleBlur} onChange={handleChange} @@ -146,7 +149,7 @@ export const Register: React.FC = () => { fullWidth type="email" name="email" - label="E-Mail" + label={t("common.email")} onBlur={handleBlur} onChange={handleChange} value={values.email} @@ -158,8 +161,7 @@ export const Register: React.FC = () => { required fullWidth type="password" - name="password" - label="Password" + label={t("common.password")} onBlur={handleBlur} onChange={handleChange} value={values.password} @@ -172,7 +174,7 @@ export const Register: React.FC = () => { fullWidth type="password" name="password2" - label="Repeat Password" + label={t("common.repeatPassword")} onBlur={handleBlur} onChange={handleChange} value={values.password2} @@ -187,12 +189,12 @@ export const Register: React.FC = () => { disabled={isSubmitting} sx={{ mt: 1 }} > - Register + {t("auth.register.confirmButton")} - Already have an account? Sign in + {t("auth.register.alreadyHasAccount")} @@ -203,5 +205,3 @@ export const Register: React.FC = () => { ); }; - -export default Register; diff --git a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx index 56bdf89d..a8347041 100644 --- a/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx +++ b/frontend/apps/web/src/pages/auth/RequestPasswordRecovery.tsx @@ -7,6 +7,8 @@ import { useNavigate } from "react-router-dom"; import { z } from "zod"; import { api } from "@/core/api"; import { selectAuthSlice, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; +import { useTitle } from "@/core/utils"; const validationSchema = z.object({ email: z.string({ required_error: "email is required" }).email("please enter a valid email address"), @@ -14,11 +16,14 @@ const validationSchema = z.object({ type FormSchema = z.infer; export const RequestPasswordRecovery: React.FC = () => { + const { t } = useTranslation(); const isLoggedIn = useAppSelector((state) => selectIsAuthenticated({ state: selectAuthSlice(state) })); const [status, setStatus] = useState("initial"); const [error, setError] = useState(null); const navigate = useNavigate(); + useTitle(t("auth.recoverPassword.tabTitle")); + useEffect(() => { if (isLoggedIn) { navigate("/"); @@ -53,10 +58,10 @@ export const RequestPasswordRecovery: React.FC = () => { }} > - Recover Password + {t("auth.recoverPassword.header")} - Please enter your email. A recovery link will be sent shortly after. + {t("auth.recoverPassword.body")} {error && ( @@ -65,7 +70,7 @@ export const RequestPasswordRecovery: React.FC = () => { )} {status === "success" ? ( - A recovery link has been sent to you via email. + {t("auth.recoverPassword.emailSent")} ) : ( { fullWidth autoFocus type="text" - label="E-Mail" + label={t("common.email")} name="email" onBlur={handleBlur} onChange={handleChange} @@ -106,7 +111,7 @@ export const RequestPasswordRecovery: React.FC = () => { disabled={isSubmitting} sx={{ margin: "3 0 2 0" }} > - Confirm + {t("common.confirm")} )} @@ -116,5 +121,3 @@ export const RequestPasswordRecovery: React.FC = () => { ); }; - -export default RequestPasswordRecovery; diff --git a/frontend/apps/web/src/pages/groups/Group.tsx b/frontend/apps/web/src/pages/groups/Group.tsx index 70a6257b..10b4bfa2 100644 --- a/frontend/apps/web/src/pages/groups/Group.tsx +++ b/frontend/apps/web/src/pages/groups/Group.tsx @@ -12,8 +12,8 @@ import React, { Suspense } from "react"; import { batch } from "react-redux"; import { Navigate, Route, Routes, useParams } from "react-router-dom"; import { toast } from "react-toastify"; -import Balances from "../accounts/Balances"; -import Loading from "../../components/style/Loading"; +import { Balances } from "../accounts/Balances"; +import { Loading } from "../../components/style/Loading"; import { api, ws } from "../../core/api"; import { selectAccountSlice, @@ -27,10 +27,10 @@ import { PersonalAccountList } from "../accounts/PersonalAccountList"; import { ClearingAccountList } from "../accounts/ClearingAccountList"; import { TransactionList } from "../transactions/TransactionList"; import { SettlementPlanDisplay } from "../accounts/SettlementPlanDisplay"; -import GroupInvites from "./GroupInvites"; -import GroupLog from "./GroupLog"; -import GroupMemberList from "./GroupMemberList"; -import GroupSettings from "./GroupSettings"; +import { GroupInvites } from "./GroupInvites"; +import { GroupLog } from "./GroupLog"; +import { GroupMemberList } from "./GroupMemberList"; +import { GroupSettings } from "./GroupSettings"; import { TransactionDetail } from "../transactions/TransactionDetail"; export const Group: React.FC = () => { diff --git a/frontend/apps/web/src/pages/groups/GroupInvite.tsx b/frontend/apps/web/src/pages/groups/GroupInvite.tsx index bae632ef..ec0bd8b7 100644 --- a/frontend/apps/web/src/pages/groups/GroupInvite.tsx +++ b/frontend/apps/web/src/pages/groups/GroupInvite.tsx @@ -5,16 +5,18 @@ import { useTitle } from "@/core/utils"; import { GroupPreview } from "@abrechnung/api"; import { Alert, Button, Grid, List, ListItem, ListItemText, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; export const GroupInvite: React.FC = () => { + const { t } = useTranslation(); const [group, setGroup] = useState(null); const [error, setError] = useState(null); const params = useParams(); const navigate = useNavigate(); const inviteToken = params["inviteToken"]; - useTitle("Abrechnung - Join Group"); + useTitle(t("groups.join.tabTitle")); useEffect(() => { api.client.groups @@ -32,7 +34,7 @@ export const GroupInvite: React.FC = () => { const join = () => { api.client.groups .joinGroup({ requestBody: { invite_token: inviteToken } }) - .then((value) => { + .then(() => { setError(null); navigate("/"); }) @@ -50,34 +52,40 @@ export const GroupInvite: React.FC = () => { ) : ( <> -

You have been invited to group {group.name}

+

{t("groups.join.youHaveBeenInvited", { group })}

- + - + - + - + - + @@ -85,5 +93,3 @@ export const GroupInvite: React.FC = () => {
); }; - -export default GroupInvite; diff --git a/frontend/apps/web/src/pages/groups/GroupInvites.tsx b/frontend/apps/web/src/pages/groups/GroupInvites.tsx index 13616cda..90358db4 100644 --- a/frontend/apps/web/src/pages/groups/GroupInvites.tsx +++ b/frontend/apps/web/src/pages/groups/GroupInvites.tsx @@ -24,18 +24,20 @@ import { import { DateTime } from "luxon"; import React, { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import InviteLinkCreate from "../../components/groups/InviteLinkCreate"; -import Loading from "../../components/style/Loading"; -import { MobilePaper } from "../../components/style/mobile"; -import { api, ws } from "../../core/api"; -import { useTitle } from "../../core/utils"; -import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "../../store"; +import { InviteLinkCreate } from "@/components/groups/InviteLinkCreate"; +import { Loading } from "@/components/style/Loading"; +import { MobilePaper } from "@/components/style/mobile"; +import { api, ws } from "@/core/api"; +import { useTitle } from "@/core/utils"; +import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; } export const GroupInvites: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const [showModal, setShowModal] = useState(false); const dispatch = useAppDispatch(); const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); @@ -48,7 +50,7 @@ export const GroupInvites: React.FC = ({ groupId }) => { const isGuest = useAppSelector((state) => selectIsGuestUser({ state: selectAuthSlice(state) })); - useTitle(`${group.name} - Invite Links`); + useTitle(t("groups.invites.tabTitle", { groupName: group.name })); useEffect(() => { dispatch(fetchGroupInvites({ groupId, api })); @@ -91,13 +93,9 @@ export const GroupInvites: React.FC = ({ groupId }) => { return ( - Active Invite Links + {t("groups.invites.header")} - {isGuest && ( - - You are a guest user on this Abrechnung and therefore not permitted to create group invites. - - )} + {isGuest && {t("groups.invites.guestUserDisclaimer")}} {invitesLoadingStatus === "loading" ? ( ) : ( @@ -112,7 +110,7 @@ export const GroupInvites: React.FC = ({ groupId }) => { token hidden, was created by another member + {t("groups.invites.tokenHidden")} ) : ( {window.location.origin}/invite/ @@ -165,5 +163,3 @@ export const GroupInvites: React.FC = ({ groupId }) => { ); }; - -export default GroupInvites; diff --git a/frontend/apps/web/src/pages/groups/GroupList.tsx b/frontend/apps/web/src/pages/groups/GroupList.tsx index 1e41875e..d55d5e4a 100644 --- a/frontend/apps/web/src/pages/groups/GroupList.tsx +++ b/frontend/apps/web/src/pages/groups/GroupList.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; -import ListItemLink from "../../components/style/ListItemLink"; -import GroupCreateModal from "../../components/groups/GroupCreateModal"; -import GroupDeleteModal from "../../components/groups/GroupDeleteModal"; +import ListItemLink from "@/components/style/ListItemLink"; +import GroupCreateModal from "@/components/groups/GroupCreateModal"; +import GroupDeleteModal from "@/components/groups/GroupDeleteModal"; import { Alert, Grid, @@ -13,13 +13,15 @@ import { Typography, } from "@mui/material"; import { Add, Delete } from "@mui/icons-material"; -import { MobilePaper } from "../../components/style/mobile"; +import { MobilePaper } from "@/components/style/mobile"; import { selectIsGuestUser, selectGroups } from "@abrechnung/redux"; -import { useAppSelector, selectGroupSlice, selectAuthSlice } from "../../store"; -import { useTitle } from "../../core/utils"; +import { useAppSelector, selectGroupSlice, selectAuthSlice } from "@/store"; +import { useTitle } from "@/core/utils"; +import { useTranslation } from "react-i18next"; export const GroupList: React.FC = () => { - useTitle("Abrechnung - Groups"); + const { t } = useTranslation(); + useTitle(t("groups.list.tabTitle")); const [showGroupCreationModal, setShowGroupCreationModal] = useState(false); const [showGroupDeletionModal, setShowGroupDeletionModal] = useState(false); const [groupToDelete, setGroupToDelete] = useState(null); @@ -49,17 +51,13 @@ export const GroupList: React.FC = () => { return ( - Groups + {t("groups.list.header")} - {isGuest && ( - - You are a guest user on this Abrechnung and therefore not permitted to create new groups. - - )} + {isGuest && {t("groups.list.guestUserDisclaimer")}} {groups.length === 0 ? ( - No Groups + {t("groups.list.noGroups")} ) : ( groups.map((group) => { @@ -100,5 +98,3 @@ export const GroupList: React.FC = () => { ); }; - -export default GroupList; diff --git a/frontend/apps/web/src/pages/groups/GroupLog.tsx b/frontend/apps/web/src/pages/groups/GroupLog.tsx index 102ef269..24bea59e 100644 --- a/frontend/apps/web/src/pages/groups/GroupLog.tsx +++ b/frontend/apps/web/src/pages/groups/GroupLog.tsx @@ -21,17 +21,19 @@ import { import { DateTime } from "luxon"; import React, { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { Loading } from "../../components/style/Loading"; -import { MobilePaper } from "../../components/style/mobile"; -import { api, ws } from "../../core/api"; -import { useTitle } from "../../core/utils"; -import { selectGroupSlice, useAppDispatch, useAppSelector } from "../../store"; +import { Loading } from "@/components/style/Loading"; +import { MobilePaper } from "@/components/style/mobile"; +import { api, ws } from "@/core/api"; +import { useTitle } from "@/core/utils"; +import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; } export const GroupLog: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId })); const members = useAppSelector((state) => selectGroupMembers({ state: selectGroupSlice(state), groupId })); @@ -43,7 +45,7 @@ export const GroupLog: React.FC = ({ groupId }) => { const [showAllLogs, setShowAllLogs] = useState(false); const [message, setMessage] = useState(""); - useTitle(`${group.name} - Log`); + useTitle(t("groups.log.tabTitle", { groupName: group.name })); useEffect(() => { dispatch(fetchGroupLog({ groupId, api })); @@ -86,7 +88,7 @@ export const GroupLog: React.FC = ({ groupId }) => { return ( - Group Log + {t("groups.log.header")} = ({ groupId }) => { onChange={(e) => setShowAllLogs(e.target.checked)} /> } - label="Show all Logs" + label={t("groups.log.showAllLogs")} /> = ({ groupId }) => { onChange={(e) => setMessage(e.target.value)} /> @@ -122,8 +124,12 @@ export const GroupLog: React.FC = ({ groupId }) => { )) @@ -132,5 +138,3 @@ export const GroupLog: React.FC = ({ groupId }) => { ); }; - -export default GroupLog; diff --git a/frontend/apps/web/src/pages/groups/GroupMemberList.tsx b/frontend/apps/web/src/pages/groups/GroupMemberList.tsx index 011bb104..41c7795f 100644 --- a/frontend/apps/web/src/pages/groups/GroupMemberList.tsx +++ b/frontend/apps/web/src/pages/groups/GroupMemberList.tsx @@ -27,16 +27,18 @@ import { Form, Formik } from "formik"; import { DateTime } from "luxon"; import React, { useState } from "react"; import { toast } from "react-toastify"; -import { MobilePaper } from "../../components/style/mobile"; -import { api } from "../../core/api"; -import { useTitle } from "../../core/utils"; -import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "../../store"; +import { MobilePaper } from "@/components/style/mobile"; +import { api } from "@/core/api"; +import { useTitle } from "@/core/utils"; +import { selectAuthSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; } export const GroupMemberList: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const currentUserId = useAppSelector((state) => selectCurrentUserId({ state: selectAuthSlice(state) })); const members = useAppSelector((state) => selectGroupMembers({ state: selectGroupSlice(state), groupId })); @@ -45,7 +47,7 @@ export const GroupMemberList: React.FC = ({ groupId }) => { const [memberToEdit, setMemberToEdit] = useState(undefined); - useTitle(`${group.name} - Members`); + useTitle(t("groups.memberList.tabTitle", { groupName: group.name })); const handleEditMemberSubmit = (values, { setSubmitting }) => { dispatch( @@ -57,7 +59,7 @@ export const GroupMemberList: React.FC = ({ groupId }) => { }) ) .unwrap() - .then((result) => { + .then(() => { setSubmitting(false); setMemberToEdit(undefined); toast.success("Successfully updated group member permissions"); @@ -106,7 +108,7 @@ export const GroupMemberList: React.FC = ({ groupId }) => { sx={{ mr: 1 }} component="span" color="primary" - label="owner" + label={t("groups.memberList.owner")} variant="outlined" /> ) : member.can_write ? ( @@ -115,20 +117,18 @@ export const GroupMemberList: React.FC = ({ groupId }) => { sx={{ mr: 1 }} component="span" color="primary" - label="editor" + label={t("groups.memberList.editor")} variant="outlined" /> ) : null} - {member.user_id === currentUserId ? ( + {member.user_id === currentUserId && ( - ) : ( - "" )} } @@ -136,25 +136,27 @@ export const GroupMemberList: React.FC = ({ groupId }) => { <> {member.invited_by && ( - invited by {getMemberUsername(member.invited_by)} - {", "} + {t("groups.memberList.invitedBy", { + username: getMemberUsername(member.invited_by), + })} )} - joined{" "} - {DateTime.fromISO(member.joined_at).toLocaleString(DateTime.DATETIME_FULL)} + {t("groups.memberList.joined", { + datetime: DateTime.fromISO(member.joined_at).toLocaleString( + DateTime.DATETIME_FULL + ), + })} } /> - {permissions.isOwner || permissions.canWrite ? ( + {(permissions.isOwner || permissions.canWrite) && ( openEditMemberModal(member.user_id)}> - ) : ( - "" )} )) @@ -172,7 +174,7 @@ export const GroupMemberList: React.FC = ({ groupId }) => { onSubmit={handleEditMemberSubmit} enableReinitialize={true} > - {({ values, handleBlur, handleChange, isSubmitting, setFieldValue }) => ( + {({ values, handleBlur, isSubmitting, setFieldValue }) => (
= ({ groupId }) => { checked={values.canWrite} /> } - label="Can Write" + label={t("groups.memberList.canWrite")} /> = ({ groupId }) => { checked={values.isOwner} /> } - label="Is Owner" + label={t("groups.memberList.isOwner")} /> {isSubmitting && } @@ -214,5 +216,3 @@ export const GroupMemberList: React.FC = ({ groupId }) => { ); }; - -export default GroupMemberList; diff --git a/frontend/apps/web/src/pages/groups/GroupSettings.tsx b/frontend/apps/web/src/pages/groups/GroupSettings.tsx index 2e994e09..5648fadf 100644 --- a/frontend/apps/web/src/pages/groups/GroupSettings.tsx +++ b/frontend/apps/web/src/pages/groups/GroupSettings.tsx @@ -24,6 +24,7 @@ import { api } from "@/core/api"; import { useTitle } from "@/core/utils"; import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; import { toFormikValidationSchema } from "@abrechnung/utils"; +import { useTranslation } from "react-i18next"; const validationSchema = z.object({ name: z.string({ required_error: "group name is required" }), @@ -40,6 +41,7 @@ interface Props { } export const GroupSettings: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const [showLeaveModal, setShowLeaveModal] = useState(false); const navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -49,7 +51,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { const [isEditing, setIsEditing] = useState(false); - useTitle(`${group.name} - Settings`); + useTitle(t("groups.settings.tabTitle", { groupName: group.name })); const startEdit = () => { setIsEditing(true); @@ -98,9 +100,9 @@ export const GroupSettings: React.FC = ({ groupId }) => { return ( {permissions.isOwner ? ( - You are an owner of this group + {t("groups.settings.ownerDisclaimer")} ) : !permissions.canWrite ? ( - You only have read access to this group + {t("groups.settings.readAccessDisclaimer")} ) : null} = ({ groupId }) => { required fullWidth type="text" - label="Name" + label={t("common.name")} name="name" disabled={!permissions.canWrite || !isEditing} onBlur={handleBlur} @@ -137,7 +139,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { fullWidth type="text" name="description" - label="Description" + label={t("common.description")} disabled={!permissions.canWrite || !isEditing} onBlur={handleBlur} onChange={handleChange} @@ -150,7 +152,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { fullWidth type="text" name="currency_symbol" - label="Currency" + label={t("common.currency")} disabled={!permissions.canWrite || !isEditing} onBlur={handleBlur} onChange={handleChange} @@ -163,7 +165,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { fullWidth type="text" name="terms" - label="Terms" + label={t("groups.settings.terms")} disabled={!permissions.canWrite || !isEditing} onBlur={handleBlur} onChange={handleChange} @@ -180,7 +182,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { checked={values.addUserAccountOnJoin} /> } - label="Automatically add accounts for newly joined group members" + label={t("groups.settings.autoAddAccounts")} /> @@ -196,7 +198,7 @@ export const GroupSettings: React.FC = ({ groupId }) => { disabled={isSubmitting} startIcon={} > - Save + {t("common.save")} )} @@ -218,12 +220,12 @@ export const GroupSettings: React.FC = ({ groupId }) => { onClick={startEdit} startIcon={} > - Edit + {t("common.edit")} )}
@@ -240,26 +242,21 @@ export const GroupSettings: React.FC = ({ groupId }) => { {/**/} setShowLeaveModal(false)}> - Leave Group + {t("groups.settings.leaveGroup")} - - Are you sure you want to leave the group {group.name}. If you are the last member to leave - this group it will be deleted and its transaction will be lost forever... - + {t("groups.settings.leaveGroupConfirm", { group })} ); }; - -export default GroupSettings; diff --git a/frontend/apps/web/src/pages/profile/ChangeEmail.tsx b/frontend/apps/web/src/pages/profile/ChangeEmail.tsx index f959e7b2..cfc3c529 100644 --- a/frontend/apps/web/src/pages/profile/ChangeEmail.tsx +++ b/frontend/apps/web/src/pages/profile/ChangeEmail.tsx @@ -5,9 +5,9 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import { z } from "zod"; -import { MobilePaper } from "../../components/style/mobile"; -import { api } from "../../core/api"; -import { useTitle } from "../../core/utils"; +import { MobilePaper } from "@/components/style/mobile"; +import { api } from "@/core/api"; +import { useTitle } from "@/core/utils"; const validationSchema = z.object({ password: z.string({ required_error: "password is required" }), @@ -84,5 +84,3 @@ export const ChangeEmail: React.FC = () => { ); }; - -export default ChangeEmail; diff --git a/frontend/apps/web/src/pages/profile/ChangePassword.tsx b/frontend/apps/web/src/pages/profile/ChangePassword.tsx index 965acec9..71fd1a19 100644 --- a/frontend/apps/web/src/pages/profile/ChangePassword.tsx +++ b/frontend/apps/web/src/pages/profile/ChangePassword.tsx @@ -5,9 +5,10 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import { z } from "zod"; -import { MobilePaper } from "../../components/style/mobile"; -import { api } from "../../core/api"; -import { useTitle } from "../../core/utils"; +import { MobilePaper } from "@/components/style/mobile"; +import { api } from "@/core/api"; +import { useTitle } from "@/core/utils"; +import i18n from "@/i18n"; const validationSchema = z .object({ @@ -16,7 +17,7 @@ const validationSchema = z newPassword2: z.string({ required_error: "please repeat your desired new password" }), }) .refine((data) => data.newPassword === data.newPassword2, { - message: "passwords don't match", + message: i18n.t("common.passwordsDoNotMatch"), path: ["newPassword2"], }); type FormSchema = z.infer; @@ -108,5 +109,3 @@ export const ChangePassword: React.FC = () => { ); }; - -export default ChangePassword; diff --git a/frontend/apps/web/src/pages/profile/SessionList.tsx b/frontend/apps/web/src/pages/profile/SessionList.tsx index 28a5f5cd..42f12a29 100644 --- a/frontend/apps/web/src/pages/profile/SessionList.tsx +++ b/frontend/apps/web/src/pages/profile/SessionList.tsx @@ -18,13 +18,15 @@ import { import { DateTime } from "luxon"; import React, { useState } from "react"; import { toast } from "react-toastify"; -import Loading from "../../components/style/Loading"; -import { MobilePaper } from "../../components/style/mobile"; -import { api } from "../../core/api"; -import { useTitle } from "../../core/utils"; -import { selectAuthSlice, useAppSelector } from "../../store"; +import Loading from "@/components/style/Loading"; +import { MobilePaper } from "@/components/style/mobile"; +import { api } from "@/core/api"; +import { useTitle } from "@/core/utils"; +import { selectAuthSlice, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; export const SessionList: React.FC = () => { + const { t } = useTranslation(); // TODO: fix editing functions const [editedSessions, setEditedSessions] = useState({}); const [sessionToDelete, setSessionToDelete] = useState({ @@ -33,7 +35,7 @@ export const SessionList: React.FC = () => { }); const profile = useAppSelector((state) => selectProfile({ state: selectAuthSlice(state) })); - useTitle("Abrechnung - Sessions"); + useTitle(t("profile.sessions.tabTitle")); const editSession = (id) => { if (editedSessions[id] === undefined) { @@ -95,7 +97,7 @@ export const SessionList: React.FC = () => { return ( - Login Sessions + {t("profile.sessions.header")} {profile === undefined ? ( @@ -161,29 +163,29 @@ export const SessionList: React.FC = () => { )} - Delete Session? + {t("profile.sessions.confirmDeleteSession")} {sessionToDelete.toDelete !== null - ? `Are you sure you want to delete session ${profile?.sessions.find( - (session) => session.id === sessionToDelete.toDelete - )?.name}` + ? t("profile.sessions.areYouSureToDelete", { + sessionName: profile?.sessions.find( + (session) => session.id === sessionToDelete.toDelete + )?.name, + }) : null} ); }; - -export default SessionList; diff --git a/frontend/apps/web/src/pages/profile/Settings.tsx b/frontend/apps/web/src/pages/profile/Settings.tsx index 8a508858..4b27ca7c 100644 --- a/frontend/apps/web/src/pages/profile/Settings.tsx +++ b/frontend/apps/web/src/pages/profile/Settings.tsx @@ -117,5 +117,3 @@ export const Settings: React.FC = () => { ); }; - -export default Settings; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx index e884603a..1655c041 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/FileGallery.tsx @@ -11,6 +11,7 @@ import { toast } from "react-toastify"; import { Transition } from "react-transition-group"; import { ImageUploadDialog } from "./ImageUploadDialog"; import placeholderImg from "./PlaceholderImage.svg"; +import { useTranslation } from "react-i18next"; const duration = 200; @@ -86,6 +87,7 @@ export interface FileGalleryProps { } export const FileGallery: React.FC = ({ groupId, transactionId }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const transaction = useAppSelector((state) => selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId }) @@ -274,7 +276,7 @@ export const FileGallery: React.FC = ({ groupId, transactionId {transaction.is_wip && ( )} @@ -282,5 +284,3 @@ export const FileGallery: React.FC = ({ groupId, transactionId ); }; - -export default FileGallery; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/ImageUploadDialog.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/ImageUploadDialog.tsx index cec6009b..1ae6d383 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/ImageUploadDialog.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/ImageUploadDialog.tsx @@ -17,6 +17,7 @@ import imageCompression from "browser-image-compression"; import React, { useState } from "react"; import { useAppDispatch } from "@/store"; import placeholderImg from "./PlaceholderImage.svg"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -28,6 +29,7 @@ interface Props { const MAX_FILESIZE_MB = 1; export const ImageUploadDialog: React.FC = ({ groupId, transactionId, show, onClose }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const [selectedFile, setSelectedFile] = useState(undefined); const [compressionProgress, setCompressionProgress] = useState(undefined); @@ -90,7 +92,7 @@ export const ImageUploadDialog: React.FC = ({ groupId, transactionId, sho return ( - Upload Image + {t("images.uploadImage")} {selectedFile ? ( @@ -112,14 +114,14 @@ export const ImageUploadDialog: React.FC = ({ groupId, transactionId, sho >{`${compressionProgress}%`} - compressing ... + {t("images.compressing")} )} {selectedFile && ( = ({ groupId, transactionId, sho accept="image/*" onChange={selectFile} /> - + ); }; - -export default ImageUploadDialog; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionActions.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionActions.tsx index 6c0b0061..98d16a73 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionActions.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionActions.tsx @@ -14,6 +14,7 @@ import { import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { selectTransactionSlice, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -34,6 +35,7 @@ export const TransactionActions: React.FC = ({ onAbortEdit, showProgress = false, }) => { + const { t } = useTranslation(); const navigate = useNavigate(); const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = useState(false); @@ -64,10 +66,10 @@ export const TransactionActions: React.FC = ({ {transaction.is_wip ? ( <> ) : ( @@ -84,21 +86,19 @@ export const TransactionActions: React.FC = ({
{showProgress && } - Confirm delete transaction + {t("transactions.confirmDeleteTransaction")} - Are you sure you want to delete the transaction "{transaction.name}" + {t("transactions.confirmDeleteTransactionInfo", { transaction })} ); }; - -export default TransactionActions; diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx index c1578688..75d8e2ae 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx @@ -22,6 +22,7 @@ import { typeToFlattenedError, z } from "zod"; import { TransactionActions } from "./TransactionActions"; import { TransactionMetadata } from "./TransactionMetadata"; import { ValidationErrors as PositionValidationErrors, TransactionPositions } from "./purchase/TransactionPositions"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -31,6 +32,7 @@ const emptyErrors = { fieldErrors: {}, formErrors: [] }; const emptyPositionErrors = {}; export const TransactionDetail: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const params = useParams(); const dispatch = useAppDispatch(); const query = useQuery(); @@ -163,7 +165,7 @@ export const TransactionDetail: React.FC = ({ groupId }) => { {transaction.type === "purchase" && !showPositions && transaction.is_wip && !hasPositions ? ( ) : (showPositions && transaction.is_wip) || hasPositions ? ( diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx index 2ac3aa71..fc10ee2f 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/TransactionMetadata.tsx @@ -17,6 +17,8 @@ import { Grid, InputAdornment, TableCell } from "@mui/material"; import * as React from "react"; import { typeToFlattenedError, z } from "zod"; import { FileGallery } from "./FileGallery"; +import { useTranslation } from "react-i18next"; +import { useFormatCurrency } from "@/hooks"; interface Props { groupId: number; @@ -31,6 +33,8 @@ export const TransactionMetadata: React.FC = ({ validationErrors, showPositions = false, }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const dispatch = useAppDispatch(); const transaction = useAppSelector((state) => selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId }) @@ -50,30 +54,30 @@ export const TransactionMetadata: React.FC = ({ showPositions || hasPositions ? ( <> - {(balanceEffect[account.id]?.positions ?? 0).toFixed(2)} {transaction.currency_symbol} + {formatCurrency(balanceEffect[account.id]?.positions ?? 0, transaction.currency_symbol)} - {(balanceEffect[account.id]?.commonDebitors ?? 0).toFixed(2)} {transaction.currency_symbol} + {formatCurrency(balanceEffect[account.id]?.commonDebitors ?? 0, transaction.currency_symbol)} - {( + {formatCurrency( (balanceEffect[account.id]?.commonDebitors ?? 0) + - (balanceEffect[account.id]?.positions ?? 0) - ).toFixed(2)}{" "} - {transaction.currency_symbol} + (balanceEffect[account.id]?.positions ?? 0), + transaction.currency_symbol + )} ) : ( - {( - (balanceEffect[account.id]?.commonDebitors ?? 0) + (balanceEffect[account.id]?.positions ?? 0) - ).toFixed(2)}{" "} - {transaction.currency_symbol} + {formatCurrency( + (balanceEffect[account.id]?.commonDebitors ?? 0) + (balanceEffect[account.id]?.positions ?? 0), + transaction.currency_symbol + )} ), - [showPositions, hasPositions, transaction, balanceEffect] + [showPositions, hasPositions, transaction, balanceEffect, formatCurrency] ); const shouldDisplayAccount = React.useCallback( @@ -109,7 +113,7 @@ export const TransactionMetadata: React.FC = ({ = ({ /> {!transaction.is_wip && transaction.description === "" ? null : ( = ({ /> )} = ({ = ({ = ({ = ({ = ({ showPositions || hasPositions ? ( <> - Positions + {t("transactions.positions.positions")} + - Shared + Rest + {t("transactions.positions.sharedPlusRest")} = - Total + {t("common.total")} ) : ( - Shared + {t("common.shared")} ) } diff --git a/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/TransactionPositions.tsx b/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/TransactionPositions.tsx index 4984fe96..1062becb 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/TransactionPositions.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionDetail/purchase/TransactionPositions.tsx @@ -2,6 +2,7 @@ import { AccountSelect } from "@/components/AccountSelect"; import { NumericInput } from "@/components/NumericInput"; import { TextInput } from "@/components/TextInput"; import { MobilePaper } from "@/components/style/mobile"; +import { useFormatCurrency } from "@/hooks"; import { RootState, selectAccountSlice, selectTransactionSlice, useAppDispatch, useAppSelector } from "@/store"; import { positionDeleted, @@ -31,6 +32,7 @@ import { } from "@mui/material"; import memoize from "proxy-memoize"; import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import { typeToFlattenedError, z } from "zod"; interface PositionTableRowProps { @@ -196,6 +198,8 @@ export const TransactionPositions: React.FC = ({ transactionId, validationErrors, }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const accounts = useAppSelector((state) => selectGroupAccounts({ state: selectAccountSlice(state), groupId })); const transaction = useAppSelector((state) => selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId }) @@ -276,13 +280,13 @@ export const TransactionPositions: React.FC = ({ return ( - Positions + {t("transactions.positions.positions")} {transaction.is_wip && ( } + control={} checked={showAdvanced} onChange={(event: React.ChangeEvent) => setShowAdvanced(event.target.checked)} - label="Advanced" + label={t("common.advanced")} /> )} @@ -290,8 +294,8 @@ export const TransactionPositions: React.FC = ({ - Name - Price + {t("common.name")} + {t("common.price")} {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => ( {accounts.find((account) => account.id === accountID).name} @@ -317,7 +321,7 @@ export const TransactionPositions: React.FC = ({ )} )} - Shared + {t("common.shared")} {transaction.is_wip && } @@ -342,7 +346,7 @@ export const TransactionPositions: React.FC = ({ {position.name} - {position.price.toFixed(2)} {transaction.currency_symbol} + {formatCurrency(position.price, transaction.currency_symbol)} {positionAccounts.map((accountID) => ( @@ -371,34 +375,36 @@ export const TransactionPositions: React.FC = ({ ))} - Total: + {t("common.totalWithColon")} - {totalPositionValue.toFixed(2)} {transaction.currency_symbol} + {formatCurrency(totalPositionValue, transaction.currency_symbol)} {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => ( - {purchaseItemSumForAccount(accountID).toFixed(2)} {transaction.currency_symbol} + {formatCurrency(purchaseItemSumForAccount(accountID), transaction.currency_symbol)} ))} - {( + {formatCurrency( positions.reduce((acc, curr) => acc + curr.price, 0) - - Object.values(transactionBalanceEffect).reduce( - (acc, curr) => acc + curr.positions, - 0 - ) - ).toFixed(2)}{" "} - {transaction.currency_symbol} + Object.values(transactionBalanceEffect).reduce( + (acc, curr) => acc + curr.positions, + 0 + ), + transaction.currency_symbol + )} {transaction.is_wip && } - Remaining: + + {t("transactions.positions.remaining")} + - {sharedTransactionValue.toFixed(2)} {transaction.currency_symbol} + {formatCurrency(sharedTransactionValue, transaction.currency_symbol)} {(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => ( @@ -412,5 +418,3 @@ export const TransactionPositions: React.FC = ({ ); }; - -export default TransactionPositions; diff --git a/frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx b/frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx index 52f8542f..68d50397 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionList/TransactionList.tsx @@ -37,6 +37,7 @@ import { MobilePaper } from "@/components/style/mobile"; import { useTitle } from "@/core/utils"; import { selectGroupSlice, useAppDispatch, useAppSelector } from "@/store"; import { TransactionListItem } from "./TransactionListItem"; +import { useTranslation } from "react-i18next"; interface Props { groupId: number; @@ -46,6 +47,7 @@ const emptyList = []; const MAX_ITEMS_PER_PAGE = 40; export const TransactionList: React.FC = ({ groupId }) => { + const { t } = useTranslation(); const theme: Theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("md")); @@ -77,7 +79,7 @@ export const TransactionList: React.FC = ({ groupId }) => { (currentPage + 1) * MAX_ITEMS_PER_PAGE ); - useTitle(`${group.name} - Transactions`); + useTitle(t("transactions.list.tabTitle", { groupName: group.name })); const onCreatePurchase = () => { dispatch(createTransaction({ groupId, type: "purchase" })) @@ -116,7 +118,7 @@ export const TransactionList: React.FC = ({ groupId }) => { setSearchValue(e.target.value)} - placeholder="Search…" + placeholder={t("common.search")} inputProps={{ "aria-label": "search", }} @@ -134,23 +136,23 @@ export const TransactionList: React.FC = ({ groupId }) => { } /> - Sort by + {t("common.sortBy")} = ({ groupId }) => {
- + - + @@ -181,7 +183,7 @@ export const TransactionList: React.FC = ({ groupId }) => { {paginatedTransactions.length === 0 ? ( - No Transactions + {t("transactions.noTransactions")} ) : ( paginatedTransactions.map((transaction) => ( = ({ groupId }) => { {permissions.canWrite && ( } // onClose={() => setSpeedDialOpen(false)} @@ -218,13 +220,13 @@ export const TransactionList: React.FC = ({ groupId }) => { > } - tooltipTitle="Purchase" + tooltipTitle={t("transactions.purchase")} tooltipOpen onClick={onCreatePurchase} /> } - tooltipTitle="Transfer" + tooltipTitle={t("transactions.transfer")} tooltipOpen onClick={onCreateTransfer} /> @@ -233,5 +235,3 @@ export const TransactionList: React.FC = ({ groupId }) => { ); }; - -export default TransactionList; diff --git a/frontend/apps/web/src/pages/transactions/TransactionList/TransactionListItem.tsx b/frontend/apps/web/src/pages/transactions/TransactionList/TransactionListItem.tsx index 0d42ef47..2f21b736 100644 --- a/frontend/apps/web/src/pages/transactions/TransactionList/TransactionListItem.tsx +++ b/frontend/apps/web/src/pages/transactions/TransactionList/TransactionListItem.tsx @@ -6,6 +6,8 @@ import React from "react"; import { PurchaseIcon, TransferIcon } from "@/components/style/AbrechnungIcons"; import { ListItemLink } from "@/components/style/ListItemLink"; import { selectAccountSlice, selectTransactionSlice, useAppSelector } from "@/store"; +import { useTranslation } from "react-i18next"; +import { useFormatCurrency } from "@/hooks"; interface Props { groupId: number; @@ -14,6 +16,8 @@ interface Props { } export const TransactionListItem: React.FC = ({ groupId, transactionId, style }) => { + const { t } = useTranslation(); + const formatCurrency = useFormatCurrency(); const accounts = useAppSelector((state) => selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId }) ); @@ -45,11 +49,11 @@ export const TransactionListItem: React.FC = ({ groupId, transactionId, s {transaction.type === "purchase" ? ( - + ) : transaction.type === "transfer" ? ( - + ) : ( @@ -74,7 +78,7 @@ export const TransactionListItem: React.FC = ({ groupId, transactionId, s secondary={ <> - by {creditorNames}, for {debitorNames} + {t("transactions.byFor", { by: creditorNames, for: debitorNames })}
{DateTime.fromISO(transaction.billed_at).toLocaleString(DateTime.DATE_FULL)} @@ -86,11 +90,14 @@ export const TransactionListItem: React.FC = ({ groupId, transactionId, s /> - {transaction.value.toFixed(2)} {transaction.currency_symbol} + {formatCurrency(transaction.value, transaction.currency_symbol)}
- last changed:{" "} - {DateTime.fromISO(transaction.last_changed).toLocaleString(DateTime.DATETIME_FULL)} + {t("common.lastChangedWithTime", { + datetime: DateTime.fromISO(transaction.last_changed).toLocaleString( + DateTime.DATETIME_FULL + ), + })}
diff --git a/frontend/libs/translations/src/lib/de.ts b/frontend/libs/translations/src/lib/de.ts index 8b34f60b..27db1dc7 100644 --- a/frontend/libs/translations/src/lib/de.ts +++ b/frontend/libs/translations/src/lib/de.ts @@ -1,12 +1,124 @@ import type { en } from "./en"; +import type { ReplaceConstStringWithString, DeepPartial } from "./util"; + +type EnglishTranslations = DeepPartial>; const translations = { + app: { + name: "Abrechnung", + }, + common: { + username: "Username", + server: "Server", + email: "E-Mail", + password: "Passwort", + repeatPassword: "Passwort wiederholen", + save: "Speichern", + yes: "Ja", + ok: "Ok", + delete: "Löschen", + add: "Hinzufügen", + cancel: "Abbrechen", + search: "Suche ...", + name: "Name", + lastChanged: "Zuletzt geändert", + lastChangedWithTime: "zuletzt geändert: {{datetime}}", + value: "Wert", + date: "Datum", + description: "Beschreibung", + sortBy: "Sortieren nach", + filterByTags: "Nach Tags filtern", + tag_one: "Tag", + tag_other: "Tags", + total: "Summe", + totalWithColon: "Summe:", + shared: "Geteilt", + advanced: "Erweitert", + price: "Preis", + }, + groups: { + addGroup: "Gruppe hinzufügen", + }, + images: { + uploadImage: "Bild hochladen", + chooseImage: "Bild auswählen", + compressing: "komprimieren ..", + filename: "Dateiname", + }, + transactions: { + createTransaction: "Transaktion erstellen", + createPurchase: "Einkauf erstellen", + createTransfer: "Überweisung erstellen", + noTransactions: "Keine Transaktionen", + purchase: "Einkauf", + transfer: "Überweisung", + transferredFrom: "Von", + transferredTo: "An", + paidBy: "Bezahlt von", + paidFor: "Für wen", + confirmDeleteTransaction: "Löschen der Transaktion bestätigen", + confirmDeleteTransactionInfo: + 'Sind Sie sicher, dass Sie die Transaktion "{{transaction.name}}" löschen möchten?', + list: { + tabTitle: "{{groupName}} - Transaktionen", + }, + byFor: "von {{by}}, für {{for}}", + positions: { + positions: "Positionen", + sharedPlusRest: "Geteilt + Rest", + addPositions: "Position hinzufügen", + remaining: "Verbleibend:", + }, + }, + accounts: { + list: { + tabTitle: "{{groupName}} - Konten", + }, + }, + profile: { + index: { + tabTitle: "Abrechnung - Profil", + pageTitle: "Profil", + guestUserDisclaimer: + "Sie sind ein Gastbenutzer auf dieser Abrechnung und daher nicht berechtigt, neue Gruppen oder Gruppeneinladungen zu erstellen.", + registered: "Registriert", + }, + settings: { + tabTitle: "Abrechnung - Einstellungen", + pageTitle: "Einstellungen", + info: "Diese Einstellungen werden lokal auf Ihrem Gerät gespeichert. Das Löschen des lokalen Speichers Ihres Browsers setzt sie zurück.", + theme: "Thema", + themeSystemDefault: "Systemstandard", + themeDarkMode: "Dark Theme", + themeLightMode: "Light Theme", + clearCache: "Cache leeren", + confirmClearCache: + "Diese Aktion wird Ihren lokalen Cache löschen. Alle Ihre Einstellungen (diese Seite) werden nicht zurückgesetzt.", + }, + changePassword: { + tabTitle: "Abrechnung - Passwort ändern", + pageTitle: "Passwort ändern", + success: "Passwort erfolgreich geändert", + newPassword: "Neues Passwort", + }, + changeEmail: { + tabTitle: "Abrechnung - E-Mail ändern", + pageTitle: "E-Mail ändern", + success: "E-Mail-Änderung angefordert. Sie sollten bald eine E-Mail mit einem Bestätigungslink erhalten.", + newEmail: "Neue E-Mail", + }, + }, + auth: { + register: { + header: "Registrieren", + }, + }, languages: { en: "Englisch", de: "Deutsch", - }, -} satisfies Partial<(typeof en)["translations"]>; + } as const, +} satisfies EnglishTranslations; export const de = { translations, -}; +} as const; diff --git a/frontend/libs/translations/src/lib/en.ts b/frontend/libs/translations/src/lib/en.ts index a0955926..34b9325a 100644 --- a/frontend/libs/translations/src/lib/en.ts +++ b/frontend/libs/translations/src/lib/en.ts @@ -8,12 +8,186 @@ const translations = { email: "E-Mail", password: "Password", repeatPassword: "Repeat Password", + passwordsDoNotMatch: "Passwords do not match", save: "Save", yes: "Yes", + no: "No", + ok: "Ok", + edit: "Edit", + delete: "Delete", + confirm: "Confirm", + add: "Add", cancel: "Cancel", + search: "Search ...", + name: "Name", + lastChanged: "Last changed", + lastChangedWithTime: "last changed: {{datetime}}", + value: "Value", + date: "Date", + description: "Description", + sortBy: "Sort by", + filterByTags: "Filter by tags", + tag_one: "Tag", + tag_other: "Tags", + total: "Total", + totalWithColon: "Total:", + shared: "Shared", + shares: "Shares", + advanced: "Advanced", + price: "Price", + createdAt: "Created At", + send: "Send", + currency: "Currency", + addNewTag: "Add new Tag", + }, + shareSelect: { + selectedPeople_one: "{{count}} Person", + selectedPeople_other: "{{count}} People", + selectedEvent_one: "{{count}} Event", + selectedEvent_other: "{{count}} Events", + accountSlashEvent: "Account / Event", + showEvents: "Show Events", + }, + navbar: { + transactions: "Transactions", + events: "Events", + balances: "Balances", + accounts: "Accounts", + groupSettings: "Group Settings", + groupMembers: "Group Members", + groupInvites: "Group Invites", + groupLog: "Group Log", + profile: "Profile", + settings: "Settings", + sessions: "Sessions", + changeEmail: "Change E-Mail", + changePassword: "Change Password", + login: "Login", + signOut: "Sign Out", + imprint: "Imprint", }, groups: { addGroup: "Add Group", + list: { + tabTitle: "Abrechnung - Groups", + header: "Groups", + guestUserDisclaimer: + "You are a guest user on this Abrechnung and therefore not permitted to create new groups.", + noGroups: "No Groups", + }, + log: { + tabTitle: "{{groupName}} - Log", + header: "Group Log", + showAllLogs: "Show all logs", + writeAMessage: "Write a message to the group ...", + messageInfo: "by {{username}} on {{datetime}}", + }, + memberList: { + tabTitle: "{{groupName}} - Members", + invitedBy: "invited by {{username}}, ", + joined: "joined {{datetime}}", + editor: "Editor", + owner: "Owner", + itsYou: "It's you", + canWrite: "Can Write", + isOwner: "Is Owner", + }, + settings: { + tabTitle: "{{groupName}} - Settings", + ownerDisclaimer: "You are an owner of this group", + readAccessDisclaimer: "You only have read access to this group", + terms: "Terms", + autoAddAccounts: "Automatically add accounts for newly joined group members", + leaveGroup: "Leave Group", + leaveGroupConfirm: + "Are you sure you want to leave the group {{group.name}}. If you are the last member to leave this group it will be deleted and its transaction will be lost forever...", + }, + join: { + tabTitle: "Abrechnung - Join Group", + youHaveBeenInvited: "You have been invited to group {{group.name}}", + invitationDescription: "Invitation Description", + invitationValidUntil: "Invitation Valid Until", + invitationSingleUse: "Invitation Single Use", + join: "Join", + }, + invites: { + tabTitle: "{{groupName}} - Invite Links", + header: "Active Invite Links", + guestUserDisclaimer: + "You are a guest user on this Abrechnung and therefore not permitted to create group invites.", + tokenHidden: "token hidden, was created by another member", + }, + }, + images: { + uploadImage: "Upload Image", + chooseImage: "Choose Image", + compressing: "compressing ..", + filename: "File Name", + }, + balanceTable: { + totalConsumed: "Received / Consumed", + totalPaid: "Paid", + balance: "Balance", + }, + transactions: { + createTransaction: "Create Transaction", + createPurchase: "Create Purchase", + createTransfer: "Create Transfer", + noTransactions: "No Transactions", + purchase: "Purchase", + transfer: "Transfer", + transferredFrom: "From", + transferredTo: "To", + paidBy: "Paid by", + paidFor: "For whom", + confirmDeleteTransaction: "Confirm delete transaction", + confirmDeleteTransactionInfo: 'Are you sure you want to delete the transaction "{{transaction.name}}"', + list: { + tabTitle: "{{groupName}} - Transactions", + }, + byFor: "by {{by}}, for {{for}}", + positions: { + positions: "Positions", + sharedPlusRest: "Shared + Rest", + addPositions: "Add Positions", + remaining: "Remaining:", + }, + }, + accounts: { + noAccounts: "No Accounts", + list: { + tabTitle: "{{groupName}} - Accounts", + }, + balances: { + tabTitle: "{{groupName}} - Balances", + clearingAccountsRemainingBalances: "Some Events have remaining balances", + }, + detail: { + tabTitleEvent: "{{group.name}} - Event {{account.name}}", + tabTitleAccount: "{{group.name}} - Account {{account.name}}", + }, + event: "Event", + account: "Account", + balanceOf: "Balance of {{account.name}}", + clearingDistributionOf: "Clearing distribution of {{account.name}}", + transactionsInvolving: "Transactions involving {{account.name}}", + participated: "Participated", + settleUp: "Settle Up", + settlement: { + transactionName: "Settlement", + title: "Settle this groups balances", + whoPaysWhom: "{{from}} pays {{to}} {{money}}", + settleButton: "Settle", + }, + deleteConfirm: "Confirm delete {{accountType}}", + deleteConfirmBody: 'Are you sure you want to delete the {{accountType}} "{{accountName}}"', + }, + events: { + list: { + tabTitle: "{{groupName}} - Events", + }, + createEvent: "Create Event", + noEvents: "No Events", }, profile: { index: { @@ -47,18 +221,60 @@ const translations = { success: "Requested email change, you should receive an email with a confirmation link soon", newEmail: "New E-Mail", }, + sessions: { + tabTitle: "Abrechnung - Sessions", + header: "Login Sessions", + confirmDeleteSession: "Delete Session?", + areYouSureToDelete: "Are you sure you want to delete session {{sessionName}}", + }, }, auth: { register: { - title: "Register", + tabTitle: "Abrechnung - Register", + header: "Register a new account", + confirmButton: "Register", + registrationSuccess: "Registered successfully, please confirm your email before logging in...", + alreadyHasAccount: "Already have an account? Sign in", + }, + login: { + tabTitle: "Abrechnung - Login", + header: "Sign in", + loginSuccess: "Logged in ...", + confirmButton: "Login", + noAccountRegister: "No account? register", + forgotPassword: "Forgot your password?", + }, + recoverPassword: { + tabTitle: "Abrechnung - Recover Password", + header: "Recover Password", + body: "Please enter your email. A recovery link will be sent shortly after.", + emailSent: "A recovery link has been sent to you via email.", + }, + confirmEmailChange: { + tabTitle: "Abrechnung - Confirm E-Mail Change", + header: "Confirm your new E-Mail", + confirmSuccessful: "Confirmation successful", + clickHereToConfirm: "Click <1>here to confirm your new email.", + }, + confirmPasswordRecovery: { + tabTitle: "Abrechnung - Confirm Password Recovery", + header: "Confirm Password Recovery", + successfulLinkToLogin: "Password recovery successful, please <1>login using your new password.", + }, + confirmRegistration: { + tabTitle: "Abrechnung - Confirm Registration", + header: "Confirm Registration", + confirmSuccessful: "Confirmation successful", + successfulLinkToLogin: "Please <1>login using your credentials.", + clickHereToConfirm: "Click <1>here to confirm your registration.", }, }, languages: { en: "English", de: "German", }, -}; +} as const; export const en = { translations, -}; +} as const; diff --git a/frontend/libs/translations/src/lib/util.ts b/frontend/libs/translations/src/lib/util.ts new file mode 100644 index 00000000..d4fa9614 --- /dev/null +++ b/frontend/libs/translations/src/lib/util.ts @@ -0,0 +1,7 @@ +export type ReplaceConstStringWithString = { + [K in keyof T]: T[K] extends string ? string : ReplaceConstStringWithString; +}; + +export type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8230fc8f..f08170e4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,7 @@ "core-js": "^3.35.0", "deepmerge": "^4.3.1", "formik": "^2.4.5", - "i18next": "^23.7.13", + "i18next": "^22.5.1 ", "i18next-browser-languagedetector": "^7.2.0", "localforage": "^1.10.0", "luxon": "^3.4.4", @@ -119,7 +119,6 @@ "eslint-plugin-prettier": "^5.1.2", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", - "i18next-parser": "^8.12.0", "jest": "29.7.0", "jest-circus": "29.7.0", "jest-environment-jsdom": "29.7.0", @@ -2552,22 +2551,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", - "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4260,18 +4243,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, - "node_modules/@gulpjs/to-absolute-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", - "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", - "dev": true, - "dependencies": { - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -12026,12 +11997,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, - "node_modules/@types/symlink-or-copy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz", - "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", - "dev": true - }, "node_modules/@types/tough-cookie": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.4.tgz", @@ -14121,100 +14086,6 @@ "node": ">=8" } }, - "node_modules/broccoli-node-api": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", - "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", - "dev": true - }, - "node_modules/broccoli-node-info": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", - "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", - "dev": true, - "engines": { - "node": "8.* || >= 10.*" - } - }, - "node_modules/broccoli-output-wrapper": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", - "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", - "dev": true, - "dependencies": { - "fs-extra": "^8.1.0", - "heimdalljs-logger": "^0.1.10", - "symlink-or-copy": "^1.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/broccoli-plugin": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", - "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", - "dev": true, - "dependencies": { - "broccoli-node-api": "^1.7.0", - "broccoli-output-wrapper": "^3.2.5", - "fs-merger": "^3.2.1", - "promise-map-series": "^0.3.0", - "quick-temp": "^0.1.8", - "rimraf": "^3.0.2", - "symlink-or-copy": "^1.3.1" - }, - "engines": { - "node": "10.* || >= 12.*" - } - }, - "node_modules/broccoli-plugin/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/browser-image-compression": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", @@ -14669,177 +14540,6 @@ "node": "*" } }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dev": true, - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cheerio-select/node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cheerio-select/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/cheerio-select/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/cheerio-select/node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "entities": "^4.3.0" - } - }, - "node_modules/cheerio/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/child-process-promise": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", @@ -14970,15 +14670,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -15004,12 +14695,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "dev": true - }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -15096,15 +14781,6 @@ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "dev": true }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/columnify": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", @@ -16073,7 +15749,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/debug": { "version": "4.3.4", @@ -17214,12 +16892,6 @@ "node": ">=8.6" } }, - "node_modules/ensure-posix-path": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", - "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", - "dev": true - }, "node_modules/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -17257,12 +16929,6 @@ "node": ">=4" } }, - "node_modules/eol": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", - "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", - "dev": true - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -17450,43 +17116,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", - "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.5", - "@esbuild/android-arm64": "0.19.5", - "@esbuild/android-x64": "0.19.5", - "@esbuild/darwin-arm64": "0.19.5", - "@esbuild/darwin-x64": "0.19.5", - "@esbuild/freebsd-arm64": "0.19.5", - "@esbuild/freebsd-x64": "0.19.5", - "@esbuild/linux-arm": "0.19.5", - "@esbuild/linux-arm64": "0.19.5", - "@esbuild/linux-ia32": "0.19.5", - "@esbuild/linux-loong64": "0.19.5", - "@esbuild/linux-mips64el": "0.19.5", - "@esbuild/linux-ppc64": "0.19.5", - "@esbuild/linux-riscv64": "0.19.5", - "@esbuild/linux-s390x": "0.19.5", - "@esbuild/linux-x64": "0.19.5", - "@esbuild/netbsd-x64": "0.19.5", - "@esbuild/openbsd-x64": "0.19.5", - "@esbuild/sunos-x64": "0.19.5", - "@esbuild/win32-arm64": "0.19.5", - "@esbuild/win32-ia32": "0.19.5", - "@esbuild/win32-x64": "0.19.5" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -18694,12 +18323,6 @@ "node": ">=6.0.0" } }, - "node_modules/fast-fifo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.1.0.tgz", - "integrity": "sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g==", - "dev": true - }, "node_modules/fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", @@ -19378,51 +19001,6 @@ "node": ">=12" } }, - "node_modules/fs-merger": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", - "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", - "dev": true, - "dependencies": { - "broccoli-node-api": "^1.7.0", - "broccoli-node-info": "^2.1.0", - "fs-extra": "^8.0.1", - "fs-tree-diff": "^2.0.1", - "walk-sync": "^2.2.0" - } - }, - "node_modules/fs-merger/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-merger/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/fs-merger/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -19435,41 +19013,12 @@ "node": ">= 8" } }, - "node_modules/fs-mkdirp-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", - "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.8", - "streamx": "^2.12.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/fs-monkey": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, - "node_modules/fs-tree-diff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", - "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", - "dev": true, - "dependencies": { - "@types/symlink-or-copy": "^1.2.0", - "heimdalljs-logger": "^0.1.7", - "object-assign": "^4.1.0", - "path-posix": "^1.0.0", - "symlink-or-copy": "^1.1.8" - }, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -19643,37 +19192,6 @@ "node": ">= 6" } }, - "node_modules/glob-stream": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.0.tgz", - "integrity": "sha512-CdIUuwOkYNv9ZadR3jJvap8CMooKziQZ/QCSPhEb7zqfsEI5YnPmvca7IvbaVE3z58ZdUYD2JsU6AUWjL8WZJA==", - "dev": true, - "dependencies": { - "@gulpjs/to-absolute-glob": "^4.0.0", - "anymatch": "^3.1.3", - "fastq": "^1.13.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "is-negated-glob": "^1.0.0", - "normalize-path": "^3.0.0", - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-stream/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -19828,15 +19346,6 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/gulp-sort": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", - "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", - "dev": true, - "dependencies": { - "through2": "^2.0.1" - } - }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -19981,46 +19490,6 @@ "he": "bin/he" } }, - "node_modules/heimdalljs": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", - "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", - "dev": true, - "dependencies": { - "rsvp": "~3.2.1" - } - }, - "node_modules/heimdalljs-logger": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", - "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", - "dev": true, - "dependencies": { - "debug": "^2.2.0", - "heimdalljs": "^0.2.6" - } - }, - "node_modules/heimdalljs-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/heimdalljs-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/heimdalljs/node_modules/rsvp": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", - "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", - "dev": true - }, "node_modules/hermes-estree": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.15.0.tgz", @@ -20437,9 +19906,9 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "node_modules/i18next": { - "version": "23.7.13", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.13.tgz", - "integrity": "sha512-DbCPlw6VmURSZa43iOnycxq9o15e+WuBWDBZ3aj+gQZcDz4sgnuKwrcwmP1n8gSSCwCN7CRFGTpnwTd93A16Mg==", + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", + "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", "funding": [ { "type": "individual", @@ -20455,7 +19924,7 @@ } ], "dependencies": { - "@babel/runtime": "^7.23.2" + "@babel/runtime": "^7.20.6" } }, "node_modules/i18next-browser-languagedetector": { @@ -20466,98 +19935,6 @@ "@babel/runtime": "^7.23.2" } }, - "node_modules/i18next-parser": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.12.0.tgz", - "integrity": "sha512-DUDFX3/nkECyd/zCqZoudTEtokMhOVX+zN8X8JasvYd/Lc/PsMcYuO2n0R7yzLiohXLj/7Hp20x0vAbuY8wo0Q==", - "dev": true, - "dependencies": { - "broccoli-plugin": "^4.0.7", - "cheerio": "^1.0.0-rc.2", - "colors": "1.4.0", - "commander": "~11.1.0", - "eol": "^0.9.1", - "esbuild": "^0.19.0", - "fs-extra": "^11.1.0", - "gulp-sort": "^2.0.0", - "i18next": "^23.5.1", - "js-yaml": "4.1.0", - "lilconfig": "^3.0.0", - "rsvp": "^4.8.2", - "sort-keys": "^5.0.0", - "typescript": "^5.0.4", - "vinyl": "~3.0.0", - "vinyl-fs": "^4.0.0", - "vue-template-compiler": "^2.6.11" - }, - "bin": { - "i18next": "bin/cli.js" - }, - "engines": { - "node": ">=16.0.0 || >=18.0.0 || >=20.0.0", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/i18next-parser/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/i18next-parser/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/i18next-parser/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/i18next-parser/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/i18next-parser/node_modules/sort-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", - "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", - "dev": true, - "dependencies": { - "is-plain-obj": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -21023,15 +20400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -21214,15 +20582,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -23739,15 +23098,6 @@ "shell-quote": "^1.8.1" } }, - "node_modules/lead": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", - "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", @@ -24330,25 +23680,6 @@ "tmpl": "1.0.5" } }, - "node_modules/matcher-collection": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", - "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", - "dev": true, - "dependencies": { - "@types/minimatch": "^3.0.3", - "minimatch": "^3.0.2" - }, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/matcher-collection/node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -25425,15 +24756,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mktemp": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", - "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", - "dev": true, - "engines": { - "node": ">0.9" - } - }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -25820,18 +25142,6 @@ "node": ">=0.10.0" } }, - "node_modules/now-and-later": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", - "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/npm-package-arg": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", @@ -26726,46 +26036,6 @@ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", "dev": true }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "dev": true, - "dependencies": { - "domhandler": "^5.0.2", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -26868,12 +26138,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-posix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "dev": true - }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -27904,15 +27168,6 @@ "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", "dev": true }, - "node_modules/promise-map-series": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", - "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", - "dev": true, - "engines": { - "node": "10.* || >= 12.*" - } - }, "node_modules/promise-polyfill": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", @@ -28132,12 +27387,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -28150,17 +27399,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/quick-temp": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", - "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", - "dev": true, - "dependencies": { - "mktemp": "~0.4.0", - "rimraf": "^2.5.4", - "underscore.string": "~3.3.4" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -30150,27 +29388,12 @@ "jsesc": "bin/jsesc" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, "node_modules/remove-trailing-slash": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz", "integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==", "dev": true }, - "node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -30247,18 +29470,6 @@ "node": ">=8" } }, - "node_modules/resolve-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", - "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", - "dev": true, - "dependencies": { - "value-or-function": "^4.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/resolve.exports": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", @@ -30311,27 +29522,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true, - "engines": { - "node": "6.* || >= 7.*" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -31316,15 +30506,6 @@ "dev": true, "peer": true }, - "node_modules/stream-composer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", - "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", - "dev": true, - "dependencies": { - "streamx": "^2.13.2" - } - }, "node_modules/stream-json": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.8.0.tgz", @@ -31343,16 +30524,6 @@ "node": ">=10.0.0" } }, - "node_modules/streamx": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.2.tgz", - "integrity": "sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==", - "dev": true, - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -31799,12 +30970,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/symlink-or-copy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", - "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", - "dev": true - }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", @@ -31904,15 +31069,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/telnet-client": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-1.2.8.tgz", @@ -32248,18 +31404,6 @@ "node": ">=8.0" } }, - "node_modules/to-through": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -32909,25 +32053,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/underscore.string/node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, "node_modules/undici": { "version": "5.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz", @@ -33200,15 +32325,6 @@ "builtins": "^1.0.3" } }, - "node_modules/value-or-function": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", - "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", - "dev": true, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -33326,120 +32442,6 @@ "node": ">=12" } }, - "node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-contents": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", - "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", - "dev": true, - "dependencies": { - "bl": "^5.0.0", - "vinyl": "^3.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-contents/node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dev": true, - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/vinyl-contents/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", - "dev": true, - "dependencies": { - "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", - "graceful-fs": "^4.2.11", - "iconv-lite": "^0.6.3", - "is-valid-glob": "^1.0.0", - "lead": "^4.0.0", - "normalize-path": "3.0.0", - "resolve-options": "^2.0.0", - "stream-composer": "^1.0.2", - "streamx": "^2.14.0", - "to-through": "^3.0.0", - "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", - "vinyl-sourcemap": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-fs/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vinyl-sourcemap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", - "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", - "dev": true, - "dependencies": { - "convert-source-map": "^2.0.0", - "graceful-fs": "^4.2.10", - "now-and-later": "^3.0.0", - "streamx": "^2.12.5", - "vinyl": "^3.0.0", - "vinyl-contents": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -33458,6 +32460,8 @@ "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" @@ -33475,27 +32479,6 @@ "node": ">=14" } }, - "node_modules/walk-sync": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", - "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", - "dev": true, - "dependencies": { - "@types/minimatch": "^3.0.3", - "ensure-posix-path": "^1.1.0", - "matcher-collection": "^2.0.0", - "minimatch": "^3.0.4" - }, - "engines": { - "node": "8.* || >= 10.*" - } - }, - "node_modules/walk-sync/node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 94d795d4..1aebf6ee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,7 @@ "core-js": "^3.35.0", "deepmerge": "^4.3.1", "formik": "^2.4.5", - "i18next": "^23.7.13", + "i18next": "^22.5.1 ", "i18next-browser-languagedetector": "^7.2.0", "localforage": "^1.10.0", "luxon": "^3.4.4", @@ -119,7 +119,6 @@ "eslint-plugin-prettier": "^5.1.2", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", - "i18next-parser": "^8.12.0", "jest": "29.7.0", "jest-circus": "29.7.0", "jest-environment-jsdom": "29.7.0",