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.*
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 }}>
<Text style={{ fontStyle: 'italic', textAlign: 'center' }}>
<Trans
i18nKey={'account.clearedBalance'}
values={{
cleared: format(cleared, 'financial'),
diff:
@ -158,9 +159,7 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
format(targetDiff, 'financial'),
balance: format(targetBalance, 'financial')
}}
>
{'account.clearedBalance'}
</Trans>
/>
</Text>
</View>
)}
@ -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 (
<Tooltip position="bottom-right" width={275} onClose={onClose}>
<View style={{ padding: '5px 8px' }}>
<Text>
Enter the current balance of your bank account that you want to
reconcile with:
</Text>
<Text>{t('account.enterCurrentBalanceToReconcileAdvice')}</Text>
<form onSubmit={onSubmit}>
{balance != null && (
<InitialFocus>
@ -200,7 +197,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
/>
</InitialFocus>
)}
<Button primary>Reconcile</Button>
<Button primary>{t('account.reconcile')}</Button>
</form>
</View>
</Tooltip>
@ -243,6 +240,7 @@ function AccountMenu({
onMenuSelect
}) {
let [tooltip, setTooltip] = useState('default');
const { t } = useTranslation();
return tooltip === 'reconcile' ? (
<ReconcileTooltip
@ -263,19 +261,21 @@ function AccountMenu({
items={[
canShowBalances && {
name: 'toggle-balance',
text: (showBalances ? 'Hide' : 'Show') + ' Running Balance'
text: showBalances
? t('account.hideRunningBalance')
: t('account.showRunningBalance')
},
{ name: 'export', text: 'Export' },
{ name: 'reconcile', text: 'Reconcile' },
{ name: 'export', text: t('general.export') },
{ name: 'reconcile', text: t('account.reconcile') },
syncEnabled &&
account &&
!account.closed &&
(canSync
? { name: 'unlink', text: 'Unlink Account' }
: { name: 'link', text: 'Link Account' }),
? { name: 'unlink', text: t('account.unlinkAccount') }
: { name: 'link', text: t('account.linkAccount') }),
account.closed
? { name: 'reopen', text: 'Reopen Account' }
: { name: 'close', text: 'Close Account' }
? { name: 'reopen', text: t('account.reopenAccount') }
: { name: 'close', text: t('account.closeAccount') }
].filter(x => x)}
/>
</MenuTooltip>
@ -283,19 +283,30 @@ function AccountMenu({
}
function CategoryMenu({ onClose, onMenuSelect }) {
const { t } = useTranslation();
return (
<MenuTooltip onClose={onClose}>
<Menu
onMenuSelect={item => {
onMenuSelect(item);
}}
items={[{ name: 'export', text: 'Export' }]}
items={[{ name: 'export', text: t('general.export') }]}
/>
</MenuTooltip>
);
}
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 (
<Text
style={{
@ -306,8 +317,12 @@ function DetailedBalance({ name, balance }) {
color: colors.n5
}}
>
{name}{' '}
<Text style={{ fontWeight: 600 }}>{format(balance, 'financial')}</Text>
<Trans
i18nKey={balanceType[name] || name}
values={{
amount: format(balance, 'financial')
}}
/>
</Text>
);
}
@ -342,7 +357,8 @@ function SelectedBalance({ selectedItems }) {
if (balance == null) {
return null;
}
return <DetailedBalance name="Selected balance:" balance={balance} />;
return <DetailedBalance name="selected" balance={balance} />;
}
function MoreBalances({ balanceQuery }) {
@ -357,8 +373,8 @@ function MoreBalances({ balanceQuery }) {
return (
<View style={{ flexDirection: 'row' }}>
<DetailedBalance name="Cleared total:" balance={cleared} />
<DetailedBalance name="Uncleared total:" balance={uncleared} />
<DetailedBalance name="cleared" balance={cleared} />
<DetailedBalance name="uncleared" balance={uncleared} />
</View>
);
}
@ -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')}
</>
)}
</Button>
@ -753,7 +777,7 @@ const AccountHeader = React.memo(
height={10}
style={{ color: 'inherit', marginRight: 3 }}
/>{' '}
Add New
{t('general.addNew')}
</Button>
)}
<View>
@ -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')}
</View>
) : null
}
@ -1739,10 +1774,13 @@ class AccountInternal extends React.PureComponent {
function AccountHack(props) {
let { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
const { t } = useTranslation();
return (
<AccountInternal
{...props}
splitsExpandedDispatch={splitsExpandedDispatch}
t={t}
/>
);
}

View file

@ -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 (
<Tooltip
position="bottom-left"
@ -146,15 +149,15 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
options={
field === 'amount'
? [
['amount', 'Amount'],
['amount-inflow', 'Amount (inflow)'],
['amount-outflow', 'Amount (outflow)']
['amount', t('general.amount')],
['amount-inflow', t('general.amountIinflow')],
['amount-outflow', t('general.amountOutflow')]
]
: field === 'date'
? [
['date', 'Date'],
['month', 'Month'],
['year', 'Year']
['date', t('general.date')],
['month', t('general.month')],
['year', t('general.year')]
]
: null
}
@ -235,7 +238,7 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
});
}}
>
Apply
{t('general.apply')}
</Button>
</View>
</form>
@ -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;
}
}

View file

@ -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 (
<Table
@ -200,43 +202,43 @@ export default function SimpleTransactionsTable({
case 'date':
return (
<Field key={i} width={100}>
Date
{t('general.date')}
</Field>
);
case 'imported_payee':
return (
<Field key={i} width="flex">
Imported payee
{t('general.importedPayee')}
</Field>
);
case 'payee':
return (
<Field key={i} width="flex">
Payee
{t('general.payee_one')}
</Field>
);
case 'category':
return (
<Field key={i} width="flex">
Category
{t('general.category_one')}
</Field>
);
case 'account':
return (
<Field key={i} width="flex">
Account
{t('general.account_one')}
</Field>
);
case 'notes':
return (
<Field key={i} width="flex">
Notes
{t('general.note_other')}
</Field>
);
case 'amount':
return (
<Field key={i} width={75} style={{ textAlign: 'right' }}>
Amount
{t('general.amount')}
</Field>
);
default:

View file

@ -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 (
<Row
@ -271,14 +273,18 @@ export const TransactionHeader = React.memo(
width={20}
onSelect={() => dispatchSelected({ type: 'select-all' })}
/>
<Cell value="Date" width={110} />
{showAccount && <Cell value="Account" width="flex" />}
<Cell value="Payee" width="flex" />
<Cell value="Notes" width="flex" />
{showCategory && <Cell value="Category" width="flex" />}
<Cell value="Payment" width={80} textAlign="right" />
<Cell value="Deposit" width={80} textAlign="right" />
{showBalance && <Cell value="Balance" width={85} textAlign="right" />}
<Cell value={t('general.date')} width={110} />
{showAccount && <Cell value={t('general.account_one')} width="flex" />}
<Cell value={t('general.payee_other')} width="flex" />
<Cell value={t('general.note_other')} width="flex" />
{showCategory && (
<Cell value={t('general.category_other')} width="flex" />
)}
<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} />
<Cell value="" width={15 + styles.scrollbarWidth} />
</Row>
@ -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) {
/>
)}
<Text style={{ fontStyle: 'italic', userSelect: 'none' }}>
Split
{t('general.split')}
</Text>
</View>
</CellButton>
@ -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"
>
<Text>
Amount left:{' '}
<Text style={{ fontWeight: 500 }}>
{integerToCurrency(
<Trans
i18nKey={'general.amountLeft'}
values={{
amount: integerToCurrency(
isDeposit ? error.difference : -error.difference
)}
</Text>
</Text>
)
}}
/>
<View style={{ flex: 1 }} />
<Button
style={{ marginLeft: 15, padding: '4px 10px' }}
primary
onClick={onAddSplit}
>
Add Split
{t('general.addSplit')}
</Button>
</View>
);
@ -1135,6 +1144,7 @@ function NewTransaction({
}) {
const error = transactions[0].error;
const isDeposit = transactions[0].amount > 0;
const { t } = useTranslation();
return (
<View
@ -1192,7 +1202,7 @@ function NewTransaction({
onClick={() => onClose()}
data-testid="cancel-button"
>
Cancel
{t('general.cancel')}
</Button>
{error ? (
<TransactionError
@ -1207,7 +1217,7 @@ function NewTransaction({
onClick={onAdd}
data-testid="add-button"
>
Add
{t('general.add')}
</Button>
)}
</View>
@ -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 {

View file

@ -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}
</View>

View file

@ -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');
}
}

View file

@ -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 (
<>
<View style={{ width: 500, marginTop: -30 }}>
<Title text="Change server password" />
<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 && (

View file

@ -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>

View file

@ -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>

View file

@ -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') }}

View file

@ -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>

View file

@ -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 => {

View file

@ -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"
}
}

View file

@ -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"
}
}