Adding translation to rule editor and transaction table (#224)

* #199 Adding translation to rule editor and transaction table

* Feature: Translation to discover schedule table

Fix: Some translation improvements

* fix: Fix minor after check

* Feature: More translation to account

Fix: Add *_old.json files to ignore

* Update packages/desktop-client/src/components/accounts/Account.js

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>

* fix: Workaround for know caveats

* lint: fix import order

* fix: t is not a function when empty transactions list

* Feature: Translate account filters

* Feature: Translation on transactions table

* Feature: Translate budget and the rest of bootstrap

* Update packages/desktop-client/src/locales/es-ES.json

Co-authored-by: Jed Fox <git@jedfox.com>

* fix: Using the new key for unknow error

* refactor: push useTranslation up above function definition, etc

* refactor: push useTranslation up above function definition

* refactor: set key for Trans component balanceType

* refactor: pass i18keys to Trans components explicitly

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
Manuel Eduardo Cánepa Cihuelo 2022-09-06 06:22:22 -03:00 committed by GitHub
parent cbf1e18299
commit 6fb497dec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 411 additions and 157 deletions

View file

@ -16,3 +16,6 @@ npm-debug.log
*kcab.* *kcab.*
public/kcab public/kcab
# Ignore auto generated dictionaries with check-i18n
src/locales/*_old.json

View file

@ -151,6 +151,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
<View style={{ color: colors.n3 }}> <View style={{ color: colors.n3 }}>
<Text style={{ fontStyle: 'italic', textAlign: 'center' }}> <Text style={{ fontStyle: 'italic', textAlign: 'center' }}>
<Trans <Trans
i18nKey={'account.clearedBalance'}
values={{ values={{
cleared: format(cleared, 'financial'), cleared: format(cleared, 'financial'),
diff: diff:
@ -158,9 +159,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
format(targetDiff, 'financial'), format(targetDiff, 'financial'),
balance: format(targetBalance, 'financial') balance: format(targetBalance, 'financial')
}} }}
> />
{'account.clearedBalance'}
</Trans>
</Text> </Text>
</View> </View>
)} )}
@ -176,6 +175,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
function ReconcileTooltip({ account, onReconcile, onClose }) { function ReconcileTooltip({ account, onReconcile, onClose }) {
let balance = useSheetValue(queries.accountBalance(account)); let balance = useSheetValue(queries.accountBalance(account));
const { t } = useTranslation();
function onSubmit(e) { function onSubmit(e) {
let input = e.target.elements[0]; let input = e.target.elements[0];
@ -187,10 +187,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
return ( return (
<Tooltip position="bottom-right" width={275} onClose={onClose}> <Tooltip position="bottom-right" width={275} onClose={onClose}>
<View style={{ padding: '5px 8px' }}> <View style={{ padding: '5px 8px' }}>
<Text> <Text>{t('account.enterCurrentBalanceToReconcileAdvice')}</Text>
Enter the current balance of your bank account that you want to
reconcile with:
</Text>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
{balance != null && ( {balance != null && (
<InitialFocus> <InitialFocus>
@ -200,7 +197,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
/> />
</InitialFocus> </InitialFocus>
)} )}
<Button primary>Reconcile</Button> <Button primary>{t('account.reconcile')}</Button>
</form> </form>
</View> </View>
</Tooltip> </Tooltip>
@ -243,6 +240,7 @@ function AccountMenu({
onMenuSelect onMenuSelect
}) { }) {
let [tooltip, setTooltip] = useState('default'); let [tooltip, setTooltip] = useState('default');
const { t } = useTranslation();
return tooltip === 'reconcile' ? ( return tooltip === 'reconcile' ? (
<ReconcileTooltip <ReconcileTooltip
@ -263,19 +261,21 @@ function AccountMenu({
items={[ items={[
canShowBalances && { canShowBalances && {
name: 'toggle-balance', name: 'toggle-balance',
text: (showBalances ? 'Hide' : 'Show') + ' Running Balance' text: showBalances
? t('account.hideRunningBalance')
: t('account.showRunningBalance')
}, },
{ name: 'export', text: 'Export' }, { name: 'export', text: t('general.export') },
{ name: 'reconcile', text: 'Reconcile' }, { name: 'reconcile', text: t('account.reconcile') },
syncEnabled && syncEnabled &&
account && account &&
!account.closed && !account.closed &&
(canSync (canSync
? { name: 'unlink', text: 'Unlink Account' } ? { name: 'unlink', text: t('account.unlinkAccount') }
: { name: 'link', text: 'Link Account' }), : { name: 'link', text: t('account.linkAccount') }),
account.closed account.closed
? { name: 'reopen', text: 'Reopen Account' } ? { name: 'reopen', text: t('account.reopenAccount') }
: { name: 'close', text: 'Close Account' } : { name: 'close', text: t('account.closeAccount') }
].filter(x => x)} ].filter(x => x)}
/> />
</MenuTooltip> </MenuTooltip>
@ -283,19 +283,30 @@ function AccountMenu({
} }
function CategoryMenu({ onClose, onMenuSelect }) { function CategoryMenu({ onClose, onMenuSelect }) {
const { t } = useTranslation();
return ( return (
<MenuTooltip onClose={onClose}> <MenuTooltip onClose={onClose}>
<Menu <Menu
onMenuSelect={item => { onMenuSelect={item => {
onMenuSelect(item); onMenuSelect(item);
}} }}
items={[{ name: 'export', text: 'Export' }]} items={[{ name: 'export', text: t('general.export') }]}
/> />
</MenuTooltip> </MenuTooltip>
); );
} }
function DetailedBalance({ name, balance }) { 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 ( return (
<Text <Text
style={{ style={{
@ -306,8 +317,12 @@ function DetailedBalance({ name, balance }) {
color: colors.n5 color: colors.n5
}} }}
> >
{name}{' '} <Trans
<Text style={{ fontWeight: 600 }}>{format(balance, 'financial')}</Text> i18nKey={balanceType[name] || name}
values={{
amount: format(balance, 'financial')
}}
/>
</Text> </Text>
); );
} }
@ -342,7 +357,8 @@ function SelectedBalance({ selectedItems }) {
if (balance == null) { if (balance == null) {
return null; return null;
} }
return <DetailedBalance name="Selected balance:" balance={balance} />;
return <DetailedBalance name="selected" balance={balance} />;
} }
function MoreBalances({ balanceQuery }) { function MoreBalances({ balanceQuery }) {
@ -357,8 +373,8 @@ function MoreBalances({ balanceQuery }) {
return ( return (
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: 'row' }}>
<DetailedBalance name="Cleared total:" balance={cleared} /> <DetailedBalance name="cleared" balance={cleared} />
<DetailedBalance name="Uncleared total:" balance={uncleared} /> <DetailedBalance name="uncleared" balance={uncleared} />
</View> </View>
); );
} }
@ -458,6 +474,7 @@ function SelectedTransactionsButton({
}) { }) {
let selectedItems = useSelectedItems(); let selectedItems = useSelectedItems();
let history = useHistory(); let history = useHistory();
const { t } = useTranslation();
let types = useMemo(() => { let types = useMemo(() => {
let items = [...selectedItems]; let items = [...selectedItems];
@ -498,37 +515,43 @@ function SelectedTransactionsButton({
items={[ items={[
...(!types.trans ...(!types.trans
? [ ? [
{ name: 'view-schedule', text: 'View schedule' }, { name: 'view-schedule', text: t('schedules.view_one') },
{ name: 'post-transaction', text: 'Post transaction' }, {
{ name: 'skip', text: 'Skip scheduled date' } name: 'post-transaction',
text: t('schedules.postTransaction')
},
{ name: 'skip', text: t('schedules.skipScheduledDate') }
] ]
: [ : [
{ name: 'show', text: 'Show', key: 'F' }, { name: 'show', text: t('general.show'), key: 'F' },
{ name: 'delete', text: 'Delete', key: 'D' }, { name: 'delete', text: t('general.delete'), key: 'D' },
...(linked ...(linked
? [ ? [
{ {
name: 'view-schedule', name: 'view-schedule',
text: 'View schedule', text: t('schedules.view_one'),
disabled: selectedItems.size > 1 disabled: selectedItems.size > 1
}, },
{ name: 'unlink-schedule', text: 'Unlink schedule' } {
name: 'unlink-schedule',
text: t('schedules.unlinkSchedule')
}
] ]
: [ : [
{ {
name: 'link-schedule', name: 'link-schedule',
text: 'Link schedule' text: t('schedules.linkSchedule')
} }
]), ]),
Menu.line, Menu.line,
{ type: Menu.label, name: 'Edit field' }, { type: Menu.label, name: t('general.editField') },
{ name: 'date', text: 'Date' }, { name: 'date', text: t('general.date') },
{ name: 'account', text: 'Account', key: 'A' }, { name: 'account', text: t('general.account_one'), key: 'A' },
{ name: 'payee', text: 'Payee', key: 'P' }, { name: 'payee', text: t('general.payee_one'), key: 'P' },
{ name: 'notes', text: 'Notes', key: 'N' }, { name: 'notes', text: t('general.note_other'), key: 'N' },
{ name: 'category', text: 'Category', key: 'C' }, { name: 'category', text: t('general.category_one'), key: 'C' },
{ name: 'amount', text: 'Amount' }, { name: 'amount', text: t('general.amount') },
{ name: 'cleared', text: 'Cleared', key: 'L' } { name: 'cleared', text: t('account.cleared'), key: 'L' }
]) ])
]} ]}
onSelect={name => { onSelect={name => {
@ -620,6 +643,7 @@ const AccountHeader = React.memo(
let [menuOpen, setMenuOpen] = useState(false); let [menuOpen, setMenuOpen] = useState(false);
let searchInput = useRef(null); let searchInput = useRef(null);
let splitsExpanded = useSplitsExpanded(); let splitsExpanded = useSplitsExpanded();
const { t } = useTranslation();
let canSync = syncEnabled && account && account.account_id; let canSync = syncEnabled && account && account.account_id;
if (!account) { if (!account) {
@ -732,7 +756,7 @@ const AccountHeader = React.memo(
} }
style={{ color: 'currentColor', marginRight: 4 }} style={{ color: 'currentColor', marginRight: 4 }}
/>{' '} />{' '}
Sync {t('general.sync')}
</> </>
) : ( ) : (
<> <>
@ -741,7 +765,7 @@ const AccountHeader = React.memo(
height={13} height={13}
style={{ color: 'currentColor', marginRight: 4 }} style={{ color: 'currentColor', marginRight: 4 }}
/>{' '} />{' '}
Import {t('general.import')}
</> </>
)} )}
</Button> </Button>
@ -753,7 +777,7 @@ const AccountHeader = React.memo(
height={10} height={10}
style={{ color: 'inherit', marginRight: 3 }} style={{ color: 'inherit', marginRight: 3 }}
/>{' '} />{' '}
Add New {t('general.addNew')}
</Button> </Button>
)} )}
<View> <View>
@ -774,7 +798,7 @@ const AccountHeader = React.memo(
} }
inputRef={searchInput} inputRef={searchInput}
value={search} value={search}
placeholder="Search" placeholder={t('general.search')}
getStyle={focused => [ getStyle={focused => [
{ {
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -812,8 +836,8 @@ const AccountHeader = React.memo(
onClick={onToggleSplits} onClick={onToggleSplits}
title={ title={
splitsExpanded.state.mode === 'collapse' splitsExpanded.state.mode === 'collapse'
? 'Collapse split transactions' ? t('account.collapseSplitTransaction_other')
: 'Expand split transactions' : t('account.expandSplitTransaction_other')
} }
> >
{splitsExpanded.state.mode === 'collapse' ? ( {splitsExpanded.state.mode === 'collapse' ? (
@ -1191,11 +1215,15 @@ class AccountInternal extends React.PureComponent {
onImport = async () => { onImport = async () => {
const accountId = this.props.accountId; const accountId = this.props.accountId;
const account = this.props.accounts.find(acct => acct.id === accountId); const account = this.props.accounts.find(acct => acct.id === accountId);
const t = this.props.t;
if (account) { if (account) {
const res = await window.Actual.openFileDialog({ const res = await window.Actual.openFileDialog({
filters: [ 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 = let normalizedName =
accountName && accountName.replace(/[()]/g, '').replace(/\s+/g, '-'); accountName && accountName.replace(/[()]/g, '').replace(/\s+/g, '-');
let filename = `${normalizedName || 'transactions'}.csv`; let filename = `${normalizedName || 'transactions'}.csv`;
const t = this.props.t;
window.Actual.saveFile( window.Actual.saveFile(
exportedTransactions, exportedTransactions,
filename, filename,
'Export Transactions' t('general.exportTransaction_other')
); );
}; };
@ -1338,21 +1367,24 @@ class AccountInternal extends React.PureComponent {
if (filterName) { if (filterName) {
return filterName; return filterName;
} }
const t = this.props.t;
if (!account) { if (!account) {
if (id === 'budgeted') { if (id === 'budgeted') {
return 'Budgeted Accounts'; return t('account.budgetedAccount_other');
} else if (id === 'offbudget') { } else if (id === 'offbudget') {
return 'Off Budget Accounts'; return t('account.offBudgetAccount_other');
} else if (id === 'uncategorized') { } else if (id === 'uncategorized') {
return 'Uncategorized'; return t('account.uncategorized');
} else if (!id) { } else if (!id) {
return 'All Accounts'; return t('account.allAccounts');
} }
return null; return null;
} }
return (account.closed ? 'Closed: ' : '') + account.name; return account.closed
? t('account.closedNamed', { name: account.name })
: account.name;
} }
getBalanceQuery(account, id) { getBalanceQuery(account, id) {
@ -1391,8 +1423,10 @@ class AccountInternal extends React.PureComponent {
}; };
onShowTransactions = async ids => { onShowTransactions = async ids => {
const t = this.props.t;
this.onApplyFilter({ this.onApplyFilter({
customName: 'Selected transactions', customName: t('account.selectedTransaction_other'),
filter: { id: { $oneof: ids } } filter: { id: { $oneof: ids } }
}); });
}; };
@ -1574,7 +1608,8 @@ class AccountInternal extends React.PureComponent {
replaceModal, replaceModal,
showExtraBalances, showExtraBalances,
expandSplits, expandSplits,
accountId accountId,
t
} = this.props; } = this.props;
let { let {
transactions, transactions,
@ -1709,7 +1744,7 @@ class AccountInternal extends React.PureComponent {
fontStyle: 'italic' fontStyle: 'italic'
}} }}
> >
No transactions {t('general.noTransaction_other')}
</View> </View>
) : null ) : null
} }
@ -1739,10 +1774,13 @@ class AccountInternal extends React.PureComponent {
function AccountHack(props) { function AccountHack(props) {
let { dispatch: splitsExpandedDispatch } = useSplitsExpanded(); let { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
const { t } = useTranslation();
return ( return (
<AccountInternal <AccountInternal
{...props} {...props}
splitsExpandedDispatch={splitsExpandedDispatch} splitsExpandedDispatch={splitsExpandedDispatch}
t={t}
/> />
); );
} }

View file

@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, useReducer } from 'react'; import React, { useState, useRef, useEffect, useReducer } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
@ -132,6 +133,8 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
ops = ['is']; ops = ['is'];
} }
const { t } = useTranslation();
return ( return (
<Tooltip <Tooltip
position="bottom-left" position="bottom-left"
@ -146,15 +149,15 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
options={ options={
field === 'amount' field === 'amount'
? [ ? [
['amount', 'Amount'], ['amount', t('general.amount')],
['amount-inflow', 'Amount (inflow)'], ['amount-inflow', t('general.amountIinflow')],
['amount-outflow', 'Amount (outflow)'] ['amount-outflow', t('general.amountOutflow')]
] ]
: field === 'date' : field === 'date'
? [ ? [
['date', 'Date'], ['date', t('general.date')],
['month', 'Month'], ['month', t('general.month')],
['year', 'Year'] ['year', t('general.year')]
] ]
: null : null
} }
@ -235,7 +238,7 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
}); });
}} }}
> >
Apply {t('general.apply')}
</Button> </Button>
</View> </View>
</form> </form>
@ -251,6 +254,8 @@ export function FilterButton({ onApply }) {
}; };
}); });
const { t } = useTranslation();
let [state, dispatch] = useReducer( let [state, dispatch] = useReducer(
(state, action) => { (state, action) => {
switch (action.type) { switch (action.type) {
@ -306,7 +311,7 @@ export function FilterButton({ onApply }) {
if (isDateValid(date)) { if (isDateValid(date)) {
cond.value = formatDate(date, 'yyyy-MM'); cond.value = formatDate(date, 'yyyy-MM');
} else { } else {
alert('Invalid date format'); alert(t('general.invalidDateFormat'));
return; return;
} }
} else if (cond.options.year) { } else if (cond.options.year) {
@ -314,7 +319,7 @@ export function FilterButton({ onApply }) {
if (isDateValid(date)) { if (isDateValid(date)) {
cond.value = formatDate(date, 'yyyy'); cond.value = formatDate(date, 'yyyy');
} else { } else {
alert('Invalid date format'); alert(t('general.invalidDateFormat'));
return; return;
} }
} }

View file

@ -1,4 +1,5 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
@ -181,6 +182,7 @@ export default function SimpleTransactionsTable({
}, },
[payees, categories, memoFields, selectedItems] [payees, categories, memoFields, selectedItems]
); );
const { t } = useTranslation();
return ( return (
<Table <Table
@ -200,43 +202,43 @@ export default function SimpleTransactionsTable({
case 'date': case 'date':
return ( return (
<Field key={i} width={100}> <Field key={i} width={100}>
Date {t('general.date')}
</Field> </Field>
); );
case 'imported_payee': case 'imported_payee':
return ( return (
<Field key={i} width="flex"> <Field key={i} width="flex">
Imported payee {t('general.importedPayee')}
</Field> </Field>
); );
case 'payee': case 'payee':
return ( return (
<Field key={i} width="flex"> <Field key={i} width="flex">
Payee {t('general.payee_one')}
</Field> </Field>
); );
case 'category': case 'category':
return ( return (
<Field key={i} width="flex"> <Field key={i} width="flex">
Category {t('general.category_one')}
</Field> </Field>
); );
case 'account': case 'account':
return ( return (
<Field key={i} width="flex"> <Field key={i} width="flex">
Account {t('general.account_one')}
</Field> </Field>
); );
case 'notes': case 'notes':
return ( return (
<Field key={i} width="flex"> <Field key={i} width="flex">
Notes {t('general.note_other')}
</Field> </Field>
); );
case 'amount': case 'amount':
return ( return (
<Field key={i} width={75} style={{ textAlign: 'right' }}> <Field key={i} width={75} style={{ textAlign: 'right' }}>
Amount {t('general.amount')}
</Field> </Field>
); );
default: default:

View file

@ -8,6 +8,7 @@ import React, {
useContext, useContext,
useReducer useReducer
} from 'react'; } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { import {
@ -253,6 +254,7 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) {
export const TransactionHeader = React.memo( export const TransactionHeader = React.memo(
({ hasSelected, showAccount, showCategory, showBalance }) => { ({ hasSelected, showAccount, showCategory, showBalance }) => {
let dispatchSelected = useSelectedDispatch(); let dispatchSelected = useSelectedDispatch();
const { t } = useTranslation();
return ( return (
<Row <Row
@ -271,14 +273,18 @@ export const TransactionHeader = React.memo(
width={20} width={20}
onSelect={() => dispatchSelected({ type: 'select-all' })} onSelect={() => dispatchSelected({ type: 'select-all' })}
/> />
<Cell value="Date" width={110} /> <Cell value={t('general.date')} width={110} />
{showAccount && <Cell value="Account" width="flex" />} {showAccount && <Cell value={t('general.account_one')} width="flex" />}
<Cell value="Payee" width="flex" /> <Cell value={t('general.payee_other')} width="flex" />
<Cell value="Notes" width="flex" /> <Cell value={t('general.note_other')} width="flex" />
{showCategory && <Cell value="Category" width="flex" />} {showCategory && (
<Cell value="Payment" width={80} textAlign="right" /> <Cell value={t('general.category_other')} width="flex" />
<Cell value="Deposit" width={80} textAlign="right" /> )}
{showBalance && <Cell value="Balance" width={85} textAlign="right" />} <Cell value={t('general.payment_one')} width={80} textAlign="right" />
<Cell value={t('general.deposit_one')} width={80} textAlign="right" />
{showBalance && (
<Cell value={t('general.balance_one')} width={85} textAlign="right" />
)}
<Field width={21} truncate={false} /> <Field width={21} truncate={false} />
<Cell value="" width={15 + styles.scrollbarWidth} /> <Cell value="" width={15 + styles.scrollbarWidth} />
</Row> </Row>
@ -528,6 +534,7 @@ export const Transaction = React.memo(function Transaction(props) {
onCreatePayee, onCreatePayee,
onToggleSplit onToggleSplit
} = props; } = props;
const { t } = useTranslation();
let dispatchSelected = useSelectedDispatch(); let dispatchSelected = useSelectedDispatch();
@ -896,7 +903,7 @@ export const Transaction = React.memo(function Transaction(props) {
/> />
)} )}
<Text style={{ fontStyle: 'italic', userSelect: 'none' }}> <Text style={{ fontStyle: 'italic', userSelect: 'none' }}>
Split {t('general.split')}
</Text> </Text>
</View> </View>
</CellButton> </CellButton>
@ -910,11 +917,11 @@ export const Transaction = React.memo(function Transaction(props) {
onExpose={!isPreview && (name => onEdit(id, name))} onExpose={!isPreview && (name => onEdit(id, name))}
value={ value={
isParent isParent
? 'Split' ? t('general.split')
: isOffBudget : isOffBudget
? 'Off Budget' ? t('general.offBudget')
: isBudgetTransfer : isBudgetTransfer
? 'Transfer' ? t('general.transfer')
: '' : ''
} }
valueStyle={valueStyle} valueStyle={valueStyle}
@ -936,7 +943,7 @@ export const Transaction = React.memo(function Transaction(props) {
'name' 'name'
) )
: transaction.id : transaction.id
? 'Categorize' ? t('general.categorize')
: '' : ''
} }
exposed={focusedField === 'category'} exposed={focusedField === 'category'}
@ -1049,6 +1056,8 @@ export const Transaction = React.memo(function Transaction(props) {
}); });
export function TransactionError({ error, isDeposit, onAddSplit, style }) { export function TransactionError({ error, isDeposit, onAddSplit, style }) {
const { t } = useTranslation();
switch (error.type) { switch (error.type) {
case 'SplitTransactionError': case 'SplitTransactionError':
if (error.version === 1) { if (error.version === 1) {
@ -1064,21 +1073,21 @@ export function TransactionError({ error, isDeposit, onAddSplit, style }) {
]} ]}
data-testid="transaction-error" data-testid="transaction-error"
> >
<Text> <Trans
Amount left:{' '} i18nKey={'general.amountLeft'}
<Text style={{ fontWeight: 500 }}> values={{
{integerToCurrency( amount: integerToCurrency(
isDeposit ? error.difference : -error.difference isDeposit ? error.difference : -error.difference
)} )
</Text> }}
</Text> />
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
<Button <Button
style={{ marginLeft: 15, padding: '4px 10px' }} style={{ marginLeft: 15, padding: '4px 10px' }}
primary primary
onClick={onAddSplit} onClick={onAddSplit}
> >
Add Split {t('general.addSplit')}
</Button> </Button>
</View> </View>
); );
@ -1135,6 +1144,7 @@ function NewTransaction({
}) { }) {
const error = transactions[0].error; const error = transactions[0].error;
const isDeposit = transactions[0].amount > 0; const isDeposit = transactions[0].amount > 0;
const { t } = useTranslation();
return ( return (
<View <View
@ -1192,7 +1202,7 @@ function NewTransaction({
onClick={() => onClose()} onClick={() => onClose()}
data-testid="cancel-button" data-testid="cancel-button"
> >
Cancel {t('general.cancel')}
</Button> </Button>
{error ? ( {error ? (
<TransactionError <TransactionError
@ -1207,7 +1217,7 @@ function NewTransaction({
onClick={onAdd} onClick={onAdd}
data-testid="add-button" data-testid="add-button"
> >
Add {t('general.add')}
</Button> </Button>
)} )}
</View> </View>
@ -1532,12 +1542,14 @@ export let TransactionTable = React.forwardRef((props, ref) => {
setPrevIsAdding(props.isAdding); setPrevIsAdding(props.isAdding);
} }
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (shouldAdd.current) { if (shouldAdd.current) {
if (newTransactions[0].account == null) { if (newTransactions[0].account == null) {
props.addNotification({ props.addNotification({
type: 'error', type: 'error',
message: 'Account is a required field' message: t('transaction.accountIsRequired')
}); });
newNavigator.onEdit('temp', 'account'); newNavigator.onEdit('temp', 'account');
} else { } else {

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useBudgetMonthCount } from 'loot-design/src/components/budget/BudgetMonthCountContext'; import { useBudgetMonthCount } from 'loot-design/src/components/budget/BudgetMonthCountContext';
import { View } from 'loot-design/src/components/common'; import { View } from 'loot-design/src/components/common';
@ -16,6 +17,7 @@ function Calendar({ color, onClick }) {
export function MonthCountSelector({ maxMonths, onChange }) { export function MonthCountSelector({ maxMonths, onChange }) {
let { displayMax } = useBudgetMonthCount(); let { displayMax } = useBudgetMonthCount();
const { t } = useTranslation();
let style = { width: 15, height: 15, color: colors.n8 }; let style = { width: 15, height: 15, color: colors.n8 };
let activeStyle = { color: colors.n5 }; let activeStyle = { color: colors.n5 };
@ -50,7 +52,7 @@ export function MonthCountSelector({ maxMonths, onChange }) {
transform: 'scale(1.2)' transform: 'scale(1.2)'
} }
}} }}
title="Choose the number of months shown at a time" title={t('budget.chooseNumberMonths')}
> >
{calendars} {calendars}
</View> </View>

View file

@ -24,13 +24,13 @@ export default function Bootstrap() {
function getErrorMessage(error) { function getErrorMessage(error) {
switch (error) { switch (error) {
case 'invalid-password': case 'invalid-password':
return 'Password cannot be empty'; return t('bootstrap.passwordCannotBeEmpty');
case 'password-match': case 'password-match':
return 'Passwords do not match'; return t('bootstrap.passwordsDoNotMatch');
case 'network-failure': case 'network-failure':
return 'Unable to contact the server'; return t('bootstrap.unableToContactTheServer');
default: default:
return "Whoops, an error occurred on our side! We'll try to get it fixed soon."; return t('bootstrap.unknownError');
} }
} }

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -14,17 +15,18 @@ export default function ChangePassword() {
let history = useHistory(); let history = useHistory();
let [error, setError] = useState(null); let [error, setError] = useState(null);
let [msg, setMessage] = useState(null); let [msg, setMessage] = useState(null);
const { t } = useTranslation();
function getErrorMessage(error) { function getErrorMessage(error) {
switch (error) { switch (error) {
case 'invalid-password': case 'invalid-password':
return 'Password cannot be empty'; return t('bootstrap.passwordCannotBeEmpty');
case 'password-match': case 'password-match':
return 'Passwords do not match'; return t('bootstrap.passwordsDoNotMatch');
case 'network-failure': case 'network-failure':
return 'Unable to contact the server'; return t('bootstrap.unableToContactTheServer');
default: default:
return 'Internal server error'; return t('bootstrap.unknownError');
} }
} }
@ -35,7 +37,7 @@ export default function ChangePassword() {
if (error) { if (error) {
setError(error); setError(error);
} else { } else {
setMessage('Password successfully changed'); setMessage(t('bootstrap.passwordSuccessfullyChanged'));
setTimeout(() => { setTimeout(() => {
history.push('/'); history.push('/');
@ -46,7 +48,7 @@ export default function ChangePassword() {
return ( return (
<> <>
<View style={{ width: 500, marginTop: -30 }}> <View style={{ width: 500, marginTop: -30 }}>
<Title text="Change server password" /> <Title text={t('bootstrap.changeServerPassword')} />
<Text <Text
style={{ style={{
fontSize: 16, fontSize: 16,
@ -54,8 +56,7 @@ export default function ChangePassword() {
lineHeight: 1.4 lineHeight: 1.4
}} }}
> >
This will change the password for this server instance. All existing {t('bootstrap.thisWillChangeThePasswordAdvice')}
sessions will stay logged in.
</Text> </Text>
{error && ( {error && (

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { import {
@ -280,7 +281,6 @@ function ActionEditor({ ops, action, editorStyle, onChange, onDelete, onAdd }) {
return ( return (
<Editor style={editorStyle} error={error}> <Editor style={editorStyle} error={error}>
{/*<OpSelect ops={ops} value={op} onChange={onChange} />*/} {/*<OpSelect ops={ops} value={op} onChange={onChange} />*/}
{op === 'set' ? ( {op === 'set' ? (
<> <>
<View style={{ padding: '5px 10px', lineHeight: '1em' }}> <View style={{ padding: '5px 10px', lineHeight: '1em' }}>
@ -325,6 +325,7 @@ function ActionEditor({ ops, action, editorStyle, onChange, onDelete, onAdd }) {
function StageInfo() { function StageInfo() {
let [open, setOpen] = useState(); let [open, setOpen] = useState();
const { t } = useTranslation();
return ( return (
<View style={{ position: 'relative', marginLeft: 5 }}> <View style={{ position: 'relative', marginLeft: 5 }}>
@ -346,9 +347,7 @@ function StageInfo() {
lineHeight: 1.5 lineHeight: 1.5
}} }}
> >
The stage of a rule allows you to force a specific order. Pre rules {t('rules.stageOfRuleAdvice')}
always run first, and post rules always run last. Within each stage
rules are automatically ordered from least to most specific.
</Tooltip> </Tooltip>
)} )}
</View> </View>
@ -554,6 +553,7 @@ export default function EditRule({
let [transactions, setTransactions] = useState([]); let [transactions, setTransactions] = useState([]);
let dispatch = useDispatch(); let dispatch = useDispatch();
let scrollableEl = useRef(); let scrollableEl = useRef();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
dispatch(initiallyLoadPayees()); dispatch(initiallyLoadPayees());
@ -687,7 +687,7 @@ export default function EditRule({
return ( return (
<Modal <Modal
title="Rule" title={t('general.rule_one')}
padding={0} padding={0}
{...modalProps} {...modalProps}
style={[modalProps.style, { flex: 'inherit', maxWidth: '90%' }]} style={[modalProps.style, { flex: 'inherit', maxWidth: '90%' }]}
@ -713,7 +713,7 @@ export default function EditRule({
}} }}
> >
<Text style={{ color: colors.n4, marginRight: 15 }}> <Text style={{ color: colors.n4, marginRight: 15 }}>
Stage of rule: {t('rules.stageOfRule')}
</Text> </Text>
<Stack direction="row" align="center" spacing={1}> <Stack direction="row" align="center" spacing={1}>
@ -721,19 +721,19 @@ export default function EditRule({
selected={stage === 'pre'} selected={stage === 'pre'}
onSelect={() => onChangeStage('pre')} onSelect={() => onChangeStage('pre')}
> >
Pre {t('rules.stages.pre')}
</StageButton> </StageButton>
<StageButton <StageButton
selected={stage === null} selected={stage === null}
onSelect={() => onChangeStage(null)} onSelect={() => onChangeStage(null)}
> >
Default {t('rules.stages.default')}
</StageButton> </StageButton>
<StageButton <StageButton
selected={stage === 'post'} selected={stage === 'post'}
onSelect={() => onChangeStage('post')} onSelect={() => onChangeStage('post')}
> >
Post {t('rules.stages.post')}
</StageButton> </StageButton>
<StageInfo /> <StageInfo />
@ -752,7 +752,7 @@ export default function EditRule({
<View style={{ flexShrink: 0 }}> <View style={{ flexShrink: 0 }}>
<View style={{ marginBottom: 30 }}> <View style={{ marginBottom: 30 }}>
<Text style={{ color: colors.n4, marginBottom: 15 }}> <Text style={{ color: colors.n4, marginBottom: 15 }}>
If all these conditions match: {t('rules.ifAllTheseConditionsMatch')}
</Text> </Text>
<ConditionsList <ConditionsList
@ -764,7 +764,7 @@ export default function EditRule({
</View> </View>
<Text style={{ color: colors.n4, marginBottom: 15 }}> <Text style={{ color: colors.n4, marginBottom: 15 }}>
Then apply these actions: {t('rules.thenApplyTheseActions')}
</Text> </Text>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
{actions.length === 0 ? ( {actions.length === 0 ? (
@ -772,7 +772,7 @@ export default function EditRule({
style={{ alignSelf: 'flex-start' }} style={{ alignSelf: 'flex-start' }}
onClick={addInitialAction} onClick={addInitialAction}
> >
Add action {t('rules.addAction')}
</Button> </Button>
) : ( ) : (
<Stack spacing={2}> <Stack spacing={2}>
@ -807,7 +807,7 @@ export default function EditRule({
}} }}
> >
<Text style={{ color: colors.n4, marginBottom: 0 }}> <Text style={{ color: colors.n4, marginBottom: 0 }}>
This rule applies to these transactions: {t('rules.thisRuleAppliesToTheseTransactions')}
</Text> </Text>
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
@ -815,7 +815,7 @@ export default function EditRule({
disabled={selectedInst.items.size === 0} disabled={selectedInst.items.size === 0}
onClick={onApply} onClick={onApply}
> >
Apply actions ({selectedInst.items.size}) {t('rules.applyAction', { size: selectedInst.items.size })}
</Button> </Button>
</View> </View>
@ -830,9 +830,11 @@ export default function EditRule({
justify="flex-end" justify="flex-end"
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
> >
<Button onClick={() => modalProps.onClose()}>Cancel</Button> <Button onClick={() => modalProps.onClose()}>
{t('general.cancel')}
</Button>
<Button primary onClick={() => onSave()}> <Button primary onClick={() => onSave()}>
Save {t('general.save')}
</Button> </Button>
</Stack> </Stack>
</View> </View>

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useHistory } from 'react-router-dom'; import { useLocation, useHistory } from 'react-router-dom';
import Platform from 'loot-core/src/client/platform'; import Platform from 'loot-core/src/client/platform';
@ -35,6 +36,7 @@ let ROW_HEIGHT = 43;
function DiscoverSchedulesTable({ schedules, loading }) { function DiscoverSchedulesTable({ schedules, loading }) {
let selectedItems = useSelectedItems(); let selectedItems = useSelectedItems();
let dispatchSelected = useSelectedDispatch(); let dispatchSelected = useSelectedDispatch();
const { t } = useTranslation();
function renderItem({ item }) { function renderItem({ item }) {
let selected = selectedItems.has(item.id); let selected = selectedItems.has(item.id);
@ -89,13 +91,13 @@ function DiscoverSchedulesTable({ schedules, loading }) {
selected={selectedItems.size > 0} selected={selectedItems.size > 0}
onSelect={() => dispatchSelected({ type: 'select-all' })} onSelect={() => dispatchSelected({ type: 'select-all' })}
/> />
<Field width="flex">Payee</Field> <Field width="flex">{t('general.payee_one')}</Field>
<Field width="flex">Account</Field> <Field width="flex">{t('general.account_one')}</Field>
<Field width="auto" style={{ flex: 1.5 }}> <Field width="auto" style={{ flex: 1.5 }}>
When {t('general.when')}
</Field> </Field>
<Field width={100} style={{ textAlign: 'right' }}> <Field width={100} style={{ textAlign: 'right' }}>
Amount {t('general.amount')}
</Field> </Field>
</TableHeader> </TableHeader>
<Table <Table
@ -107,7 +109,7 @@ function DiscoverSchedulesTable({ schedules, loading }) {
loading={loading} loading={loading}
isSelected={id => selectedItems.has(id)} isSelected={id => selectedItems.has(id)}
renderItem={renderItem} renderItem={renderItem}
renderEmpty="No schedules found" renderEmpty={t('schedules.noSchedulesFound')}
/> />
</View> </View>
); );
@ -161,24 +163,19 @@ export default function DiscoverSchedules() {
setCreating(false); setCreating(false);
history.goBack(); history.goBack();
} }
const { t } = useTranslation();
return ( 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> <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 {Platform.isBrowser
? ' from the "Find schedules" item in the sidebar menu' ? t('schedules.doFromFindSchedules')
: ' from the "Tools > Find schedules" menu item'} : t('schedules.doFromToolsFindSchedules')}
.
</P> </P>
<SelectedProvider instance={selectedInst}> <SelectedProvider instance={selectedInst}>
@ -194,14 +191,16 @@ export default function DiscoverSchedules() {
justify="flex-end" justify="flex-end"
style={{ paddingTop: 20 }} style={{ paddingTop: 20 }}
> >
<Button onClick={() => history.goBack()}>Do nothing</Button> <Button onClick={() => history.goBack()}>
{t('general.doNothing')}
</Button>
<ButtonWithLoading <ButtonWithLoading
primary primary
loading={creating} loading={creating}
disabled={selectedInst.items.size === 0} disabled={selectedInst.items.size === 0}
onClick={onCreate} onClick={onCreate}
> >
Create schedules {t('schedules.createSchedule', { count: selectedInst.items.size })}
</ButtonWithLoading> </ButtonWithLoading>
</Stack> </Stack>
</Page> </Page>

View file

@ -437,7 +437,7 @@ export default function ScheduleDetails() {
> >
<Stack direction="row" style={{ marginTop: 20 }}> <Stack direction="row" style={{ marginTop: 20 }}>
<FormField style={{ flex: 1 }}> <FormField style={{ flex: 1 }}>
<FormLabel title={t('general.payee')} /> <FormLabel title={t('general.payee_one')} />
<PayeeAutocomplete <PayeeAutocomplete
value={state.fields.payee} value={state.fields.payee}
inputProps={{ placeholder: t('schedules.none') }} inputProps={{ placeholder: t('schedules.none') }}

View file

@ -241,7 +241,7 @@ export function SchedulesTable({
return ( return (
<> <>
<TableHeader height={ROW_HEIGHT} inset={15} version="v2"> <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="flex">{t('general.account_one')}</Field>
<Field width={110}>{t('schedules.nextDate')}</Field> <Field width={110}>{t('schedules.nextDate')}</Field>
<Field width={120}>{t('general.status')}</Field> <Field width={120}>{t('general.status')}</Field>

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
integerToCurrency, integerToCurrency,
@ -52,6 +53,8 @@ export function BetweenAmountInput({ defaultValue, onChange }) {
let [num1, setNum1] = useState(defaultValue.num1); let [num1, setNum1] = useState(defaultValue.num1);
let [num2, setNum2] = useState(defaultValue.num2); let [num2, setNum2] = useState(defaultValue.num2);
const { t } = useTranslation();
return ( return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<AmountInput <AmountInput
@ -61,7 +64,7 @@ export function BetweenAmountInput({ defaultValue, onChange }) {
onChange({ num1: value, num2 }); onChange({ num1: value, num2 });
}} }}
/> />
<View style={{ margin: '0 5px' }}>and</View> <View style={{ margin: '0 5px' }}>{t('general.and')}</View>
<AmountInput <AmountInput
defaultValue={num2} defaultValue={num2}
onChange={value => { onChange={value => {

View file

@ -2,58 +2,148 @@
"account": { "account": {
"addAccount": "Add account", "addAccount": "Add account",
"addAccountInFutureFromSidebar": "In the future, you can add accounts from the sidebar.", "addAccountInFutureFromSidebar": "In the future, you can add accounts from the sidebar.",
"allAccounts": "All Accounts",
"allReconciled": "All reconciled!", "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>", "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", "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": { "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", "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", "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": { "general": {
"account_one": "Account", "account_one": "Account",
"account_other": "Accounts", "account_other": "Accounts",
"add": "Add", "add": "Add",
"addNew": "Add new",
"addSplit": "Add Split",
"amount": "Amount", "amount": "Amount",
"amountIinflow": "Amount (inflow)",
"amountLeft": "Amount left: <strong>{{amount}}</strong>",
"amountOutflow": "Amount (outflow)",
"and": "and",
"apply": "Apply",
"approximatelyWithAmount": "Approximately {{amount}}", "approximatelyWithAmount": "Approximately {{amount}}",
"balance_one": "Balance",
"cancel": "Cancel", "cancel": "Cancel",
"categorize": "Categorize",
"category_one": "Category",
"category_other": "Categories",
"complete": "Complete", "complete": "Complete",
"date": "Date", "date": "Date",
"delete": "Delete", "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", "recurring": "Recurring",
"repeats": "Repeats", "repeats": "Repeats",
"restart": "Restart", "restart": "Restart",
"rule_one": "Rule",
"save": "Save", "save": "Save",
"schedule": "Schedule", "schedule": "Schedule",
"schedule_other": "Schedules", "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": { "schedules": {
"addNewSchedule": "Add new schedule", "addNewSchedule": "Add new schedule",
"automaticallyAddTransaction": "Automatically add transaction", "automaticallyAddTransaction": "Automatically add transaction",
"automaticallyAddTransactionAdvice": "If checked, the schedule will automatically create transactions for you in the specified account", "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", "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", "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", "isApproximately": "is approximately",
"isBetween": "is between", "isBetween": "is between",
"isExactly": "is exactly", "isExactly": "is exactly",
"linkedTransactions": "Linked transactions", "linkedTransactions": "Linked transactions",
"linkSchedule": "Link Schedule",
"linkToSchedule": "Link to schedule", "linkToSchedule": "Link to schedule",
"nextDate": "Next date", "nextDate": "Next date",
"none": "(none)", "none": "(none)",
"noSchedules": "No schedules", "noSchedules": "No schedules",
"noSchedulesFound": "No schedules found",
"postTransaction": "Post transaction", "postTransaction": "Post transaction",
"scheduleNamed": "Schedule: {{name}}", "scheduleNamed": "Schedule: {{name}}",
"selectTransactionsToLinkOnSave": "Select transactions to link on save", "selectTransactionsToLinkOnSave": "Select transactions to link on save",
"showCompletedSchedules": "Show completed schedules", "showCompletedSchedules": "Show completed schedules",
"skipNextDate": "Skip next date", "skipNextDate": "Skip next date",
"skipScheduledDate": "Skip scheduled date",
"theseTransactionsMatchThisSchedule": "These transactions match this schedule:", "theseTransactionsMatchThisSchedule": "These transactions match this schedule:",
"thisScheduleHasCustomConditionsAndActions": "This schedule has custom conditions and actions", "thisScheduleHasCustomConditionsAndActions": "This schedule has custom conditions and actions",
"unlinkFromSchedule": "Unlink from schedule", "unlinkFromSchedule": "Unlink from schedule",
"upcomingDates": "Upcoming dates" "unlinkSchedule": "Unlink Schedule",
"upcomingDates": "Upcoming dates",
"view_one": "View schedule"
}, },
"status": { "status": {
"completed": "completed", "completed": "completed",
@ -66,5 +156,8 @@
}, },
"support": { "support": {
"anErrorOccuredWhileSaving": "An error occurred while saving. Please contact {{email}} for support." "anErrorOccuredWhileSaving": "An error occurred while saving. Please contact {{email}} for support."
},
"transaction": {
"accountIsRequired": "Account is a required field"
} }
} }

View file

@ -2,58 +2,149 @@
"account": { "account": {
"addAccount": "Agregar cuenta", "addAccount": "Agregar cuenta",
"addAccountInFutureFromSidebar": "En el futuro puedes agregar cuentas desde la barra lateral.", "addAccountInFutureFromSidebar": "En el futuro puedes agregar cuentas desde la barra lateral.",
"allAccounts": "Todas las cuentas",
"allReconciled": "¡Todo conciliado!", "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>", "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", "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": { "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", "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", "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": { "general": {
"account_one": "Cuenta", "account_one": "Cuenta",
"account_other": "Cuentas", "account_other": "Cuentas",
"add": "Agregar", "add": "Agregar",
"addNew": "Agregar nuevo",
"addSplit": "Agregar división",
"amount": "Monto", "amount": "Monto",
"amountIinflow": "Monto (Entrada)",
"amountLeft": "Monto restante: <strong>{{amount}}</strong>",
"amountOutflow": "Monto (Salida)",
"and": "y",
"apply": "Aplicar",
"approximatelyWithAmount": "Aproximadamente {{amount}}", "approximatelyWithAmount": "Aproximadamente {{amount}}",
"balance_one": "Balance",
"cancel": "Cancelar", "cancel": "Cancelar",
"categorize": "Categorizar",
"category_one": "Categoría",
"category_other": "Categorías",
"complete": "Completar", "complete": "Completar",
"date": "Fecha", "date": "Fecha",
"delete": "Borrar", "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", "recurring": "Periódico",
"repeats": "Repetir", "repeats": "Repetir",
"restart": "Reiniciar", "restart": "Reiniciar",
"rule_one": "Regla",
"save": "Guardar", "save": "Guardar",
"schedule": "Agenda", "schedule": "Agenda",
"schedule_other": "Agendas", "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": { "schedules": {
"addNewSchedule": "Agregar nuevo agenda", "addNewSchedule": "Agregar nueva agenda",
"automaticallyAddTransaction": "Agregar transacción automáticamente", "automaticallyAddTransaction": "Agregar transacción automáticamente",
"automaticallyAddTransactionAdvice": "Si se selecciona, la agenda creará automáticamente una transacción para la cuenta especificada", "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", "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", "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", "isApproximately": "es aproximadamente",
"isBetween": "está entre", "isBetween": "está entre",
"isExactly": "es exactamente", "isExactly": "es exactamente",
"linkedTransactions": "Transacciones vinculadas", "linkedTransactions": "Transacciones vinculadas",
"linkSchedule": "Vincular agenda",
"linkToSchedule": "Vincular a agenda", "linkToSchedule": "Vincular a agenda",
"nextDate": "Próxima fecha", "nextDate": "Próxima fecha",
"none": "(ninguno)", "none": "(ninguno)",
"noSchedules": "Sin agendas", "noSchedules": "Sin agendas",
"noSchedulesFound": "No se encontraron agendas",
"postTransaction": "Publicar transacción", "postTransaction": "Publicar transacción",
"scheduleNamed": "Agenda: {{name}}", "scheduleNamed": "Agenda: {{name}}",
"selectTransactionsToLinkOnSave": "Seleccionar transacciones para vincular al guardar", "selectTransactionsToLinkOnSave": "Seleccionar transacciones para vincular al guardar",
"showCompletedSchedules": "Mostrar agendas completadas", "showCompletedSchedules": "Mostrar agendas completadas",
"skipNextDate": "Saltar próxima fecha", "skipNextDate": "Saltar próxima fecha",
"skipScheduledDate": "Saltar próxima fecha agendada",
"theseTransactionsMatchThisSchedule": "Éstas transacciones coinciden con la agenda", "theseTransactionsMatchThisSchedule": "Éstas transacciones coinciden con la agenda",
"thisScheduleHasCustomConditionsAndActions": "Ésta agenda tiene condiciones y acciones personalizadas", "thisScheduleHasCustomConditionsAndActions": "Ésta agenda tiene condiciones y acciones personalizadas",
"unlinkFromSchedule": "Desvincular de la agenda", "unlinkFromSchedule": "Desvincular de la agenda",
"upcomingDates": "Próximas fechas" "unlinkSchedule": "Desvincular agenda",
"upcomingDates": "Próximas fechas",
"view_one": "Ver agenda"
}, },
"status": { "status": {
"completed": "completo", "completed": "completo",
@ -66,5 +157,8 @@
}, },
"support": { "support": {
"anErrorOccuredWhileSaving": "Ocurrió un error al guardar. Por favor, póngase en contacto con {{email}} para obtener asistencia." "anErrorOccuredWhileSaving": "Ocurrió un error al guardar. Por favor, póngase en contacto con {{email}} para obtener asistencia."
},
"transaction": {
"accountIsRequired": "Cuenta es un campo requerido"
} }
} }