diff --git a/packages/desktop-client/.gitignore b/packages/desktop-client/.gitignore index e30abb2..09eab09 100644 --- a/packages/desktop-client/.gitignore +++ b/packages/desktop-client/.gitignore @@ -16,3 +16,6 @@ npm-debug.log *kcab.* public/kcab + +# Ignore auto generated dictionaries with check-i18n +src/locales/*_old.json diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index 0719e71..d4dd3a8 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -151,6 +151,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) { - {'account.clearedBalance'} - + /> )} @@ -176,6 +175,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) { function ReconcileTooltip({ account, onReconcile, onClose }) { let balance = useSheetValue(queries.accountBalance(account)); + const { t } = useTranslation(); function onSubmit(e) { let input = e.target.elements[0]; @@ -187,10 +187,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) { return ( - - Enter the current balance of your bank account that you want to - reconcile with: - + {t('account.enterCurrentBalanceToReconcileAdvice')}
{balance != null && ( @@ -200,7 +197,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) { /> )} - +
@@ -243,6 +240,7 @@ function AccountMenu({ onMenuSelect }) { let [tooltip, setTooltip] = useState('default'); + const { t } = useTranslation(); return tooltip === 'reconcile' ? ( x)} /> @@ -283,19 +283,30 @@ function AccountMenu({ } function CategoryMenu({ onClose, onMenuSelect }) { + const { t } = useTranslation(); + return ( { onMenuSelect(item); }} - items={[{ name: 'export', text: 'Export' }]} + items={[{ name: 'export', text: t('general.export') }]} /> ); } function DetailedBalance({ name, balance }) { + const balanceType = { + // t('account.selectedBalance') + selected: 'account.selectedBalance', + // t('account.clearedTotal') + cleared: 'account.clearedTotal', + // t('account.unclearedTotal') + uncleared: 'account.unclearedTotal' + }; + return ( - {name}{' '} - {format(balance, 'financial')} + ); } @@ -342,7 +357,8 @@ function SelectedBalance({ selectedItems }) { if (balance == null) { return null; } - return ; + + return ; } function MoreBalances({ balanceQuery }) { @@ -357,8 +373,8 @@ function MoreBalances({ balanceQuery }) { return ( - - + + ); } @@ -458,6 +474,7 @@ function SelectedTransactionsButton({ }) { let selectedItems = useSelectedItems(); let history = useHistory(); + const { t } = useTranslation(); let types = useMemo(() => { let items = [...selectedItems]; @@ -498,37 +515,43 @@ function SelectedTransactionsButton({ items={[ ...(!types.trans ? [ - { name: 'view-schedule', text: 'View schedule' }, - { name: 'post-transaction', text: 'Post transaction' }, - { name: 'skip', text: 'Skip scheduled date' } + { name: 'view-schedule', text: t('schedules.view_one') }, + { + name: 'post-transaction', + text: t('schedules.postTransaction') + }, + { name: 'skip', text: t('schedules.skipScheduledDate') } ] : [ - { name: 'show', text: 'Show', key: 'F' }, - { name: 'delete', text: 'Delete', key: 'D' }, + { name: 'show', text: t('general.show'), key: 'F' }, + { name: 'delete', text: t('general.delete'), key: 'D' }, ...(linked ? [ { name: 'view-schedule', - text: 'View schedule', + text: t('schedules.view_one'), disabled: selectedItems.size > 1 }, - { name: 'unlink-schedule', text: 'Unlink schedule' } + { + name: 'unlink-schedule', + text: t('schedules.unlinkSchedule') + } ] : [ { name: 'link-schedule', - text: 'Link schedule' + text: t('schedules.linkSchedule') } ]), Menu.line, - { type: Menu.label, name: 'Edit field' }, - { name: 'date', text: 'Date' }, - { name: 'account', text: 'Account', key: 'A' }, - { name: 'payee', text: 'Payee', key: 'P' }, - { name: 'notes', text: 'Notes', key: 'N' }, - { name: 'category', text: 'Category', key: 'C' }, - { name: 'amount', text: 'Amount' }, - { name: 'cleared', text: 'Cleared', key: 'L' } + { type: Menu.label, name: t('general.editField') }, + { name: 'date', text: t('general.date') }, + { name: 'account', text: t('general.account_one'), key: 'A' }, + { name: 'payee', text: t('general.payee_one'), key: 'P' }, + { name: 'notes', text: t('general.note_other'), key: 'N' }, + { name: 'category', text: t('general.category_one'), key: 'C' }, + { name: 'amount', text: t('general.amount') }, + { name: 'cleared', text: t('account.cleared'), key: 'L' } ]) ]} onSelect={name => { @@ -620,6 +643,7 @@ const AccountHeader = React.memo( let [menuOpen, setMenuOpen] = useState(false); let searchInput = useRef(null); let splitsExpanded = useSplitsExpanded(); + const { t } = useTranslation(); let canSync = syncEnabled && account && account.account_id; if (!account) { @@ -732,7 +756,7 @@ const AccountHeader = React.memo( } style={{ color: 'currentColor', marginRight: 4 }} />{' '} - Sync + {t('general.sync')} ) : ( <> @@ -741,7 +765,7 @@ const AccountHeader = React.memo( height={13} style={{ color: 'currentColor', marginRight: 4 }} />{' '} - Import + {t('general.import')} )} @@ -753,7 +777,7 @@ const AccountHeader = React.memo( height={10} style={{ color: 'inherit', marginRight: 3 }} />{' '} - Add New + {t('general.addNew')} )} @@ -774,7 +798,7 @@ const AccountHeader = React.memo( } inputRef={searchInput} value={search} - placeholder="Search" + placeholder={t('general.search')} getStyle={focused => [ { backgroundColor: 'transparent', @@ -812,8 +836,8 @@ const AccountHeader = React.memo( onClick={onToggleSplits} title={ splitsExpanded.state.mode === 'collapse' - ? 'Collapse split transactions' - : 'Expand split transactions' + ? t('account.collapseSplitTransaction_other') + : t('account.expandSplitTransaction_other') } > {splitsExpanded.state.mode === 'collapse' ? ( @@ -1191,11 +1215,15 @@ class AccountInternal extends React.PureComponent { onImport = async () => { const accountId = this.props.accountId; const account = this.props.accounts.find(acct => acct.id === accountId); + const t = this.props.t; if (account) { const res = await window.Actual.openFileDialog({ filters: [ - { name: 'Financial Files', extensions: ['qif', 'ofx', 'qfx', 'csv'] } + { + name: t('general.financialFile_other'), + extensions: ['qif', 'ofx', 'qfx', 'csv'] + } ] }); @@ -1220,11 +1248,12 @@ class AccountInternal extends React.PureComponent { let normalizedName = accountName && accountName.replace(/[()]/g, '').replace(/\s+/g, '-'); let filename = `${normalizedName || 'transactions'}.csv`; + const t = this.props.t; window.Actual.saveFile( exportedTransactions, filename, - 'Export Transactions' + t('general.exportTransaction_other') ); }; @@ -1338,21 +1367,24 @@ class AccountInternal extends React.PureComponent { if (filterName) { return filterName; } + const t = this.props.t; if (!account) { if (id === 'budgeted') { - return 'Budgeted Accounts'; + return t('account.budgetedAccount_other'); } else if (id === 'offbudget') { - return 'Off Budget Accounts'; + return t('account.offBudgetAccount_other'); } else if (id === 'uncategorized') { - return 'Uncategorized'; + return t('account.uncategorized'); } else if (!id) { - return 'All Accounts'; + return t('account.allAccounts'); } return null; } - return (account.closed ? 'Closed: ' : '') + account.name; + return account.closed + ? t('account.closedNamed', { name: account.name }) + : account.name; } getBalanceQuery(account, id) { @@ -1391,8 +1423,10 @@ class AccountInternal extends React.PureComponent { }; onShowTransactions = async ids => { + const t = this.props.t; + this.onApplyFilter({ - customName: 'Selected transactions', + customName: t('account.selectedTransaction_other'), filter: { id: { $oneof: ids } } }); }; @@ -1574,7 +1608,8 @@ class AccountInternal extends React.PureComponent { replaceModal, showExtraBalances, expandSplits, - accountId + accountId, + t } = this.props; let { transactions, @@ -1709,7 +1744,7 @@ class AccountInternal extends React.PureComponent { fontStyle: 'italic' }} > - No transactions + {t('general.noTransaction_other')} ) : null } @@ -1739,10 +1774,13 @@ class AccountInternal extends React.PureComponent { function AccountHack(props) { let { dispatch: splitsExpandedDispatch } = useSplitsExpanded(); + const { t } = useTranslation(); + return ( ); } diff --git a/packages/desktop-client/src/components/accounts/Filters.js b/packages/desktop-client/src/components/accounts/Filters.js index d366dd6..ed0c635 100644 --- a/packages/desktop-client/src/components/accounts/Filters.js +++ b/packages/desktop-client/src/components/accounts/Filters.js @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect, useReducer } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -132,6 +133,8 @@ function ConfigureField({ field, op, value, dispatch, onApply }) { ops = ['is']; } + const { t } = useTranslation(); + return ( - Apply + {t('general.apply')} @@ -251,6 +254,8 @@ export function FilterButton({ onApply }) { }; }); + const { t } = useTranslation(); + let [state, dispatch] = useReducer( (state, action) => { switch (action.type) { @@ -306,7 +311,7 @@ export function FilterButton({ onApply }) { if (isDateValid(date)) { cond.value = formatDate(date, 'yyyy-MM'); } else { - alert('Invalid date format'); + alert(t('general.invalidDateFormat')); return; } } else if (cond.options.year) { @@ -314,7 +319,7 @@ export function FilterButton({ onApply }) { if (isDateValid(date)) { cond.value = formatDate(date, 'yyyy'); } else { - alert('Invalid date format'); + alert(t('general.invalidDateFormat')); return; } } diff --git a/packages/desktop-client/src/components/accounts/SimpleTransactionsTable.js b/packages/desktop-client/src/components/accounts/SimpleTransactionsTable.js index 819b3ca..e51402c 100644 --- a/packages/desktop-client/src/components/accounts/SimpleTransactionsTable.js +++ b/packages/desktop-client/src/components/accounts/SimpleTransactionsTable.js @@ -1,4 +1,5 @@ import React, { useMemo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -181,6 +182,7 @@ export default function SimpleTransactionsTable({ }, [payees, categories, memoFields, selectedItems] ); + const { t } = useTranslation(); return ( - Date + {t('general.date')} ); case 'imported_payee': return ( - Imported payee + {t('general.importedPayee')} ); case 'payee': return ( - Payee + {t('general.payee_one')} ); case 'category': return ( - Category + {t('general.category_one')} ); case 'account': return ( - Account + {t('general.account_one')} ); case 'notes': return ( - Notes + {t('general.note_other')} ); case 'amount': return ( - Amount + {t('general.amount')} ); default: diff --git a/packages/desktop-client/src/components/accounts/TransactionsTable.js b/packages/desktop-client/src/components/accounts/TransactionsTable.js index 3613391..38a7f24 100644 --- a/packages/desktop-client/src/components/accounts/TransactionsTable.js +++ b/packages/desktop-client/src/components/accounts/TransactionsTable.js @@ -8,6 +8,7 @@ import React, { useContext, useReducer } from 'react'; +import { useTranslation, Trans } from 'react-i18next'; import { useSelector, useDispatch } from 'react-redux'; import { @@ -253,6 +254,7 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { export const TransactionHeader = React.memo( ({ hasSelected, showAccount, showCategory, showBalance }) => { let dispatchSelected = useSelectedDispatch(); + const { t } = useTranslation(); return ( dispatchSelected({ type: 'select-all' })} /> - - {showAccount && } - - - {showCategory && } - - - {showBalance && } + + {showAccount && } + + + {showCategory && ( + + )} + + + {showBalance && ( + + )} @@ -528,6 +534,7 @@ export const Transaction = React.memo(function Transaction(props) { onCreatePayee, onToggleSplit } = props; + const { t } = useTranslation(); let dispatchSelected = useSelectedDispatch(); @@ -896,7 +903,7 @@ export const Transaction = React.memo(function Transaction(props) { /> )} - Split + {t('general.split')} @@ -910,11 +917,11 @@ export const Transaction = React.memo(function Transaction(props) { onExpose={!isPreview && (name => onEdit(id, name))} value={ isParent - ? 'Split' + ? t('general.split') : isOffBudget - ? 'Off Budget' + ? t('general.offBudget') : isBudgetTransfer - ? 'Transfer' + ? t('general.transfer') : '' } valueStyle={valueStyle} @@ -936,7 +943,7 @@ export const Transaction = React.memo(function Transaction(props) { 'name' ) : transaction.id - ? 'Categorize' + ? t('general.categorize') : '' } exposed={focusedField === 'category'} @@ -1049,6 +1056,8 @@ export const Transaction = React.memo(function Transaction(props) { }); export function TransactionError({ error, isDeposit, onAddSplit, style }) { + const { t } = useTranslation(); + switch (error.type) { case 'SplitTransactionError': if (error.version === 1) { @@ -1064,21 +1073,21 @@ export function TransactionError({ error, isDeposit, onAddSplit, style }) { ]} data-testid="transaction-error" > - - Amount left:{' '} - - {integerToCurrency( + - + ) + }} + /> ); @@ -1135,6 +1144,7 @@ function NewTransaction({ }) { const error = transactions[0].error; const isDeposit = transactions[0].amount > 0; + const { t } = useTranslation(); return ( onClose()} data-testid="cancel-button" > - Cancel + {t('general.cancel')} {error ? ( - Add + {t('general.add')} )} @@ -1532,12 +1542,14 @@ export let TransactionTable = React.forwardRef((props, ref) => { setPrevIsAdding(props.isAdding); } + const { t } = useTranslation(); + useEffect(() => { if (shouldAdd.current) { if (newTransactions[0].account == null) { props.addNotification({ type: 'error', - message: 'Account is a required field' + message: t('transaction.accountIsRequired') }); newNavigator.onEdit('temp', 'account'); } else { diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.js b/packages/desktop-client/src/components/budget/MonthCountSelector.js index b944948..e8e4720 100644 --- a/packages/desktop-client/src/components/budget/MonthCountSelector.js +++ b/packages/desktop-client/src/components/budget/MonthCountSelector.js @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useBudgetMonthCount } from 'loot-design/src/components/budget/BudgetMonthCountContext'; import { View } from 'loot-design/src/components/common'; @@ -16,6 +17,7 @@ function Calendar({ color, onClick }) { export function MonthCountSelector({ maxMonths, onChange }) { let { displayMax } = useBudgetMonthCount(); + const { t } = useTranslation(); let style = { width: 15, height: 15, color: colors.n8 }; let activeStyle = { color: colors.n5 }; @@ -50,7 +52,7 @@ export function MonthCountSelector({ maxMonths, onChange }) { transform: 'scale(1.2)' } }} - title="Choose the number of months shown at a time" + title={t('budget.chooseNumberMonths')} > {calendars} diff --git a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.js b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.js index 9c9c956..6ce5d1a 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.js +++ b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.js @@ -24,13 +24,13 @@ export default function Bootstrap() { function getErrorMessage(error) { switch (error) { case 'invalid-password': - return 'Password cannot be empty'; + return t('bootstrap.passwordCannotBeEmpty'); case 'password-match': - return 'Passwords do not match'; + return t('bootstrap.passwordsDoNotMatch'); case 'network-failure': - return 'Unable to contact the server'; + return t('bootstrap.unableToContactTheServer'); default: - return "Whoops, an error occurred on our side! We'll try to get it fixed soon."; + return t('bootstrap.unknownError'); } } diff --git a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.js b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.js index abda7d4..af48730 100644 --- a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.js +++ b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.js @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -14,17 +15,18 @@ export default function ChangePassword() { let history = useHistory(); let [error, setError] = useState(null); let [msg, setMessage] = useState(null); + const { t } = useTranslation(); function getErrorMessage(error) { switch (error) { case 'invalid-password': - return 'Password cannot be empty'; + return t('bootstrap.passwordCannotBeEmpty'); case 'password-match': - return 'Passwords do not match'; + return t('bootstrap.passwordsDoNotMatch'); case 'network-failure': - return 'Unable to contact the server'; + return t('bootstrap.unableToContactTheServer'); default: - return 'Internal server error'; + return t('bootstrap.unknownError'); } } @@ -35,7 +37,7 @@ export default function ChangePassword() { if (error) { setError(error); } else { - setMessage('Password successfully changed'); + setMessage(t('bootstrap.passwordSuccessfullyChanged')); setTimeout(() => { history.push('/'); @@ -46,7 +48,7 @@ export default function ChangePassword() { return ( <> - + <Title text={t('bootstrap.changeServerPassword')} /> <Text style={{ fontSize: 16, @@ -54,8 +56,7 @@ export default function ChangePassword() { lineHeight: 1.4 }} > - This will change the password for this server instance. All existing - sessions will stay logged in. + {t('bootstrap.thisWillChangeThePasswordAdvice')} </Text> {error && ( diff --git a/packages/desktop-client/src/components/modals/EditRule.js b/packages/desktop-client/src/components/modals/EditRule.js index abfdd1e..5e55cee 100644 --- a/packages/desktop-client/src/components/modals/EditRule.js +++ b/packages/desktop-client/src/components/modals/EditRule.js @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -280,7 +281,6 @@ function ActionEditor({ ops, action, editorStyle, onChange, onDelete, onAdd }) { return ( <Editor style={editorStyle} error={error}> {/*<OpSelect ops={ops} value={op} onChange={onChange} />*/} - {op === 'set' ? ( <> <View style={{ padding: '5px 10px', lineHeight: '1em' }}> @@ -325,6 +325,7 @@ function ActionEditor({ ops, action, editorStyle, onChange, onDelete, onAdd }) { function StageInfo() { let [open, setOpen] = useState(); + const { t } = useTranslation(); return ( <View style={{ position: 'relative', marginLeft: 5 }}> @@ -346,9 +347,7 @@ function StageInfo() { lineHeight: 1.5 }} > - The stage of a rule allows you to force a specific order. Pre rules - always run first, and post rules always run last. Within each stage - rules are automatically ordered from least to most specific. + {t('rules.stageOfRuleAdvice')} </Tooltip> )} </View> @@ -554,6 +553,7 @@ export default function EditRule({ let [transactions, setTransactions] = useState([]); let dispatch = useDispatch(); let scrollableEl = useRef(); + const { t } = useTranslation(); useEffect(() => { dispatch(initiallyLoadPayees()); @@ -687,7 +687,7 @@ export default function EditRule({ return ( <Modal - title="Rule" + title={t('general.rule_one')} padding={0} {...modalProps} style={[modalProps.style, { flex: 'inherit', maxWidth: '90%' }]} @@ -713,7 +713,7 @@ export default function EditRule({ }} > <Text style={{ color: colors.n4, marginRight: 15 }}> - Stage of rule: + {t('rules.stageOfRule')} </Text> <Stack direction="row" align="center" spacing={1}> @@ -721,19 +721,19 @@ export default function EditRule({ selected={stage === 'pre'} onSelect={() => onChangeStage('pre')} > - Pre + {t('rules.stages.pre')} </StageButton> <StageButton selected={stage === null} onSelect={() => onChangeStage(null)} > - Default + {t('rules.stages.default')} </StageButton> <StageButton selected={stage === 'post'} onSelect={() => onChangeStage('post')} > - Post + {t('rules.stages.post')} </StageButton> <StageInfo /> @@ -752,7 +752,7 @@ export default function EditRule({ <View style={{ flexShrink: 0 }}> <View style={{ marginBottom: 30 }}> <Text style={{ color: colors.n4, marginBottom: 15 }}> - If all these conditions match: + {t('rules.ifAllTheseConditionsMatch')} </Text> <ConditionsList @@ -764,7 +764,7 @@ export default function EditRule({ </View> <Text style={{ color: colors.n4, marginBottom: 15 }}> - Then apply these actions: + {t('rules.thenApplyTheseActions')} </Text> <View style={{ flex: 1 }}> {actions.length === 0 ? ( @@ -772,7 +772,7 @@ export default function EditRule({ style={{ alignSelf: 'flex-start' }} onClick={addInitialAction} > - Add action + {t('rules.addAction')} </Button> ) : ( <Stack spacing={2}> @@ -807,7 +807,7 @@ export default function EditRule({ }} > <Text style={{ color: colors.n4, marginBottom: 0 }}> - This rule applies to these transactions: + {t('rules.thisRuleAppliesToTheseTransactions')} </Text> <View style={{ flex: 1 }} /> @@ -815,7 +815,7 @@ export default function EditRule({ disabled={selectedInst.items.size === 0} onClick={onApply} > - Apply actions ({selectedInst.items.size}) + {t('rules.applyAction', { size: selectedInst.items.size })} </Button> </View> @@ -830,9 +830,11 @@ export default function EditRule({ justify="flex-end" style={{ marginTop: 20 }} > - <Button onClick={() => modalProps.onClose()}>Cancel</Button> + <Button onClick={() => modalProps.onClose()}> + {t('general.cancel')} + </Button> <Button primary onClick={() => onSave()}> - Save + {t('general.save')} </Button> </Stack> </View> diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js index f9fff59..d769547 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLocation, useHistory } from 'react-router-dom'; import Platform from 'loot-core/src/client/platform'; @@ -35,6 +36,7 @@ let ROW_HEIGHT = 43; function DiscoverSchedulesTable({ schedules, loading }) { let selectedItems = useSelectedItems(); let dispatchSelected = useSelectedDispatch(); + const { t } = useTranslation(); function renderItem({ item }) { let selected = selectedItems.has(item.id); @@ -89,13 +91,13 @@ function DiscoverSchedulesTable({ schedules, loading }) { selected={selectedItems.size > 0} onSelect={() => dispatchSelected({ type: 'select-all' })} /> - <Field width="flex">Payee</Field> - <Field width="flex">Account</Field> + <Field width="flex">{t('general.payee_one')}</Field> + <Field width="flex">{t('general.account_one')}</Field> <Field width="auto" style={{ flex: 1.5 }}> - When + {t('general.when')} </Field> <Field width={100} style={{ textAlign: 'right' }}> - Amount + {t('general.amount')} </Field> </TableHeader> <Table @@ -107,7 +109,7 @@ function DiscoverSchedulesTable({ schedules, loading }) { loading={loading} isSelected={id => selectedItems.has(id)} renderItem={renderItem} - renderEmpty="No schedules found" + renderEmpty={t('schedules.noSchedulesFound')} /> </View> ); @@ -161,24 +163,19 @@ export default function DiscoverSchedules() { setCreating(false); history.goBack(); } + const { t } = useTranslation(); return ( - <Page title="Found schedules" modalSize={{ width: 850, height: 650 }}> + <Page + title={t('schedules.foundSchedules')} + modalSize={{ width: 850, height: 650 }} + > + <P>{t('schedules.foundSomePossibleSchedulesAdvice')}</P> + <P>{t('schedules.expectedSchedulesAdvice')}</P> <P> - We found some possible schedules in your current transactions. Select - the ones you want to create. - </P> - <P> - 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. - </P> - <P> - You can always do this later {Platform.isBrowser - ? ' from the "Find schedules" item in the sidebar menu' - : ' from the "Tools > Find schedules" menu item'} - . + ? t('schedules.doFromFindSchedules') + : t('schedules.doFromToolsFindSchedules')} </P> <SelectedProvider instance={selectedInst}> @@ -194,14 +191,16 @@ export default function DiscoverSchedules() { justify="flex-end" style={{ paddingTop: 20 }} > - <Button onClick={() => history.goBack()}>Do nothing</Button> + <Button onClick={() => history.goBack()}> + {t('general.doNothing')} + </Button> <ButtonWithLoading primary loading={creating} disabled={selectedInst.items.size === 0} onClick={onCreate} > - Create schedules + {t('schedules.createSchedule', { count: selectedInst.items.size })} </ButtonWithLoading> </Stack> </Page> diff --git a/packages/desktop-client/src/components/schedules/EditSchedule.js b/packages/desktop-client/src/components/schedules/EditSchedule.js index 1310c74..4828f89 100644 --- a/packages/desktop-client/src/components/schedules/EditSchedule.js +++ b/packages/desktop-client/src/components/schedules/EditSchedule.js @@ -437,7 +437,7 @@ export default function ScheduleDetails() { > <Stack direction="row" style={{ marginTop: 20 }}> <FormField style={{ flex: 1 }}> - <FormLabel title={t('general.payee')} /> + <FormLabel title={t('general.payee_one')} /> <PayeeAutocomplete value={state.fields.payee} inputProps={{ placeholder: t('schedules.none') }} diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.js b/packages/desktop-client/src/components/schedules/SchedulesTable.js index c2f1773..9877582 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.js +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.js @@ -241,7 +241,7 @@ export function SchedulesTable({ return ( <> <TableHeader height={ROW_HEIGHT} inset={15} version="v2"> - <Field width="flex">{t('general.payee')}</Field> + <Field width="flex">{t('general.payee_one')}</Field> <Field width="flex">{t('general.account_one')}</Field> <Field width={110}>{t('schedules.nextDate')}</Field> <Field width={120}>{t('general.status')}</Field> diff --git a/packages/desktop-client/src/components/util/AmountInput.js b/packages/desktop-client/src/components/util/AmountInput.js index bdf657e..f9e9820 100644 --- a/packages/desktop-client/src/components/util/AmountInput.js +++ b/packages/desktop-client/src/components/util/AmountInput.js @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { integerToCurrency, @@ -52,6 +53,8 @@ export function BetweenAmountInput({ defaultValue, onChange }) { let [num1, setNum1] = useState(defaultValue.num1); let [num2, setNum2] = useState(defaultValue.num2); + const { t } = useTranslation(); + return ( <View style={{ flexDirection: 'row', alignItems: 'center' }}> <AmountInput @@ -61,7 +64,7 @@ export function BetweenAmountInput({ defaultValue, onChange }) { onChange({ num1: value, num2 }); }} /> - <View style={{ margin: '0 5px' }}>and</View> + <View style={{ margin: '0 5px' }}>{t('general.and')}</View> <AmountInput defaultValue={num2} onChange={value => { diff --git a/packages/desktop-client/src/locales/en-GB.json b/packages/desktop-client/src/locales/en-GB.json index 5f5a058..49ee421 100644 --- a/packages/desktop-client/src/locales/en-GB.json +++ b/packages/desktop-client/src/locales/en-GB.json @@ -2,58 +2,148 @@ "account": { "addAccount": "Add account", "addAccountInFutureFromSidebar": "In the future, you can add accounts from the sidebar.", + "allAccounts": "All Accounts", "allReconciled": "All reconciled!", + "budgetedAccount_other": "Budgeted Accounts", + "cleared": "Cleared", "clearedBalance": "Your cleared balance <strong>{{cleared}}</strong> needs <strong>{{diff}}</strong> to match your bank's balance of <strong>{{balance}}</strong>", + "clearedTotal": "Cleared Total: <strong>{{amount}}</strong>", + "closeAccount": "Close Account", + "closedNamed": "Close: {{name}}", + "collapseSplitTransaction_other": "Collapse split transactions", "doneReconciling": "Done Reconciling", - "needAccountMessage": "For Actual to be useful, you need to <strong>add an account</strong>. You can link an account to automatically download transactions, or manage it locally yourself." + "enterCurrentBalanceToReconcileAdvice": "Enter the current balance of your bank account that you want to reconcile with:", + "expandSplitTransaction_other": "Expand split transactions", + "hideRunningBalance": "Hide Running Balance", + "linkAccount": "Link account", + "needAccountMessage": "For Actual to be useful, you need to <strong>add an account</strong>. You can link an account to automatically download transactions, or manage it locally yourself.", + "offBudgetAccount_other": "Off Budget Accounts", + "reconcile": "Reconcile", + "reopenAccount": "Reopen account", + "selectedBalance": "Selected Balance: <strong>{{amount}}</strong>", + "selectedTransaction_other": "Selected Transactions", + "showRunningBalance": "Show Running Balance", + "uncategorized": "Uncategorized", + "unclearedTotal": "Uncleared Total: <strong>{{amount}}</strong>", + "unlinkAccount": "Unlink Account" }, "bootstrap": { + "changeServerPassword": "Change server password", + "passwordCannotBeEmpty": "Password cannot be empty", + "passwordsDoNotMatch": "Passwords do not match", + "passwordSuccessfullyChanged": "Password Successfully changed", "setPassword": "Set a password for this server instance", + "thisWillChangeThePasswordAdvice": "This will change the password for this server instance. All existing sessions will stay logged in.", "title": "Bootstrap this Actual instance", - "tryDemo": "Try Demo" + "tryDemo": "Try Demo", + "unableToContactTheServer": "Unable to contact the server", + "unknownError": "Whoops, an error occurred on our side! We'll try to get it fixed soon." + }, + "budget": { + "chooseNumberMonths": "Choose the number of months shown at a time" }, "general": { "account_one": "Account", "account_other": "Accounts", "add": "Add", + "addNew": "Add new", + "addSplit": "Add Split", "amount": "Amount", + "amountIinflow": "Amount (inflow)", + "amountLeft": "Amount left: <strong>{{amount}}</strong>", + "amountOutflow": "Amount (outflow)", + "and": "and", + "apply": "Apply", "approximatelyWithAmount": "Approximately {{amount}}", + "balance_one": "Balance", "cancel": "Cancel", + "categorize": "Categorize", + "category_one": "Category", + "category_other": "Categories", "complete": "Complete", "date": "Date", "delete": "Delete", - "payee": "Payee", + "deposit_one": "Deposit", + "doNothing": "Do nothing", + "editField": "Edit field", + "export": "Export", + "exportTransaction_other": "Export Transactions", + "financialFile_other": "Financial Files", + "import": "Import", + "importedPayee": "Imported payee", + "invalidDateFormat": "Invalid date format", + "month": "Month", + "note_other": "Notes", + "noTransaction_other": "No transactions", + "offBudget": "Off Budget", + "payee_one": "Payee", + "payee_other": "Payees", + "payment_one": "Payment", "recurring": "Recurring", "repeats": "Repeats", "restart": "Restart", + "rule_one": "Rule", "save": "Save", "schedule": "Schedule", "schedule_other": "Schedules", - "status": "Status" + "search": "Search", + "show": "Show", + "split": "Split", + "status": "Status", + "sync": "Sync", + "transfer": "Transfer", + "when": "When", + "year": "Year" + }, + "rules": { + "addAction": "Add action", + "applyAction": "Apply action ({{size}})", + "ifAllTheseConditionsMatch": "If all these conditions match:", + "stageOfRule": "Stage of rule:", + "stageOfRuleAdvice": "The stage of a rule allows you to force a specific order. Pre rules always run first, and post rules always run last. Within each stage rules are automatically ordered from least to most specific.", + "stages": { + "default": "Default", + "post": "Post", + "pre": "Pre" + }, + "thenApplyTheseActions": "Then apply these actions:", + "thisRuleAppliesToTheseTransactions": "This rule applies to these transactions:" }, "schedules": { "addNewSchedule": "Add new schedule", "automaticallyAddTransaction": "Automatically add transaction", "automaticallyAddTransactionAdvice": "If checked, the schedule will automatically create transactions for you in the specified account", + "createSchedule_one": "Create schedule", + "createSchedule_other": "Create schedules", + "doFromFindSchedules": "You can always do this later from the \"Find schedules\" item in the sidebar menu", + "doFromToolsFindSchedules": "You can always do this later from the \"Tools > Find schedules\" menu item", "editAsRule": "Edit as rule", + "expectedSchedulesAdvice": "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.", "findMatchingTransactions": "Find matching transactions", + "foundSchedules": "Found schedules", + "foundSomePossibleSchedulesAdvice": "We found some possible schedules in your current transactions. Select the ones you want to create.", "isApproximately": "is approximately", "isBetween": "is between", "isExactly": "is exactly", "linkedTransactions": "Linked transactions", + "linkSchedule": "Link Schedule", "linkToSchedule": "Link to schedule", "nextDate": "Next date", "none": "(none)", "noSchedules": "No schedules", + "noSchedulesFound": "No schedules found", "postTransaction": "Post transaction", "scheduleNamed": "Schedule: {{name}}", "selectTransactionsToLinkOnSave": "Select transactions to link on save", "showCompletedSchedules": "Show completed schedules", "skipNextDate": "Skip next date", + "skipScheduledDate": "Skip scheduled date", "theseTransactionsMatchThisSchedule": "These transactions match this schedule:", "thisScheduleHasCustomConditionsAndActions": "This schedule has custom conditions and actions", "unlinkFromSchedule": "Unlink from schedule", - "upcomingDates": "Upcoming dates" + "unlinkSchedule": "Unlink Schedule", + "upcomingDates": "Upcoming dates", + "view_one": "View schedule" }, "status": { "completed": "completed", @@ -66,5 +156,8 @@ }, "support": { "anErrorOccuredWhileSaving": "An error occurred while saving. Please contact {{email}} for support." + }, + "transaction": { + "accountIsRequired": "Account is a required field" } } diff --git a/packages/desktop-client/src/locales/es-ES.json b/packages/desktop-client/src/locales/es-ES.json index 0dc3c2b..9691503 100644 --- a/packages/desktop-client/src/locales/es-ES.json +++ b/packages/desktop-client/src/locales/es-ES.json @@ -2,58 +2,149 @@ "account": { "addAccount": "Agregar cuenta", "addAccountInFutureFromSidebar": "En el futuro puedes agregar cuentas desde la barra lateral.", + "allAccounts": "Todas las cuentas", "allReconciled": "¡Todo conciliado!", + "budgetedAccount_other": "Cuentas presupuestadas", + "cleared": "Aclarada", "clearedBalance": "Su saldo compensado <strong>{{cleared}}</strong> necesita <strong>{{diff}}</strong> para coincidir con el saldo del banco de <strong>{{balance}}</strong>", + "clearedTotal": "Total aclarado: <strong>{{amount}}</strong>", + "closeAccount": "Cerrar cuenta", + "closedNamed": "Cerrada: {{name}}", + "collapseSplitTransaction_other": "Colapsar transacciones divididas", "doneReconciling": "Conciliación finalizada", - "needAccountMessage": "Para que Actual sea útil, debe <strong>agregar una cuenta</strong>. Puedes vincular la cuenta para descargar transacciones automáticamente o administrala localmente." + "enterCurrentBalanceToReconcileAdvice": "Ingrese el saldo actual de su cuenta que desea conciliar:", + "expandSplitTransaction_other": "Expandir transacciones divididas", + "hideRunningBalance": "Ocultar saldo actualizado", + "linkAccount": "Vincular cuenta", + "needAccountMessage": "Para que Actual sea útil, debe <strong>agregar una cuenta</strong>. Puedes vincular la cuenta para descargar transacciones automáticamente o administrala localmente.", + "offBudgetAccount_other": "Cuentas sin presupuestar", + "reconcile": "Conciliar", + "reopenAccount": "Re abrir cuenta", + "selectedBalance": "Balance seleccionado: <strong>{{amount}}</strong>", + "selectedTransaction_other": "Transacciones seleccionadas", + "showRunningBalance": "Mostrar saldo actualizado", + "uncategorized": "Sin categorizar", + "unclearedTotal": "Total sin aclarar: <strong>{{amount}}</strong>", + "unlinkAccount": "Desvincular cuenta" }, "bootstrap": { + "changeServerPassword": "Cambiar contraseña del servidor", + "passwordCannotBeEmpty": "La contraseña no puede estar vacía", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "passwordSuccessfullyChanged": "La contraseña fue modificada exitosamente", "setPassword": "Establecer una contraseña para esta instancia de servidor", + "thisWillChangeThePasswordAdvice": "Este proceso modificará la contraseña para el servidor. Todas las sesiones existentes permanecerán conectadas.", "title": "Bootstrap esta instancia de Actual", - "tryDemo": "Probar Demo" + "tryDemo": "Probar Demo", + "unableToContactTheServer": "Incapaz de conectarse con el servidor", + "unknownError": "¡Vaya, ocurrió un error de nuestro lado! Intentaremos solucionarlo pronto." + }, + "budget": { + "chooseNumberMonths": "Seleccióne el numero de meses para mostrar a la vez" }, "general": { "account_one": "Cuenta", "account_other": "Cuentas", "add": "Agregar", + "addNew": "Agregar nuevo", + "addSplit": "Agregar división", "amount": "Monto", + "amountIinflow": "Monto (Entrada)", + "amountLeft": "Monto restante: <strong>{{amount}}</strong>", + "amountOutflow": "Monto (Salida)", + "and": "y", + "apply": "Aplicar", "approximatelyWithAmount": "Aproximadamente {{amount}}", + "balance_one": "Balance", "cancel": "Cancelar", + "categorize": "Categorizar", + "category_one": "Categoría", + "category_other": "Categorías", "complete": "Completar", "date": "Fecha", "delete": "Borrar", - "payee": "Beneficiario", + "deposit_one": "Depósito", + "doNothing": "No hacer nada", + "editField": "Editar campo", + "export": "Exportar", + "exportTransaction_other": "Exportar transacciones", + "financialFile_other": "Archivos financieros", + "import": "Importar", + "importedPayee": "Beneficiario importado", + "invalidDateFormat": "Formato de fecha inválido", + "month": "Mes", + "note_other": "Notas", + "noTransaction_other": "Sin transacciones", + "offBudget": "Fuera de presupuesto", + "payee_one": "Beneficiario", + "payee_other": "Beneficiarios", + "payment_one": "Pago", "recurring": "Periódico", "repeats": "Repetir", "restart": "Reiniciar", + "rule_one": "Regla", "save": "Guardar", "schedule": "Agenda", "schedule_other": "Agendas", - "status": "Estado" + "search": "Buscar", + "show": "Mostrar", + "split": "Dividir", + "status": "Estado", + "sync": "Sincronizar", + "transfer": "Transferencia", + "when": "Cuando", + "year": "Año" + }, + "rules": { + "addAction": "Agregar acción", + "applyAction": "Aplicar acción ({{size}})", + "ifAllTheseConditionsMatch": "Si todas estas condiciones coinciden:", + "stageOfRule": "Etapa de la regla:", + "stageOfRuleAdvice": "La etapa de una regla le permite forzar un orden específico. Reglas previas siempre se ejecuta primero y las reglas posteriores siempre se ejecutan al final. Dentro de cada etapa las reglas se ordenan automáticamente de menos a más específicas.", + "stages": { + "default": "Por defecto", + "post": "Posterior", + "pre": "Previa" + }, + "thenApplyTheseActions": "Aplíque éstas acciones:", + "thisRuleAppliesToTheseTransactions": "Ésta regla aplica a las siguientes transacciones:" }, "schedules": { - "addNewSchedule": "Agregar nuevo agenda", + "addNewSchedule": "Agregar nueva agenda", "automaticallyAddTransaction": "Agregar transacción automáticamente", "automaticallyAddTransactionAdvice": "Si se selecciona, la agenda creará automáticamente una transacción para la cuenta especificada", + "createSchedule_one": "Crear agenda", + "createSchedule_many": "Crear agendas", + "createSchedule_other": "Crear agendas", + "doFromFindSchedules": "Puedes hacerlo después desde el ítem de menú \"Buscar agendas\" en la barra lateral", + "doFromToolsFindSchedules": "Puedes hacerlo después desde el ítem de menú \"Herramientas > Buscar agendas\"", "editAsRule": "Editar como regla", + "expectedSchedulesAdvice": "Si estabas esperando encontrar alguna agenda y no se visualiza, puede deberse a que los beneficiarios de las transacciones no coincidan. Asegurate de cambiar el nombre los beneficiarios en todas las transacciones de una agenda para que sean el mismo.", "findMatchingTransactions": "Encontrar transacciones que coincidan", + "foundSchedules": "Agendas encontradas", + "foundSomePossibleSchedulesAdvice": "Encontramos posibles agendas en la transacción actual. Selecciona las que desea crear.", "isApproximately": "es aproximadamente", "isBetween": "está entre", "isExactly": "es exactamente", "linkedTransactions": "Transacciones vinculadas", + "linkSchedule": "Vincular agenda", "linkToSchedule": "Vincular a agenda", "nextDate": "Próxima fecha", "none": "(ninguno)", "noSchedules": "Sin agendas", + "noSchedulesFound": "No se encontraron agendas", "postTransaction": "Publicar transacción", "scheduleNamed": "Agenda: {{name}}", "selectTransactionsToLinkOnSave": "Seleccionar transacciones para vincular al guardar", "showCompletedSchedules": "Mostrar agendas completadas", "skipNextDate": "Saltar próxima fecha", + "skipScheduledDate": "Saltar próxima fecha agendada", "theseTransactionsMatchThisSchedule": "Éstas transacciones coinciden con la agenda", "thisScheduleHasCustomConditionsAndActions": "Ésta agenda tiene condiciones y acciones personalizadas", "unlinkFromSchedule": "Desvincular de la agenda", - "upcomingDates": "Próximas fechas" + "unlinkSchedule": "Desvincular agenda", + "upcomingDates": "Próximas fechas", + "view_one": "Ver agenda" }, "status": { "completed": "completo", @@ -66,5 +157,8 @@ }, "support": { "anErrorOccuredWhileSaving": "Ocurrió un error al guardar. Por favor, póngase en contacto con {{email}} para obtener asistencia." + }, + "transaction": { + "accountIsRequired": "Cuenta es un campo requerido" } }