@@ -137,18 +148,22 @@ export function AccountSyncCheck() {
{showAuth ? (
<>
-
+
>
) : (
-
+
)}
diff --git a/packages/desktop-client/src/components/accounts/Balance.jsx b/packages/desktop-client/src/components/accounts/Balance.jsx
index 91a50086626..3422add9161 100644
--- a/packages/desktop-client/src/components/accounts/Balance.jsx
+++ b/packages/desktop-client/src/components/accounts/Balance.jsx
@@ -1,4 +1,5 @@
import React, { useRef } from 'react';
+import { useTranslation } from 'react-i18next';
import { useHover } from 'usehooks-ts';
@@ -42,6 +43,8 @@ function DetailedBalance({ name, balance, isExactBalance = true }) {
}
function SelectedBalance({ selectedItems, account }) {
+ const { t } = useTranslation();
+
const name = `selected-balance-${[...selectedItems].join('-')}`;
const rows = useSheetValue({
@@ -99,7 +102,7 @@ function SelectedBalance({ selectedItems, account }) {
return (
@@ -107,9 +110,11 @@ function SelectedBalance({ selectedItems, account }) {
}
function FilteredBalance({ filteredAmount }) {
+ const { t } = useTranslation();
+
return (
@@ -117,6 +122,8 @@ function FilteredBalance({ filteredAmount }) {
}
function MoreBalances({ balanceQuery }) {
+ const { t } = useTranslation();
+
const cleared = useSheetValue({
name: balanceQuery.name + '-cleared',
query: balanceQuery.query.filter({ cleared: true }),
@@ -128,8 +135,8 @@ function MoreBalances({ balanceQuery }) {
return (
-
-
+
+
);
}
diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx
index 91204e6b3bd..f8aa6b492d8 100644
--- a/packages/desktop-client/src/components/accounts/Header.jsx
+++ b/packages/desktop-client/src/components/accounts/Header.jsx
@@ -1,5 +1,6 @@
import React, { useState, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
+import { Trans, useTranslation } from 'react-i18next';
import { useLocalPref } from '../../hooks/useLocalPref';
import { useSplitsExpanded } from '../../hooks/useSplitsExpanded';
@@ -88,6 +89,8 @@ export function AccountHeader({
onMakeAsSplitTransaction,
onMakeAsNonSplitTransactions,
}) {
+ const { t } = useTranslation();
+
const [menuOpen, setMenuOpen] = useState(false);
const searchInput = useRef(null);
const triggerRef = useRef(null);
@@ -231,7 +234,7 @@ export function AccountHeader({
data-testid="account-name"
>
{account && account.closed
- ? 'Closed: ' + accountName
+ ? t('Closed: {{ accountName }}', { accountName })
: accountName}
@@ -243,7 +246,7 @@ export function AccountHeader({
)}
)}
{!showEmptyMessage && (
)}
@@ -327,7 +330,7 @@ export function AccountHeader({
0}
style={{ padding: 6, marginLeft: 10 }}
@@ -369,8 +372,8 @@ export function AccountHeader({
{splitsExpanded.state.mode === 'collapse' ? (
@@ -432,9 +435,9 @@ export function AccountHeader({
items={[
isSorted && {
name: 'remove-sorting',
- text: 'Remove all sorting',
+ text: t('Remove all sorting'),
},
- { name: 'export', text: 'Export' },
+ { name: 'export', text: t('Export') },
]}
/>
@@ -480,6 +483,8 @@ function AccountMenu({
onReconcile,
onMenuSelect,
}) {
+ const { t } = useTranslation();
+
const [tooltip, setTooltip] = useState('default');
const syncServerStatus = useSyncServerStatus();
@@ -501,36 +506,42 @@ function AccountMenu({
items={[
isSorted && {
name: 'remove-sorting',
- text: 'Remove all sorting',
+ text: t('Remove all sorting'),
},
canShowBalances && {
name: 'toggle-balance',
- text: (showBalances ? 'Hide' : 'Show') + ' running balance',
+ text: showBalances
+ ? t('Hide running balance')
+ : t('Show running balance'),
},
{
name: 'toggle-cleared',
- text: (showCleared ? 'Hide' : 'Show') + ' “cleared” checkboxes',
+ text: showCleared
+ ? t('Hide “cleared” checkboxes')
+ : t('Show “cleared” checkboxes'),
},
{
name: 'toggle-reconciled',
- text: (showReconciled ? 'Hide' : 'Show') + ' reconciled transactions',
+ text: showReconciled
+ ? t('Hide reconciled transactions')
+ : t('Show reconciled transactions'),
},
- { name: 'export', text: 'Export' },
- { name: 'reconcile', text: 'Reconcile' },
+ { name: 'export', text: t('Export') },
+ { name: 'reconcile', text: t('Reconcile') },
account &&
!account.closed &&
(canSync
? {
name: 'unlink',
- text: 'Unlink account',
+ text: t('Unlink account'),
}
: syncServerStatus === 'online' && {
name: 'link',
- text: 'Link account',
+ text: t('Link account'),
}),
account.closed
- ? { name: 'reopen', text: 'Reopen account' }
- : { name: 'close', text: 'Close account' },
+ ? { name: 'reopen', text: t('Reopen account') }
+ : { name: 'close', text: t('Close account') },
].filter(x => x)}
/>
);
diff --git a/packages/desktop-client/src/components/accounts/Reconcile.jsx b/packages/desktop-client/src/components/accounts/Reconcile.jsx
index ab7127858ef..0a4ac13b16c 100644
--- a/packages/desktop-client/src/components/accounts/Reconcile.jsx
+++ b/packages/desktop-client/src/components/accounts/Reconcile.jsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { Trans } from 'react-i18next';
import * as queries from 'loot-core/src/client/queries';
import { currencyToInteger } from 'loot-core/src/shared/util';
@@ -59,33 +60,42 @@ export function ReconcilingMessage({
marginRight: 3,
}}
/>
- All reconciled!
+ All reconciled!
) : (
- Your cleared balance{' '}
- {format(cleared, 'financial')} needs{' '}
-
- {(targetDiff > 0 ? '+' : '') + format(targetDiff, 'financial')}
- {' '}
- to match
-
your bank’s balance of{' '}
-
- {format(targetBalance, 'financial')}
-
+
+ Your cleared balance{' '}
+
+ {{ clearedBalance: format(cleared, 'financial') }}
+ {' '}
+ needs{' '}
+
+ {{
+ difference:
+ (targetDiff > 0 ? '+' : '') +
+ format(targetDiff, 'financial'),
+ }}
+ {' '}
+ to match
+
your bank's balance of{' '}
+
+ {{ bankBalance: format(targetBalance, 'financial') }}
+
+
)}
{targetDiff !== 0 && (
)}
@@ -121,8 +131,10 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
return (
- Enter the current balance of your bank account that you want to
- reconcile with:
+
+ Enter the current balance of your bank account that you want to
+ reconcile with:
+
{clearedBalance != null && (
@@ -136,7 +148,7 @@ export function ReconcileMenu({ account, onReconcile, onClose }) {
)}
);
diff --git a/upcoming-release-notes/3277.md b/upcoming-release-notes/3277.md
new file mode 100644
index 00000000000..201f78a71f7
--- /dev/null
+++ b/upcoming-release-notes/3277.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [matt-fidd]
+---
+
+Support translations in desktop-client/components/accounts.
From 3f8963273be0c737da78ea47e4bcf7adabd9ca5f Mon Sep 17 00:00:00 2001
From: Robert Dyer
Date: Tue, 3 Sep 2024 13:23:15 -0500
Subject: [PATCH 4/7] Translation: desktop-client/components/schedules (#3313)
* Translation: desktop-client/components/schedules
* add release note
* better handling of plural
* clean plural use
* add review suggestions
* more review suggestions
* change to t() syntax
* use basic list format
* eslint no longer needs disabled
* Fix interpolation of payees list.
Co-authored-by: Julian Dominguez-Schatz
* fix linter
* fix typecheck
* tighten the types
* move to hook
* memoize the hook
---------
Co-authored-by: Julian Dominguez-Schatz
---
.../schedules/DiscoverSchedules.tsx | 39 ++++++---
.../schedules/PostsOfflineNotification.jsx | 77 +++++++++--------
.../components/schedules/ScheduleDetails.jsx | 84 +++++++++++--------
.../src/components/schedules/ScheduleLink.tsx | 18 ++--
.../components/schedules/SchedulesTable.tsx | 67 ++++++++++-----
.../src/components/schedules/index.tsx | 13 ++-
.../desktop-client/src/hooks/useFormatList.ts | 32 +++++++
upcoming-release-notes/3313.md | 6 ++
8 files changed, 223 insertions(+), 113 deletions(-)
create mode 100644 packages/desktop-client/src/hooks/useFormatList.ts
create mode 100644 upcoming-release-notes/3313.md
diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
index 31d7fc10a24..dd7aee0011b 100644
--- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
+++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import React, { useState } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
import { runQuery } from 'loot-core/src/client/query-helpers';
import { send } from 'loot-core/src/platform/client/fetch';
@@ -35,6 +36,8 @@ function DiscoverSchedulesTable({
schedules: DiscoverScheduleEntity[];
loading: boolean;
}) {
+ const { t } = useTranslation();
+
const selectedItems = useSelectedItems();
const dispatchSelected = useSelectedDispatch();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
@@ -107,13 +110,17 @@ function DiscoverSchedulesTable({
dispatchSelected({ type: 'select-all', isRangeSelect: e.shiftKey })
}
/>
- Payee
- Account
+
+ Payee
+
+
+ Account
+
- When
+ When
- Amount
+ Amount
selectedItems.has(String(id))}
renderItem={renderItem}
- renderEmpty="No schedules found"
+ renderEmpty={t('No schedules found')}
/>
);
}
export function DiscoverSchedules() {
+ const { t } = useTranslation();
+
const { data, isLoading } = useSendPlatformRequest('schedule/discover');
const schedules = data || [];
@@ -185,18 +194,22 @@ export function DiscoverSchedules() {
{({ state: { close } }) => (
<>
}
/>
- We found some possible schedules in your current transactions.
- Select the ones you want to create.
+
+ We found some possible schedules in your current transactions.
+ Select the ones you want to create.
+
- If you expected a schedule here and don’t see it, it might be
- because the payees of the transactions don’t match. Make sure you
- rename payees on all transactions for a schedule to be the same
- payee.
+
+ If you expected a schedule here and don’t see it, it might be
+ because the payees of the transactions don’t match. Make sure you
+ rename payees on all transactions for a schedule to be the same
+ payee.
+
@@ -221,7 +234,7 @@ export function DiscoverSchedules() {
close();
}}
>
- Create schedules
+ Create schedules
>
diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
index 29ad0f5da38..a1ba84dd3a4 100644
--- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
+++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
@@ -1,10 +1,12 @@
import React from 'react';
+import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { popModal } from 'loot-core/client/actions';
import { send } from 'loot-core/src/platform/client/fetch';
+import { useFormatList } from '../../hooks/useFormatList';
import { theme } from '../../style';
import { Button } from '../common/Button2';
import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
@@ -14,63 +16,66 @@ import { Text } from '../common/Text';
import { DisplayId } from '../util/DisplayId';
export function PostsOfflineNotification() {
+ const { t } = useTranslation();
+
const location = useLocation();
const dispatch = useDispatch();
const payees = (location.state && location.state.payees) || [];
- const plural = payees.length > 1;
async function onPost() {
await send('schedule/force-run-service');
dispatch(popModal());
}
+ const payeesList = payees.map(id => (
+
+
+
+ ));
+ const payeeNamesList = useFormatList(payeesList, t.language);
+
return (
{({ state: { close } }) => (
<>
}
/>
- {payees.length > 0 ? (
-
- The {plural ? 'payees ' : 'payee '}
- {payees.map((id, idx) => (
-
-
-
-
- {idx === payees.length - 1
- ? ' '
- : idx === payees.length - 2
- ? ', and '
- : ', '}
-
- ))}
-
- ) : (
- There {plural ? 'are payees ' : 'is a payee '} that
- )}
-
- {plural ? 'have ' : 'has '} schedules that are due today. Usually
- we automatically post transactions for these, but you are offline
- or syncing failed. In order to avoid duplicate transactions, we
- let you choose whether or not to create transactions for these
- schedules.
+ {payees.length > 0 ? (
+
+ The payees {payeeNamesList} have schedules that
+ are due today.
+
+ ) : (
+ t('There are payees that have schedules that are due today.', {
+ count: payees.length,
+ })
+ )}{' '}
+
+ Usually we automatically post transactions for these, but you
+ are offline or syncing failed. In order to avoid duplicate
+ transactions, we let you choose whether or not to create
+ transactions for these schedules.
+
- Be aware that other devices may have already created these
- transactions. If you have multiple devices, make sure you only do
- this on one device or you will have duplicate transactions.
+
+ Be aware that other devices may have already created these
+ transactions. If you have multiple devices, make sure you only do
+ this on one device or you will have duplicate transactions.
+
- You can always manually post a transaction later for a due schedule
- by selecting the schedule and clicking “Post transaction” in the
- action menu.
+
+ You can always manually post a transaction later for a due
+ schedule by selecting the schedule and clicking “Post transaction”
+ in the action menu.
+
-
+
>
diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
index 67900bf27b9..f9419ffdf8d 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
@@ -1,4 +1,5 @@
import React, { useEffect, useReducer } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { t } from 'i18next';
@@ -49,11 +50,11 @@ function updateScheduleConditions(schedule, fields) {
// Validate
if (fields.date == null) {
- return { error: 'Date is required' };
+ return { error: t('Date is required') };
}
if (fields.amount == null) {
- return { error: 'A valid amount is required' };
+ return { error: t('A valid amount is required') };
}
return {
@@ -73,6 +74,8 @@ function updateScheduleConditions(schedule, fields) {
}
export function ScheduleDetails({ id, transaction }) {
+ const { t } = useTranslation();
+
const adding = id == null;
const fromTrans = transaction != null;
const payees = getPayeesById(usePayees());
@@ -365,7 +368,7 @@ export function ScheduleDetails({ id, transaction }) {
if (sameName.length > 0 && sameName[0].id !== state.schedule.id) {
dispatch({
type: 'form-error',
- error: 'There is already a schedule with this name',
+ error: t('There is already a schedule with this name'),
});
return;
}
@@ -396,8 +399,9 @@ export function ScheduleDetails({ id, transaction }) {
if (res.error) {
dispatch({
type: 'form-error',
- error:
+ error: t(
'An error occurred while saving. Please visit https://actualbudget.org/contact/ for support.',
+ ),
});
return;
}
@@ -457,12 +461,16 @@ export function ScheduleDetails({ id, transaction }) {
{({ state: { close } }) => (
<>
}
/>
-
+
-
+
dispatch({ type: 'set-field', field: 'payee', value: id })
}
@@ -491,7 +503,7 @@ export function ScheduleDetails({ id, transaction }) {
@@ -499,7 +511,7 @@ export function ScheduleDetails({ id, transaction }) {
includeClosedAccounts={false}
value={state.fields.account}
labelProps={{ id: 'account-label' }}
- inputProps={{ id: 'account-field', placeholder: '(none)' }}
+ inputProps={{ id: 'account-field', placeholder: t('(none)') }}
onSelect={id =>
dispatch({ type: 'set-field', field: 'account', value: id })
}
@@ -509,7 +521,7 @@ export function ScheduleDetails({ id, transaction }) {
@@ -519,11 +531,11 @@ export function ScheduleDetails({ id, transaction }) {
formatOp={op => {
switch (op) {
case 'is':
- return 'is exactly';
+ return t('is exactly');
case 'isapprox':
- return 'is approximately';
+ return t('is approximately');
case 'isbetween':
- return 'is between';
+ return t('is between');
default:
throw new Error('Invalid op for select: ' + op);
}
@@ -570,7 +582,7 @@ export function ScheduleDetails({ id, transaction }) {
-
+
@@ -595,7 +607,7 @@ export function ScheduleDetails({ id, transaction }) {
{state.upcomingDates && (
- Upcoming dates
+ Upcoming dates
@@ -657,7 +669,7 @@ export function ScheduleDetails({ id, transaction }) {
htmlFor="form_posts_transaction"
style={{ userSelect: 'none' }}
>
- Automatically add transaction
+ Automatically add transaction
@@ -671,8 +683,10 @@ export function ScheduleDetails({ id, transaction }) {
lineHeight: '1.4em',
}}
>
- If checked, the schedule will automatically create transactions
- for you in the specified account
+
+ If checked, the schedule will automatically create
+ transactions for you in the specified account
+
{!adding && state.schedule.rule && (
@@ -686,11 +700,13 @@ export function ScheduleDetails({ id, transaction }) {
width: 350,
}}
>
- This schedule has custom conditions and actions
+
+ This schedule has custom conditions and actions
+
)}
)}
@@ -702,11 +718,11 @@ export function ScheduleDetails({ id, transaction }) {
{adding ? (
- These transactions match this schedule:
+ These transactions match this schedule:
- Select transactions to link on save
+ Select transactions to link on save
) : (
@@ -723,7 +739,7 @@ export function ScheduleDetails({ id, transaction }) {
}}
onPress={() => onSwitchTransactions('linked')}
>
- Linked transactions
+ Linked transactions
{' '}
t('{{count}} transactions', { count })}
items={
state.transactionsMode === 'linked'
- ? [{ name: 'unlink', text: 'Unlink from schedule' }]
- : [{ name: 'link', text: 'Link to schedule' }]
+ ? [{ name: 'unlink', text: t('Unlink from schedule') }]
+ : [{ name: 'link', text: t('Link to schedule') }]
}
onSelect={(name, ids) => {
switch (name) {
@@ -792,7 +808,7 @@ export function ScheduleDetails({ id, transaction }) {
{state.error}
)}
>
@@ -810,6 +826,8 @@ export function ScheduleDetails({ id, transaction }) {
}
function NoTransactionsMessage(props) {
+ const { t } = useTranslation();
+
return (
{props.error ? (
- Could not search: {props.error}
+ Could not search: {props.error}
) : props.transactionsMode === 'matched' ? (
- 'No matching transactions'
+ t('No matching transactions')
) : (
- 'No linked transactions'
+ t('No linked transactions')
)}
);
diff --git a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
index 15b3409a75f..7b3a3e40677 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import React, { useCallback, useRef, useState } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { pushModal } from 'loot-core/client/actions';
@@ -32,6 +33,8 @@ export function ScheduleLink({
accountName?: string;
onScheduleLinked?: (schedule: ScheduleEntity) => void;
}) {
+ const { t } = useTranslation();
+
const dispatch = useDispatch();
const [filter, setFilter] = useState(accountName || '');
@@ -76,7 +79,7 @@ export function ScheduleLink({
{({ state: { close } }) => (
<>
}
/>
- Choose the schedule{' '}
- {ids?.length > 1
- ? `these ${ids.length} transactions belong`
- : `this transaction belongs`}{' '}
- to:
+ {t(
+ 'Choose the schedule these {{ count }} transactions belong to:',
+ { count: ids?.length ?? 0 },
+ )}
@@ -114,7 +116,7 @@ export function ScheduleLink({
}}
>
- Create New
+ Create New
)}
diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
index 66bc8fa8269..25d97f9e398 100644
--- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
+++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx
@@ -1,5 +1,6 @@
// @ts-strict-ignore
import React, { useRef, useState, useMemo, type CSSProperties } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
import {
type ScheduleStatusType,
@@ -61,6 +62,8 @@ function OverflowMenu({
status: ScheduleStatusType;
onAction: SchedulesTableProps['onAction'];
}) {
+ const { t } = useTranslation();
+
const triggerRef = useRef(null);
const [open, setOpen] = useState(false);
@@ -69,28 +72,28 @@ function OverflowMenu({
menuItems.push({
name: 'post-transaction',
- text: 'Post transaction',
+ text: t('Post transaction'),
});
if (status === 'completed') {
menuItems.push({
name: 'restart',
- text: 'Restart',
+ text: t('Restart'),
});
} else {
menuItems.push(
{
name: 'skip',
- text: 'Skip next date',
+ text: t('Skip next date'),
},
{
name: 'complete',
- text: 'Complete',
+ text: t('Complete'),
},
);
}
- menuItems.push({ name: 'delete', text: 'Delete' });
+ menuItems.push({ name: 'delete', text: t('Delete') });
return menuItems;
};
@@ -100,7 +103,7 @@ function OverflowMenu({