Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
f5d9f30e17 | |||
adbaf27859 | |||
5217835c55 | |||
6fb497dec5 | |||
cbf1e18299 | |||
11186c9374 | |||
953846732c | |||
55049da705 | |||
618dd0f27f | |||
e436c01430 | |||
304a384b6c | |||
b0f0c4a71d | |||
1fd3234613 | |||
a4fe21927d | |||
5c56370920 | |||
43740f18f1 | |||
2d025d8b08 | |||
dd9d32a6ed | |||
9b3dbd187f | |||
fd0d30c07c |
18
.github/workflows/i18n.yml
vendored
Normal file
18
.github/workflows/i18n.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: i18n
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches: '*'
|
||||
|
||||
jobs:
|
||||
check-keys:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Check i18n keys
|
||||
run: yarn workspaces foreach --verbose run check-i18n --fail-on-update
|
3
packages/desktop-client/.gitignore
vendored
3
packages/desktop-client/.gitignore
vendored
|
@ -16,3 +16,6 @@ npm-debug.log
|
|||
|
||||
*kcab.*
|
||||
public/kcab
|
||||
|
||||
# Ignore auto generated dictionaries with check-i18n
|
||||
src/locales/*_old.json
|
||||
|
|
16
packages/desktop-client/i18next-parser.config.js
Normal file
16
packages/desktop-client/i18next-parser.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
input: ['src/**/*.js'],
|
||||
output: 'src/locales/$LOCALE.json',
|
||||
locales: ['en-GB', 'es-ES'],
|
||||
defaultNamespace: 'web',
|
||||
sort: true,
|
||||
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
|
||||
lexers: {
|
||||
js: ['JsxLexer'],
|
||||
ts: ['JsxLexer'],
|
||||
jsx: ['JsxLexer'],
|
||||
tsx: ['JsxLexer'],
|
||||
|
||||
default: ['JsxLexer']
|
||||
}
|
||||
};
|
|
@ -42,6 +42,8 @@
|
|||
"fs-extra": "7.0.0",
|
||||
"glamor": "^2.20.40",
|
||||
"html-webpack-plugin": "4.0.0-alpha.2",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-parser": "^6.5.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"load-js": "^3.0.3",
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
|
@ -61,6 +63,7 @@
|
|||
"react-dev-utils": "^12.0.1",
|
||||
"react-dnd": "^10.0.2",
|
||||
"react-dom": "16.13.1",
|
||||
"react-i18next": "^11.18.4",
|
||||
"react-modal": "3.4.4",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router": "5.2.0",
|
||||
|
@ -86,7 +89,8 @@
|
|||
"watch": "cross-env PORT=3001 node scripts/start.js",
|
||||
"build": "cross-env INLINE_RUNTIME_CHUNK=false node scripts/build.js",
|
||||
"build:browser": "cross-env ./bin/build-browser",
|
||||
"lint": "eslint src"
|
||||
"lint": "eslint src",
|
||||
"check-i18n": "i18next"
|
||||
},
|
||||
"browserslist": [
|
||||
"electron 3.0"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Redirect, useParams, useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
|
@ -67,6 +68,8 @@ import {
|
|||
} from './TransactionsTable';
|
||||
|
||||
function EmptyMessage({ onAdd }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
@ -86,17 +89,15 @@ function EmptyMessage({ onAdd }) {
|
|||
}}
|
||||
>
|
||||
<Text style={{ textAlign: 'center', lineHeight: '1.4em' }}>
|
||||
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.
|
||||
<Trans>{'account.needAccountMessage'}</Trans>
|
||||
</Text>
|
||||
|
||||
<Button primary style={{ marginTop: 20 }} onClick={onAdd}>
|
||||
Add account
|
||||
{t('account.addAccount')}
|
||||
</Button>
|
||||
|
||||
<View style={{ marginTop: 20, fontSize: 13, color: colors.n5 }}>
|
||||
In the future, you can add accounts from the sidebar.
|
||||
{t('account.addAccountInFutureFromSidebar')}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -104,6 +105,8 @@ function EmptyMessage({ onAdd }) {
|
|||
}
|
||||
|
||||
function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let cleared = useSheetValue({
|
||||
name: balanceQuery.name + '-cleared',
|
||||
query: balanceQuery.query.filter({ cleared: true })
|
||||
|
@ -142,27 +145,27 @@ function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
|
|||
marginRight: 3
|
||||
}}
|
||||
/>
|
||||
All reconciled!
|
||||
{t('account.allReconciled')}
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ color: colors.n3 }}>
|
||||
<Text style={{ fontStyle: 'italic', textAlign: 'center' }}>
|
||||
Your cleared balance{' '}
|
||||
<strong>{format(cleared, 'financial')}</strong> needs{' '}
|
||||
<strong>
|
||||
{(targetDiff > 0 ? '+' : '') + format(targetDiff, 'financial')}
|
||||
</strong>{' '}
|
||||
to match
|
||||
<br /> your bank{"'"}s balance of{' '}
|
||||
<Text style={{ fontWeight: 700 }}>
|
||||
{format(targetBalance, 'financial')}
|
||||
</Text>
|
||||
<Trans
|
||||
i18nKey={'account.clearedBalance'}
|
||||
values={{
|
||||
cleared: format(cleared, 'financial'),
|
||||
diff:
|
||||
(targetDiff > 0 ? '+' : '') +
|
||||
format(targetDiff, 'financial'),
|
||||
balance: format(targetBalance, 'financial')
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={{ marginLeft: 15 }}>
|
||||
<Button primary onClick={onDone}>
|
||||
Done Reconciling
|
||||
{t('account.doneReconciling')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -172,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];
|
||||
|
@ -183,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>
|
||||
|
@ -196,7 +197,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
|
|||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button primary>Reconcile</Button>
|
||||
<Button primary>{t('account.reconcile')}</Button>
|
||||
</form>
|
||||
</View>
|
||||
</Tooltip>
|
||||
|
@ -239,6 +240,7 @@ function AccountMenu({
|
|||
onMenuSelect
|
||||
}) {
|
||||
let [tooltip, setTooltip] = useState('default');
|
||||
const { t } = useTranslation();
|
||||
|
||||
return tooltip === 'reconcile' ? (
|
||||
<ReconcileTooltip
|
||||
|
@ -259,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>
|
||||
|
@ -279,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={{
|
||||
|
@ -302,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>
|
||||
);
|
||||
}
|
||||
|
@ -338,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 }) {
|
||||
|
@ -353,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>
|
||||
);
|
||||
}
|
||||
|
@ -454,6 +474,7 @@ function SelectedTransactionsButton({
|
|||
}) {
|
||||
let selectedItems = useSelectedItems();
|
||||
let history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
let types = useMemo(() => {
|
||||
let items = [...selectedItems];
|
||||
|
@ -494,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 => {
|
||||
|
@ -616,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) {
|
||||
|
@ -728,7 +756,7 @@ const AccountHeader = React.memo(
|
|||
}
|
||||
style={{ color: 'currentColor', marginRight: 4 }}
|
||||
/>{' '}
|
||||
Sync
|
||||
{t('general.sync')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -737,7 +765,7 @@ const AccountHeader = React.memo(
|
|||
height={13}
|
||||
style={{ color: 'currentColor', marginRight: 4 }}
|
||||
/>{' '}
|
||||
Import
|
||||
{t('general.import')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
@ -749,7 +777,7 @@ const AccountHeader = React.memo(
|
|||
height={10}
|
||||
style={{ color: 'inherit', marginRight: 3 }}
|
||||
/>{' '}
|
||||
Add New
|
||||
{t('general.addNew')}
|
||||
</Button>
|
||||
)}
|
||||
<View>
|
||||
|
@ -770,7 +798,7 @@ const AccountHeader = React.memo(
|
|||
}
|
||||
inputRef={searchInput}
|
||||
value={search}
|
||||
placeholder="Search"
|
||||
placeholder={t('general.search')}
|
||||
getStyle={focused => [
|
||||
{
|
||||
backgroundColor: 'transparent',
|
||||
|
@ -808,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' ? (
|
||||
|
@ -1187,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']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -1216,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')
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1334,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) {
|
||||
|
@ -1387,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 } }
|
||||
});
|
||||
};
|
||||
|
@ -1570,7 +1608,8 @@ class AccountInternal extends React.PureComponent {
|
|||
replaceModal,
|
||||
showExtraBalances,
|
||||
expandSplits,
|
||||
accountId
|
||||
accountId,
|
||||
t
|
||||
} = this.props;
|
||||
let {
|
||||
transactions,
|
||||
|
@ -1705,7 +1744,7 @@ class AccountInternal extends React.PureComponent {
|
|||
fontStyle: 'italic'
|
||||
}}
|
||||
>
|
||||
No transactions
|
||||
{t('general.noTransaction_other')}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
|
@ -1735,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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -327,7 +327,7 @@ class Budget extends React.PureComponent {
|
|||
pathname: '/accounts',
|
||||
state: {
|
||||
goBack: true,
|
||||
filterName: `${categoryName} (${monthUtils.format(
|
||||
filterName: `${categoryName} (${monthUtils.nonLocalizedFormat(
|
||||
month,
|
||||
'MMMM yyyy'
|
||||
)})`,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -12,6 +13,8 @@ import { useBootstrapped, Title } from './common';
|
|||
import { ConfirmPasswordForm } from './ConfirmPasswordForm';
|
||||
|
||||
export default function Bootstrap() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let dispatch = useDispatch();
|
||||
let history = useHistory();
|
||||
let [error, setError] = useState(null);
|
||||
|
@ -21,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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,7 +56,7 @@ export default function Bootstrap() {
|
|||
return (
|
||||
<>
|
||||
<View style={{ width: 450, marginTop: -30 }}>
|
||||
<Title text="Bootstrap this Actual instance" />
|
||||
<Title text={t('bootstrap.title')} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
|
@ -61,7 +64,7 @@ export default function Bootstrap() {
|
|||
lineHeight: 1.4
|
||||
}}
|
||||
>
|
||||
Set a password for this server instance
|
||||
{t('bootstrap.setPassword')}
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
|
@ -84,7 +87,7 @@ export default function Bootstrap() {
|
|||
style={{ fontSize: 15, color: colors.b4, marginRight: 15 }}
|
||||
onClick={onDemo}
|
||||
>
|
||||
Try Demo
|
||||
{t('bootstrap.tryDemo')}
|
||||
</Button>
|
||||
}
|
||||
onSetPassword={onSetPassword}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
|
@ -258,7 +259,7 @@ function ScheduleDescription({ id }) {
|
|||
</Text>
|
||||
<Text style={{ margin: '0 5px' }}> — </Text>
|
||||
<Text style={{ flexShrink: 0 }}>
|
||||
Next: {monthUtils.format(schedule.next_date, dateFormat)}
|
||||
Next: {monthUtils.nonLocalizedFormat(schedule.next_date, dateFormat)}
|
||||
</Text>
|
||||
</View>
|
||||
<StatusBadge status={status} />
|
||||
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { format as formatDate, parseISO } from 'date-fns';
|
||||
|
@ -52,6 +53,7 @@ export function Value({
|
|||
data: dataProp,
|
||||
describe = x => x.name
|
||||
}) {
|
||||
const { i18n } = useTranslation();
|
||||
let { data, dateFormat } = useSelector(state => {
|
||||
let data;
|
||||
if (dataProp) {
|
||||
|
@ -95,7 +97,7 @@ export function Value({
|
|||
} else if (field === 'date') {
|
||||
if (value) {
|
||||
if (value.frequency) {
|
||||
return getRecurringDescription(value);
|
||||
return getRecurringDescription(value, i18n);
|
||||
}
|
||||
return formatDate(parseISO(value), dateFormat);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ function CashFlow() {
|
|||
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
||||
.map(month => ({
|
||||
name: month,
|
||||
pretty: monthUtils.format(month, 'MMMM, yyyy')
|
||||
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ function NetWorth({ accounts }) {
|
|||
.rangeInclusive(earliestMonth, monthUtils.currentMonth())
|
||||
.map(month => ({
|
||||
name: month,
|
||||
pretty: monthUtils.format(month, 'MMMM, yyyy')
|
||||
pretty: monthUtils.nonLocalizedFormat(month, 'MMMM, yyyy')
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
|
|
|
@ -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,11 +36,12 @@ let ROW_HEIGHT = 43;
|
|||
function DiscoverSchedulesTable({ schedules, loading }) {
|
||||
let selectedItems = useSelectedItems();
|
||||
let dispatchSelected = useSelectedDispatch();
|
||||
let { t, i18n } = useTranslation();
|
||||
|
||||
function renderItem({ item }) {
|
||||
let selected = selectedItems.has(item.id);
|
||||
let amountOp = item._conditions.find(c => c.field === 'amount').op;
|
||||
let recurDescription = getRecurringDescription(item.date);
|
||||
let recurDescription = getRecurringDescription(item.date, i18n);
|
||||
|
||||
return (
|
||||
<Row
|
||||
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useReducer } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -93,6 +94,7 @@ export default function ScheduleDetails() {
|
|||
});
|
||||
|
||||
let pageType = usePageType();
|
||||
const { t } = useTranslation();
|
||||
|
||||
let [state, dispatch] = useReducer(
|
||||
(state, action) => {
|
||||
|
@ -366,8 +368,10 @@ export default function ScheduleDetails() {
|
|||
if (res.error) {
|
||||
dispatch({
|
||||
type: 'form-error',
|
||||
error:
|
||||
'An error occurred while saving. Please contact help@actualbudget.com for support.'
|
||||
// Note: email is outside of translation to be easily replace on future
|
||||
error: t('support.anErrorOccuredWhileSaving', {
|
||||
email: 'help@actualbudget.com'
|
||||
})
|
||||
});
|
||||
} else {
|
||||
if (adding) {
|
||||
|
@ -424,15 +428,19 @@ export default function ScheduleDetails() {
|
|||
|
||||
return (
|
||||
<Page
|
||||
title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
|
||||
title={
|
||||
payee
|
||||
? t('schedules.scheduleNamed', { name: payee.name })
|
||||
: t('general.schedule')
|
||||
}
|
||||
modalSize="medium"
|
||||
>
|
||||
<Stack direction="row" style={{ marginTop: 20 }}>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title="Payee" />
|
||||
<FormLabel title={t('general.payee_one')} />
|
||||
<PayeeAutocomplete
|
||||
value={state.fields.payee}
|
||||
inputProps={{ placeholder: '(none)' }}
|
||||
inputProps={{ placeholder: t('schedules.none') }}
|
||||
onSelect={id =>
|
||||
dispatch({ type: 'set-field', field: 'payee', value: id })
|
||||
}
|
||||
|
@ -440,10 +448,10 @@ export default function ScheduleDetails() {
|
|||
</FormField>
|
||||
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title="Account" />
|
||||
<FormLabel title={t('general.account_one')} />
|
||||
<AccountAutocomplete
|
||||
value={state.fields.account}
|
||||
inputProps={{ placeholder: '(none)' }}
|
||||
inputProps={{ placeholder: t('schedules.none') }}
|
||||
onSelect={id =>
|
||||
dispatch({ type: 'set-field', field: 'account', value: id })
|
||||
}
|
||||
|
@ -452,18 +460,21 @@ export default function ScheduleDetails() {
|
|||
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<Stack direction="row" align="center" style={{ marginBottom: 3 }}>
|
||||
<FormLabel title="Amount" style={{ margin: 0, flex: 1 }} />
|
||||
<FormLabel
|
||||
title={t('general.amount')}
|
||||
style={{ margin: 0, flex: 1 }}
|
||||
/>
|
||||
<OpSelect
|
||||
ops={['is', 'isapprox', 'isbetween']}
|
||||
value={state.fields.amountOp}
|
||||
formatOp={op => {
|
||||
switch (op) {
|
||||
case 'is':
|
||||
return 'is exactly';
|
||||
return t('schedules.isExactly');
|
||||
case 'isapprox':
|
||||
return 'is approximately';
|
||||
return t('schedules.isApproximately');
|
||||
case 'isbetween':
|
||||
return 'is between';
|
||||
return t('schedules.isBetween');
|
||||
default:
|
||||
throw new Error('Invalid op for select: ' + op);
|
||||
}
|
||||
|
@ -505,7 +516,7 @@ export default function ScheduleDetails() {
|
|||
</Stack>
|
||||
|
||||
<View style={{ marginTop: 20 }}>
|
||||
<FormLabel title="Date" />
|
||||
<FormLabel title={t('general.date')} />
|
||||
</View>
|
||||
|
||||
<Stack direction="row" align="flex-start">
|
||||
|
@ -530,7 +541,7 @@ export default function ScheduleDetails() {
|
|||
{state.upcomingDates && (
|
||||
<View style={{ fontSize: 13, marginTop: 20 }}>
|
||||
<Text style={{ color: colors.n4, fontWeight: 600 }}>
|
||||
Upcoming dates
|
||||
{t('schedules.upcomingDates')}
|
||||
</Text>
|
||||
<Stack
|
||||
direction="column"
|
||||
|
@ -538,7 +549,9 @@ export default function ScheduleDetails() {
|
|||
style={{ marginTop: 10, color: colors.n4 }}
|
||||
>
|
||||
{state.upcomingDates.map(date => (
|
||||
<View>{monthUtils.format(date, `${dateFormat} EEEE`)}</View>
|
||||
<View>
|
||||
{monthUtils.nonLocalizedFormat(date, `${dateFormat} EEEE`)}
|
||||
</View>
|
||||
))}
|
||||
</Stack>
|
||||
</View>
|
||||
|
@ -562,7 +575,7 @@ export default function ScheduleDetails() {
|
|||
}}
|
||||
/>
|
||||
<label for="form_repeats" style={{ userSelect: 'none' }}>
|
||||
Repeats
|
||||
{t('general.repeats')}
|
||||
</label>
|
||||
</View>
|
||||
|
||||
|
@ -593,7 +606,7 @@ export default function ScheduleDetails() {
|
|||
}}
|
||||
/>
|
||||
<label for="form_posts_transaction" style={{ userSelect: 'none' }}>
|
||||
Automatically add transaction
|
||||
{t('schedules.automaticallyAddTransaction')}
|
||||
</label>
|
||||
</View>
|
||||
|
||||
|
@ -607,8 +620,7 @@ export default function ScheduleDetails() {
|
|||
lineHeight: '1.4em'
|
||||
}}
|
||||
>
|
||||
If checked, the schedule will automatically create transactions for
|
||||
you in the specified account
|
||||
{t('schedules.automaticallyAddTransactionAdvice')}
|
||||
</Text>
|
||||
|
||||
{!adding && state.schedule.rule && (
|
||||
|
@ -622,11 +634,11 @@ export default function ScheduleDetails() {
|
|||
width: 350
|
||||
}}
|
||||
>
|
||||
This schedule has custom conditions and actions
|
||||
{t('schedules.thisScheduleHasCustomConditionsAndActions')}
|
||||
</Text>
|
||||
)}
|
||||
<Button onClick={() => onEditRule()} disabled={adding}>
|
||||
Edit as rule
|
||||
{t('schedules.editAsRule')}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -638,11 +650,11 @@ export default function ScheduleDetails() {
|
|||
{adding ? (
|
||||
<View style={{ flexDirection: 'row', padding: '5px 0' }}>
|
||||
<Text style={{ color: colors.n4 }}>
|
||||
These transactions match this schedule:
|
||||
{t('schedules.theseTransactionsMatchThisSchedule')}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ color: colors.n6 }}>
|
||||
Select transactions to link on save
|
||||
{t('schedules.selectTransactionsToLinkOnSave')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
|
@ -657,7 +669,7 @@ export default function ScheduleDetails() {
|
|||
}}
|
||||
onClick={() => onSwitchTransactions('linked')}
|
||||
>
|
||||
Linked transactions
|
||||
{t('schedules.linkedTransactions')}
|
||||
</Button>{' '}
|
||||
<Button
|
||||
bare
|
||||
|
@ -670,15 +682,20 @@ export default function ScheduleDetails() {
|
|||
}}
|
||||
onClick={() => onSwitchTransactions('matched')}
|
||||
>
|
||||
Find matching transactions
|
||||
{t('schedules.findMatchingTransactions')}
|
||||
</Button>
|
||||
<View style={{ flex: 1 }} />
|
||||
<SelectedItemsButton
|
||||
name="transactions"
|
||||
items={
|
||||
state.transactionsMode === 'linked'
|
||||
? [{ name: 'unlink', text: 'Unlink from schedule' }]
|
||||
: [{ name: 'link', text: 'Link to schedule' }]
|
||||
? [
|
||||
{
|
||||
name: 'unlink',
|
||||
text: t('schedules.unlinkFromSchedule')
|
||||
}
|
||||
]
|
||||
: [{ name: 'link', text: t('schedules.linkToSchedule') }]
|
||||
}
|
||||
onSelect={(name, ids) => {
|
||||
switch (name) {
|
||||
|
@ -716,10 +733,10 @@ export default function ScheduleDetails() {
|
|||
>
|
||||
{state.error && <Text style={{ color: colors.r4 }}>{state.error}</Text>}
|
||||
<Button style={{ marginRight: 10 }} onClick={() => history.goBack()}>
|
||||
Cancel
|
||||
{t('general.cancel')}
|
||||
</Button>
|
||||
<Button primary onClick={onSave}>
|
||||
{adding ? 'Add' : 'Save'}
|
||||
{adding ? t('general.add') : t('general.save')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Page>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as monthUtils from 'loot-core/src/shared/months';
|
||||
|
@ -30,6 +31,8 @@ export let ROW_HEIGHT = 43;
|
|||
function OverflowMenu({ schedule, status, onAction }) {
|
||||
let [open, setOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button
|
||||
|
@ -60,15 +63,15 @@ function OverflowMenu({ schedule, status, onAction }) {
|
|||
items={[
|
||||
status === 'due' && {
|
||||
name: 'post-transaction',
|
||||
text: 'Post transaction'
|
||||
text: t('schedules.postTransaction')
|
||||
},
|
||||
...(schedule.completed
|
||||
? [{ name: 'restart', text: 'Restart' }]
|
||||
? [{ name: 'restart', text: t('general.restart') }]
|
||||
: [
|
||||
{ name: 'skip', text: 'Skip next date' },
|
||||
{ name: 'complete', text: 'Complete' }
|
||||
{ name: 'skip', text: t('schedules.skipNextDate') },
|
||||
{ name: 'complete', text: t('general.complete') }
|
||||
]),
|
||||
{ name: 'delete', text: 'Delete' }
|
||||
{ name: 'delete', text: t('general.delete') }
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
@ -82,6 +85,8 @@ export function ScheduleAmountCell({ amount, op }) {
|
|||
let str = integerToCurrency(Math.abs(num || 0));
|
||||
let isApprox = op === 'isapprox' || op === 'isbetween';
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Cell
|
||||
width={100}
|
||||
|
@ -101,7 +106,7 @@ export function ScheduleAmountCell({ amount, op }) {
|
|||
lineHeight: '1em',
|
||||
marginRight: 10
|
||||
}}
|
||||
title={(isApprox ? 'Approximately ' : '') + str}
|
||||
title={t('general.approximatelyWithAmount', { amount: str })}
|
||||
>
|
||||
~
|
||||
</View>
|
||||
|
@ -114,7 +119,9 @@ export function ScheduleAmountCell({ amount, op }) {
|
|||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
title={(isApprox ? 'Approximately ' : '') + str}
|
||||
title={
|
||||
isApprox ? t('general.approximatelyWithAmount', { amount: str }) : str
|
||||
}
|
||||
>
|
||||
{num > 0 ? `+${str}` : `${str}`}
|
||||
</Text>
|
||||
|
@ -137,6 +144,8 @@ export function SchedulesTable({
|
|||
|
||||
let [showCompleted, setShowCompleted] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
let items = useMemo(() => {
|
||||
if (!allowCompleted) {
|
||||
return schedules.filter(s => !s.completed);
|
||||
|
@ -172,7 +181,7 @@ export function SchedulesTable({
|
|||
</Field>
|
||||
<Field width={110}>
|
||||
{item.next_date
|
||||
? monthUtils.format(item.next_date, dateFormat)
|
||||
? monthUtils.nonLocalizedFormat(item.next_date, dateFormat)
|
||||
: null}
|
||||
</Field>
|
||||
<Field width={120} style={{ alignItems: 'flex-start' }}>
|
||||
|
@ -221,7 +230,7 @@ export function SchedulesTable({
|
|||
color: colors.n6
|
||||
}}
|
||||
>
|
||||
Show completed schedules
|
||||
{t('schedules.showCompletedSchedules')}
|
||||
</Field>
|
||||
</Row>
|
||||
);
|
||||
|
@ -232,16 +241,16 @@ export function SchedulesTable({
|
|||
return (
|
||||
<>
|
||||
<TableHeader height={ROW_HEIGHT} inset={15} version="v2">
|
||||
<Field width="flex">Payee</Field>
|
||||
<Field width="flex">Account</Field>
|
||||
<Field width={110}>Next date</Field>
|
||||
<Field width={120}>Status</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>
|
||||
<Field width={100} style={{ textAlign: 'right' }}>
|
||||
Amount
|
||||
{t('general.amount')}
|
||||
</Field>
|
||||
{!minimal && (
|
||||
<Field width={80} style={{ textAlign: 'center' }}>
|
||||
Recurring
|
||||
{t('general.recurring')}
|
||||
</Field>
|
||||
)}
|
||||
{!minimal && <Field width={40}></Field>}
|
||||
|
@ -253,7 +262,7 @@ export function SchedulesTable({
|
|||
style={[{ flex: 1, backgroundColor: 'transparent' }, style]}
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
renderEmpty="No schedules"
|
||||
renderEmpty={t('schedules.noSchedules')}
|
||||
allowPopupsEscape={items.length < 6}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { titleFirst } from 'loot-core/src/shared/util';
|
||||
import { View, Text } from 'loot-design/src/components/common';
|
||||
|
@ -11,52 +12,62 @@ import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar';
|
|||
import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck';
|
||||
|
||||
export function getStatusProps(status) {
|
||||
let color, backgroundColor, Icon;
|
||||
let color, backgroundColor, Icon, title;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
switch (status) {
|
||||
case 'missed':
|
||||
color = colors.r1;
|
||||
backgroundColor = colors.r10;
|
||||
Icon = EditSkull1;
|
||||
title = t('status.missed');
|
||||
break;
|
||||
case 'due':
|
||||
color = colors.y1;
|
||||
backgroundColor = colors.y9;
|
||||
Icon = AlertTriangle;
|
||||
title = t('status.due');
|
||||
break;
|
||||
case 'upcoming':
|
||||
color = colors.p1;
|
||||
backgroundColor = colors.p10;
|
||||
Icon = CalendarIcon;
|
||||
title = t('status.upcoming');
|
||||
break;
|
||||
case 'paid':
|
||||
color = colors.g2;
|
||||
backgroundColor = colors.g10;
|
||||
Icon = ValidationCheck;
|
||||
title = t('status.paid');
|
||||
break;
|
||||
case 'completed':
|
||||
color = colors.n4;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = FavoriteStar;
|
||||
title = t('status.completed');
|
||||
break;
|
||||
case 'pending':
|
||||
color = colors.g4;
|
||||
backgroundColor = colors.g11;
|
||||
Icon = CalendarIcon;
|
||||
title = t('status.pending');
|
||||
break;
|
||||
case 'scheduled':
|
||||
color = colors.n1;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CalendarIcon;
|
||||
title = t('status.scheduled');
|
||||
break;
|
||||
default:
|
||||
color = colors.n1;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CheckCircle1;
|
||||
title = status;
|
||||
break;
|
||||
}
|
||||
|
||||
return { color, backgroundColor, Icon };
|
||||
return { title, color, backgroundColor, Icon };
|
||||
}
|
||||
|
||||
export function StatusIcon({ status }) {
|
||||
|
@ -66,7 +77,7 @@ export function StatusIcon({ status }) {
|
|||
}
|
||||
|
||||
export function StatusBadge({ status, style }) {
|
||||
let { color, backgroundColor, Icon } = getStatusProps(status);
|
||||
let { title, color, backgroundColor, Icon } = getStatusProps(status);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
@ -90,7 +101,7 @@ export function StatusBadge({ status, style }) {
|
|||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
<Text style={{ lineHeight: '1em' }}>{titleFirst(status)}</Text>
|
||||
<Text style={{ lineHeight: '1em' }}>{titleFirst(title)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
|
||||
|
@ -13,6 +14,8 @@ export default function Schedules() {
|
|||
|
||||
let scheduleData = useSchedules();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (scheduleData == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -52,7 +55,7 @@ export default function Schedules() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Page title="Schedules">
|
||||
<Page title={t('general.schedule_other')}>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 20,
|
||||
|
@ -72,7 +75,7 @@ export default function Schedules() {
|
|||
|
||||
<View style={{ alignItems: 'flex-end', margin: '20px 0', flexShrink: 0 }}>
|
||||
<Button primary onClick={onAdd}>
|
||||
Add new schedule
|
||||
{t('schedules.addNewSchedule')}
|
||||
</Button>
|
||||
</View>
|
||||
</Page>
|
||||
|
|
|
@ -16,7 +16,7 @@ import Navigation from './Navigation';
|
|||
function Overspending({ navigationProps, stepTwo }) {
|
||||
let currentMonth = monthUtils.currentMonth();
|
||||
let sheetName = monthUtils.sheetForMonth(currentMonth);
|
||||
let month = monthUtils.format(currentMonth, 'MMM');
|
||||
let month = monthUtils.nonLocalizedFormat(currentMonth, 'MMM');
|
||||
let [minimized, toggle] = useMinimized();
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import './browser-preload';
|
|||
// A hack for now: this makes sure it's appended before glamor
|
||||
import '@reach/listbox/styles.css';
|
||||
|
||||
import './locales';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
|
163
packages/desktop-client/src/locales/en-GB.json
Normal file
163
packages/desktop-client/src/locales/en-GB.json
Normal file
|
@ -0,0 +1,163 @@
|
|||
{
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"unlinkSchedule": "Unlink Schedule",
|
||||
"upcomingDates": "Upcoming dates",
|
||||
"view_one": "View schedule"
|
||||
},
|
||||
"status": {
|
||||
"completed": "completed",
|
||||
"due": "due",
|
||||
"missed": "missed",
|
||||
"paid": "paid",
|
||||
"pending": "pending",
|
||||
"scheduled": "scheduled",
|
||||
"upcoming": "upcoming"
|
||||
},
|
||||
"support": {
|
||||
"anErrorOccuredWhileSaving": "An error occurred while saving. Please contact {{email}} for support."
|
||||
},
|
||||
"transaction": {
|
||||
"accountIsRequired": "Account is a required field"
|
||||
}
|
||||
}
|
164
packages/desktop-client/src/locales/es-ES.json
Normal file
164
packages/desktop-client/src/locales/es-ES.json
Normal file
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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 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",
|
||||
"unlinkSchedule": "Desvincular agenda",
|
||||
"upcomingDates": "Próximas fechas",
|
||||
"view_one": "Ver agenda"
|
||||
},
|
||||
"status": {
|
||||
"completed": "completo",
|
||||
"due": "adeudado",
|
||||
"missed": "omitido",
|
||||
"paid": "pago",
|
||||
"pending": "pendiente",
|
||||
"scheduled": "agendado",
|
||||
"upcoming": "próximo"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
40
packages/desktop-client/src/locales/index.js
Normal file
40
packages/desktop-client/src/locales/index.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import i18n from 'i18next';
|
||||
|
||||
import enUKCore from 'loot-core/src/locales/en-GB.json';
|
||||
import esESCore from 'loot-core/src/locales/es-ES.json';
|
||||
|
||||
import enUK from './en-GB.json';
|
||||
import esES from './es-ES.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
web: enUK,
|
||||
core: enUKCore
|
||||
},
|
||||
es: {
|
||||
web: esES,
|
||||
core: esESCore
|
||||
}
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.init({
|
||||
resources,
|
||||
defaultNS: 'web',
|
||||
lng: 'es', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
||||
// if you're using a language detector, do not define the lng option
|
||||
|
||||
// We enforce that a locales have all keys so we treat empty string as missing value.
|
||||
returnEmptyString: false,
|
||||
fallbackLng: 'en',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
16
packages/loot-core/i18next-parser.config.js
Normal file
16
packages/loot-core/i18next-parser.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
input: ['src/**/*.js'],
|
||||
output: 'src/locales/$LOCALE.json',
|
||||
locales: ['en-GB', 'es-ES'],
|
||||
defaultNamespace: 'core',
|
||||
sort: true,
|
||||
// Force usage of JsxLexer for .js files as otherwise we can't pick up <Trans> components.
|
||||
lexers: {
|
||||
js: ['JsxLexer'],
|
||||
ts: ['JsxLexer'],
|
||||
jsx: ['JsxLexer'],
|
||||
tsx: ['JsxLexer'],
|
||||
|
||||
default: ['JsxLexer']
|
||||
}
|
||||
};
|
|
@ -12,7 +12,8 @@
|
|||
"lint": "eslint src",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:node": "jest -c jest.config.js",
|
||||
"test:web": "jest -c jest.web.config.js"
|
||||
"test:web": "jest -c jest.web.config.js",
|
||||
"check-i18n": "i18next"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
@ -56,6 +57,8 @@
|
|||
"fake-indexeddb": "^3.1.3",
|
||||
"fast-check": "2.13.0",
|
||||
"fast-glob": "^2.2.0",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-parser": "^6.5.0",
|
||||
"jest": "^28.1.0",
|
||||
"jsverify": "^0.8.4",
|
||||
"lru-cache": "^5.1.1",
|
||||
|
|
27
packages/loot-core/src/locales/en-GB.json
Normal file
27
packages/loot-core/src/locales/en-GB.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"general": {
|
||||
"ordinal_one": "{{count}}st",
|
||||
"ordinal_two": "{{count}}nd",
|
||||
"ordinal_few": "{{count}}rd",
|
||||
"ordinal_other": "{{count}}th"
|
||||
},
|
||||
"schedules": {
|
||||
"recurring": {
|
||||
"monthly_one": "Every month on the {{day}}",
|
||||
"monthly_other": "Every {{count}} months on the {{day}}",
|
||||
"monthlyPattern_one": "Every month on the {{pattern}}",
|
||||
"monthlyPattern_other": "Every {{count}} months on the {{pattern}}",
|
||||
"pattern": {
|
||||
"lastDay": "last day",
|
||||
"lastWeekday": "last {{dayName}}",
|
||||
"lastWeekday_sameDay": "last",
|
||||
"weekAndDay": "{{week}} {{dayName}}",
|
||||
"weekAndDay_sameDay": "{{week}}"
|
||||
},
|
||||
"weekly_one": "Every week on {{day}}",
|
||||
"weekly_other": "Every {{count}} weeks on {{day}}",
|
||||
"yearly_one": "Every year on {{day}}",
|
||||
"yearly_other": "Every {{count}} years on {{day}}"
|
||||
}
|
||||
}
|
||||
}
|
28
packages/loot-core/src/locales/es-ES.json
Normal file
28
packages/loot-core/src/locales/es-ES.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"general": {
|
||||
"ordinal_other": "{{count}}º"
|
||||
},
|
||||
"schedules": {
|
||||
"recurring": {
|
||||
"monthly_one": "Cada {{day}} día del mes",
|
||||
"monthly_many": "Cada {{count}} de meses el {{day}} día",
|
||||
"monthly_other": "Cada {{count}} meses el {{day}} día",
|
||||
"monthlyPattern_one": "Cada mes {{pattern}}",
|
||||
"monthlyPattern_many": "Cada {{count}} de meses {{pattern}}",
|
||||
"monthlyPattern_other": "Cada {{count}} meses {{pattern}}",
|
||||
"pattern": {
|
||||
"lastDay": "el último día",
|
||||
"lastWeekday": "el último {{dayName}}",
|
||||
"lastWeekday_sameDay": "el último",
|
||||
"weekAndDay": "{{week}} {{dayName}}",
|
||||
"weekAndDay_sameDay": "{{week}}"
|
||||
},
|
||||
"weekly_one": "Cada semana {{day}}",
|
||||
"weekly_many": "Cada {{count}} de semanas el {{day}}",
|
||||
"weekly_other": "Cada {{count}} semanas el {{day}}",
|
||||
"yearly_one": "Cada año el {{day}}",
|
||||
"yearly_many": "Cada {{count}} de años el {{day}}",
|
||||
"yearly_other": "Cada {{count}} años el {{day}}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -210,11 +210,11 @@ export function sheetForMonth(month) {
|
|||
return 'budget' + month.replace('-', '');
|
||||
}
|
||||
|
||||
export function nameForMonth(month) {
|
||||
return d.format(_parse(month), "MMMM 'yy");
|
||||
export function format(month, opts, locale) {
|
||||
return Intl.DateTimeFormat(locale, opts).format(_parse(month));
|
||||
}
|
||||
|
||||
export function format(month, str) {
|
||||
export function nonLocalizedFormat(month, str) {
|
||||
return d.format(_parse(month), str);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,39 +42,49 @@ export function getHasTransactionsQuery(schedules) {
|
|||
.select(['schedule', 'date']);
|
||||
}
|
||||
|
||||
function makeNumberSuffix(num) {
|
||||
// Slight abuse of date-fns to turn a number like "1" into the full
|
||||
// form "1st" but formatting a date with that number
|
||||
return monthUtils.format(new Date(2020, 0, num, 12), 'do');
|
||||
}
|
||||
|
||||
function prettyDayName(day) {
|
||||
function prettyDayName(day, locale) {
|
||||
let days = {
|
||||
SU: 'Sunday',
|
||||
MO: 'Monday',
|
||||
TU: 'Tuesday',
|
||||
WE: 'Wednesday',
|
||||
TH: 'Thursday',
|
||||
FR: 'Friday',
|
||||
SA: 'Saturday'
|
||||
SU: new Date('2020-01-05T12:00:00.000Z'),
|
||||
MO: new Date('2020-01-06T12:00:00.000Z'),
|
||||
TU: new Date('2020-01-07T12:00:00.000Z'),
|
||||
WE: new Date('2020-01-08T12:00:00.000Z'),
|
||||
TH: new Date('2020-01-09T12:00:00.000Z'),
|
||||
FR: new Date('2020-01-10T12:00:00.000Z'),
|
||||
SA: new Date('2020-01-11T12:00:00.000Z')
|
||||
};
|
||||
return days[day];
|
||||
return Intl.DateTimeFormat(locale, { weekday: 'long' }).format(days[day]);
|
||||
}
|
||||
|
||||
export function getRecurringDescription(config) {
|
||||
function formatMonthAndDay(month, i18n) {
|
||||
let parts = Intl.DateTimeFormat(i18n.resolvedLanguage, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).formatToParts(monthUtils.parseDate(month));
|
||||
let dayPart = parts.find(p => p.type === 'day');
|
||||
dayPart.value = i18n.t('general.ordinal', {
|
||||
count: monthUtils.parseDate(month).getDate(),
|
||||
ordinal: true,
|
||||
ns: 'core'
|
||||
});
|
||||
return parts.map(part => part.value).join('');
|
||||
}
|
||||
|
||||
export function getRecurringDescription(config, i18n) {
|
||||
let interval = config.interval || 1;
|
||||
|
||||
switch (config.frequency) {
|
||||
case 'weekly': {
|
||||
let desc = 'Every ';
|
||||
desc += interval !== 1 ? `${interval} weeks` : 'week';
|
||||
desc += ' on ' + monthUtils.format(config.start, 'EEEE');
|
||||
return desc;
|
||||
return i18n.t('schedules.recurring.weekly', {
|
||||
count: interval,
|
||||
day: monthUtils.format(
|
||||
config.start,
|
||||
{ weekday: 'long' },
|
||||
i18n.resolvedLanguage
|
||||
),
|
||||
ns: 'core'
|
||||
});
|
||||
}
|
||||
case 'monthly': {
|
||||
let desc = 'Every ';
|
||||
desc += interval !== 1 ? `${interval} months` : 'month';
|
||||
|
||||
if (config.patterns && config.patterns.length > 0) {
|
||||
// Sort the days ascending. We filter out -1 because that
|
||||
// represents "last days" and should always be last, but this
|
||||
|
@ -95,56 +105,101 @@ export function getRecurringDescription(config) {
|
|||
// Add on all -1 values to the end
|
||||
patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
|
||||
|
||||
desc += ' on the ';
|
||||
|
||||
let strs = [];
|
||||
|
||||
let uniqueDays = new Set(patterns.map(p => p.type));
|
||||
let isSameDay = uniqueDays.length === 1 && !uniqueDays.has('day');
|
||||
let context =
|
||||
uniqueDays.length === 1 && !uniqueDays.has('day')
|
||||
? 'sameDay'
|
||||
: undefined;
|
||||
|
||||
for (let pattern of patterns) {
|
||||
if (pattern.type === 'day') {
|
||||
if (pattern.value === -1) {
|
||||
strs.push('last day');
|
||||
strs.push(
|
||||
i18n.t('schedules.recurring.pattern.lastDay', {
|
||||
ns: 'core'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Example: 15th day
|
||||
strs.push(makeNumberSuffix(pattern.value));
|
||||
strs.push(
|
||||
i18n.t('general.ordinal', {
|
||||
count: pattern.value,
|
||||
ordinal: true,
|
||||
ns: 'core'
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let dayName = isSameDay ? '' : ' ' + prettyDayName(pattern.type);
|
||||
let dayName = prettyDayName(
|
||||
pattern.type,
|
||||
i18n.resolvedLanguage,
|
||||
i18n.resolvedLanguage
|
||||
);
|
||||
|
||||
if (pattern.value === -1) {
|
||||
// Example: last Monday
|
||||
strs.push('last' + dayName);
|
||||
// t('schedules.recurring.pattern.lastWeekday')
|
||||
// t('schedules.recurring.pattern.lastWeekday_sameDay')
|
||||
strs.push(
|
||||
i18n.t('schedules.recurring.pattern.lastWeekday', {
|
||||
context,
|
||||
dayName,
|
||||
ns: 'core'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Example: 3rd Monday
|
||||
strs.push(makeNumberSuffix(pattern.value) + dayName);
|
||||
// t('schedules.recurring.pattern.weekAndDay')
|
||||
// t('schedules.recurring.pattern.weekAndDay_sameDay')
|
||||
strs.push(
|
||||
i18n.t('schedules.recurring.pattern.weekAndDay', {
|
||||
context,
|
||||
week: i18n.t('general.ordinal', {
|
||||
count: pattern.value,
|
||||
ordinal: true,
|
||||
ns: 'core'
|
||||
}),
|
||||
dayName,
|
||||
ns: 'core'
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strs.length > 2) {
|
||||
desc += strs.slice(0, strs.length - 1).join(', ');
|
||||
desc += ', and ';
|
||||
desc += strs[strs.length - 1];
|
||||
} else {
|
||||
desc += strs.join(' and ');
|
||||
}
|
||||
|
||||
if (isSameDay) {
|
||||
desc += ' ' + prettyDayName(patterns[0].type);
|
||||
}
|
||||
return i18n.t('schedules.recurring.monthlyPattern', {
|
||||
context,
|
||||
count: interval,
|
||||
day: prettyDayName(patterns[0].type, i18n.resolvedLanguage),
|
||||
pattern: new Intl.ListFormat(i18n.resolvedLanguage, {
|
||||
style: 'long',
|
||||
type: 'conjunction'
|
||||
}).format(strs),
|
||||
ns: 'core'
|
||||
});
|
||||
} else {
|
||||
desc += ' on the ' + monthUtils.format(config.start, 'do');
|
||||
return i18n.t('schedules.recurring.monthly', {
|
||||
count: interval,
|
||||
day: i18n.t('general.ordinal', {
|
||||
count: monthUtils.parseDate(config.start).getDate(),
|
||||
ordinal: true,
|
||||
ns: 'core'
|
||||
}),
|
||||
ns: 'core'
|
||||
});
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
case 'yearly': {
|
||||
let desc = 'Every ';
|
||||
desc += interval !== 1 ? `${interval} years` : 'year';
|
||||
desc += ' on ' + monthUtils.format(config.start, 'LLL do');
|
||||
return desc;
|
||||
return i18n.t(
|
||||
'schedules.recurring.yearly',
|
||||
{
|
||||
count: interval,
|
||||
day: formatMonthAndDay(config.start, i18n),
|
||||
ns: 'core'
|
||||
},
|
||||
i18n.resolvedLanguage
|
||||
);
|
||||
}
|
||||
default:
|
||||
return 'Recurring error';
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import i18n from 'i18next';
|
||||
import MockDate from 'mockdate';
|
||||
|
||||
import enUKCore from '../locales/en-GB.json';
|
||||
import { getRecurringDescription } from './schedules';
|
||||
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
core: enUKCore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('recurring date description', () => {
|
||||
beforeEach(() => {
|
||||
MockDate.set(new Date(2021, 4, 14));
|
||||
|
@ -9,149 +20,197 @@ describe('recurring date description', () => {
|
|||
|
||||
it('describes weekly interval', () => {
|
||||
expect(
|
||||
getRecurringDescription({ start: '2021-05-17', frequency: 'weekly' })
|
||||
getRecurringDescription(
|
||||
{ start: '2021-05-17', frequency: 'weekly' },
|
||||
i18n
|
||||
)
|
||||
).toBe('Every week on Monday');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-05-17',
|
||||
frequency: 'weekly',
|
||||
interval: 2
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-05-17',
|
||||
frequency: 'weekly',
|
||||
interval: 2
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every 2 weeks on Monday');
|
||||
});
|
||||
|
||||
it('describes monthly interval', () => {
|
||||
expect(
|
||||
getRecurringDescription({ start: '2021-04-25', frequency: 'monthly' })
|
||||
getRecurringDescription(
|
||||
{ start: '2021-04-25', frequency: 'monthly' },
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 25th');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
interval: 2
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
interval: 2
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every 2 months on the 25th');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: 25 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: 25 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 25th');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
interval: 2,
|
||||
patterns: [{ type: 'day', value: 25 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
interval: 2,
|
||||
patterns: [{ type: 'day', value: 25 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every 2 months on the 25th');
|
||||
|
||||
// Last day should work
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: 31 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: 31 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 31st');
|
||||
|
||||
// -1 should work, representing the last day
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: -1 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'day', value: -1 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the last day');
|
||||
|
||||
// Day names should work
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'FR', value: 2 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'FR', value: 2 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 2nd Friday');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'FR', value: -1 }]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [{ type: 'FR', value: -1 }]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the last Friday');
|
||||
});
|
||||
|
||||
it('describes monthly interval with multiple days', () => {
|
||||
// Note how order doesn't matter - the day should be sorted
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 15 },
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: 20 }
|
||||
]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 15 },
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: 20 }
|
||||
]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 3rd, 15th, and 20th');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: -1 },
|
||||
{ type: 'day', value: 20 }
|
||||
]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: -1 },
|
||||
{ type: 'day', value: 20 }
|
||||
]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 3rd, 20th, and last day');
|
||||
|
||||
// Mix days and day names
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: -1 },
|
||||
{ type: 'FR', value: 2 }
|
||||
]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'day', value: 3 },
|
||||
{ type: 'day', value: -1 },
|
||||
{ type: 'FR', value: 2 }
|
||||
]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 2nd Friday, 3rd, and last day');
|
||||
|
||||
// When there is a mixture of types, day names should always come first
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'SA', value: 1 },
|
||||
{ type: 'day', value: 2 },
|
||||
{ type: 'FR', value: 3 },
|
||||
{ type: 'day', value: 10 }
|
||||
]
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-04-25',
|
||||
frequency: 'monthly',
|
||||
patterns: [
|
||||
{ type: 'SA', value: 1 },
|
||||
{ type: 'day', value: 2 },
|
||||
{ type: 'FR', value: 3 },
|
||||
{ type: 'day', value: 10 }
|
||||
]
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every month on the 1st Saturday, 3rd Friday, 2nd, and 10th');
|
||||
});
|
||||
|
||||
it('describes yearly interval', () => {
|
||||
expect(
|
||||
getRecurringDescription({ start: '2021-05-17', frequency: 'yearly' })
|
||||
getRecurringDescription(
|
||||
{ start: '2021-05-17', frequency: 'yearly' },
|
||||
i18n
|
||||
)
|
||||
).toBe('Every year on May 17th');
|
||||
|
||||
expect(
|
||||
getRecurringDescription({
|
||||
start: '2021-05-17',
|
||||
frequency: 'yearly',
|
||||
interval: 2
|
||||
})
|
||||
getRecurringDescription(
|
||||
{
|
||||
start: '2021-05-17',
|
||||
frequency: 'yearly',
|
||||
interval: 2
|
||||
},
|
||||
i18n
|
||||
)
|
||||
).toBe('Every 2 years on May 17th');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useReducer, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendCatch } from 'loot-core/src/platform/client/fetch';
|
||||
|
@ -12,8 +13,6 @@ import SubtractIcon from 'loot-design/src/svg/Subtract';
|
|||
import { Button, Select, Input, Tooltip, View, Text, Stack } from './common';
|
||||
import DateSelect from './DateSelect';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
// ex: There is no 6th Friday of the Month
|
||||
const MAX_DAY_OF_WEEK_INTERVAL = 5;
|
||||
|
||||
|
@ -62,7 +61,7 @@ function unparseConfig(parsed) {
|
|||
|
||||
function createMonthlyRecurrence(startDate) {
|
||||
return {
|
||||
value: parseInt(monthUtils.format(startDate, 'd')),
|
||||
value: parseInt(monthUtils.nonLocalizedFormat(startDate, 'd')),
|
||||
type: 'day'
|
||||
};
|
||||
}
|
||||
|
@ -152,8 +151,8 @@ function SchedulePreview({ previewDates }) {
|
|||
<Stack direction="row" spacing={4} style={{ marginTop: 10 }}>
|
||||
{previewDates.map(d => (
|
||||
<View>
|
||||
<Text>{monthUtils.format(d, dateFormat)}</Text>
|
||||
<Text>{monthUtils.format(d, 'EEEE')}</Text>
|
||||
<Text>{monthUtils.nonLocalizedFormat(d, dateFormat)}</Text>
|
||||
<Text>{monthUtils.nonLocalizedFormat(d, 'EEEE')}</Text>
|
||||
</View>
|
||||
))}
|
||||
</Stack>
|
||||
|
@ -370,6 +369,7 @@ export default function RecurringSchedulePicker({
|
|||
onChange
|
||||
}) {
|
||||
let { isOpen, close, getOpenEvents } = useTooltip();
|
||||
let { i18n } = useTranslation();
|
||||
|
||||
function onSave(config) {
|
||||
onChange(config);
|
||||
|
@ -379,7 +379,7 @@ export default function RecurringSchedulePicker({
|
|||
return (
|
||||
<View>
|
||||
<Button {...getOpenEvents()} style={[{ textAlign: 'left' }, buttonStyle]}>
|
||||
{value ? getRecurringDescription(value) : 'No recurring date'}
|
||||
{value ? getRecurringDescription(value, i18n) : 'No recurring date'}
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<RecurringScheduleTooltip
|
||||
|
|
|
@ -1256,7 +1256,7 @@ export const MonthPicker = scope(lively => {
|
|||
|
||||
function getCurrentMonthName(startMonth, currentMonth) {
|
||||
return monthUtils.getYear(startMonth) === monthUtils.getYear(currentMonth)
|
||||
? monthUtils.format(currentMonth, 'MMM')
|
||||
? monthUtils.nonLocalizedFormat(currentMonth, 'MMM')
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -1264,7 +1264,7 @@ export const MonthPicker = scope(lively => {
|
|||
const currentMonth = monthUtils.currentMonth();
|
||||
const range = getRangeForYear(currentMonth);
|
||||
const monthNames = range.map(month => {
|
||||
return monthUtils.format(month, 'MMM');
|
||||
return monthUtils.nonLocalizedFormat(month, 'MMM');
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1314,7 +1314,7 @@ export const MonthPicker = scope(lively => {
|
|||
flex: '0 0 40px'
|
||||
}}
|
||||
>
|
||||
{monthUtils.format(year, 'yyyy')}
|
||||
{monthUtils.nonLocalizedFormat(year, 'yyyy')}
|
||||
</View>
|
||||
<ElementQuery
|
||||
sizes={[
|
||||
|
|
|
@ -321,7 +321,7 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
currentMonth === month && { textDecoration: 'underline' }
|
||||
])}
|
||||
>
|
||||
{monthUtils.format(month, 'MMMM')}
|
||||
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
|
||||
</div>
|
||||
|
||||
<View
|
||||
|
|
|
@ -271,7 +271,10 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
let prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
|
||||
let prevMonthName = monthUtils.nonLocalizedFormat(
|
||||
monthUtils.prevMonth(month),
|
||||
'MMM'
|
||||
);
|
||||
|
||||
let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1;
|
||||
|
||||
|
@ -337,7 +340,7 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
currentMonth === month && { textDecoration: 'underline' }
|
||||
])}
|
||||
>
|
||||
{monthUtils.format(month, 'MMMM')}
|
||||
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
|
||||
</div>
|
||||
|
||||
<View
|
||||
|
|
|
@ -1092,7 +1092,7 @@ export function BudgetHeader({
|
|||
}
|
||||
]}
|
||||
>
|
||||
{monthUtils.format(currentMonth, "MMMM ''yy")}
|
||||
{monthUtils.nonLocalizedFormat(currentMonth, "MMMM ''yy")}
|
||||
</Text>
|
||||
{editMode ? (
|
||||
<Button
|
||||
|
|
|
@ -692,7 +692,7 @@ export function DateHeader({ date }) {
|
|||
}}
|
||||
>
|
||||
<Text style={[styles.text, { fontSize: 13, color: colors.n4 }]}>
|
||||
{monthUtils.format(date, 'MMMM dd, yyyy')}
|
||||
{monthUtils.nonLocalizedFormat(date, 'MMMM dd, yyyy')}
|
||||
</Text>
|
||||
</ListItem>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||
import * as d from 'date-fns';
|
||||
|
||||
import * as actions from 'loot-core/src/client/actions';
|
||||
import { format as formatDate_ } from 'loot-core/src/shared/months';
|
||||
import { nonLocalizedFormat as formatDate_ } from 'loot-core/src/shared/months';
|
||||
import {
|
||||
amountToCurrency,
|
||||
amountToInteger,
|
||||
|
|
|
@ -28,7 +28,10 @@ import {
|
|||
} from 'loot-core/src/shared/categories.js';
|
||||
|
||||
function BudgetSummary({ month, onClose }) {
|
||||
const prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM');
|
||||
const prevMonthName = monthUtils.nonLocalizedFormat(
|
||||
monthUtils.prevMonth(month),
|
||||
'MMM'
|
||||
);
|
||||
|
||||
return (
|
||||
<NamespaceContext.Provider value={monthUtils.sheetForMonth(month)}>
|
||||
|
|
Loading…
Reference in a new issue