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 |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
|
@ -6,7 +6,7 @@ runs:
|
|||
- name: Install node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.15.0
|
||||
node-version: 16
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
|
|
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
|
22
.github/workflows/stale.yml
vendored
22
.github/workflows/stale.yml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: -1
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "🚧🚨 This issue is being marked as stale due to 90 days of inactivity. 🚧🚨"
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
only-labels: 'needs triage'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
67
README.md
67
README.md
|
@ -1,39 +1,72 @@
|
|||
## Getting Started
|
||||
This is the source code for [Actual](https://actualbudget.com), a local-first personal finance tool. It is 100% free and open-source.
|
||||
|
||||
Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting.
|
||||
If you are only interested in running the latest version, you don't need this repo. You can get the latest version through npm.
|
||||
|
||||
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md) we would love to have you.
|
||||
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
|
||||
|
||||
Want to say thanks? Click the ⭐ at the top of the page.
|
||||
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md)
|
||||
|
||||
## Key Links
|
||||
|
||||
* Actual [discord](https://discord.gg/pRYNYr4W5A) community.
|
||||
* Actual [Community Documentation](https://actualbudget.github.io/docs)
|
||||
Join the [discord](https://discord.gg/pRYNYr4W5A)!
|
||||
|
||||
## Installation
|
||||
|
||||
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
|
||||
|
||||
**Please Note:** While the Actual repository holds source code for the mobile applications that were supported when Actual was closed source, these are no longer supported on the Open Source version of Actual.
|
||||
|
||||
### The easy way: using a server (recommended)
|
||||
|
||||
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
|
||||
|
||||
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.github.io/docs/Installing/Local/your-own-machine)
|
||||
```
|
||||
git clone https://github.com/actualbudget/actual-server.git
|
||||
cd actual-server
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Navigate to https://localhost:5006 in your browser and you will see Actual.
|
||||
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.github.io/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
You should deploy the server somewhere so you can access your data from anywhere. See instructions on the [actual-server](https://github.com/actualbudget/actual-server) repo.
|
||||
|
||||
### Without a server
|
||||
|
||||
This will give you a fully local web app without a server. This npm package is the `packages/desktop-client` package in this repo built for production:
|
||||
|
||||
```
|
||||
yarn add @actual-app/web
|
||||
```
|
||||
|
||||
Now you need to serve the files in `node_modules/@actual-app/web/build`. One way to do it:
|
||||
|
||||
```
|
||||
cd node_modules/@actual-app/web/build
|
||||
npx http-server .
|
||||
```
|
||||
|
||||
Navigate to http://localhost:8080 and you should see Actual.
|
||||
|
||||
## Building
|
||||
|
||||
If you want to build the latest version, see [releasing.md](https://github.com/actualbudget/actual/blob/master/docs/releasing.md). It provides instructions for building this code into the same artifacts that come from npm.
|
||||
|
||||
## Run locally
|
||||
|
||||
Both the electron and web app can started with a single command. When running in development, it will store data in a `data` directory in the root of the `actual` directory.
|
||||
|
||||
First, make sure to run `yarn install` to install all dependencies.
|
||||
|
||||
In the root of the project:
|
||||
|
||||
```
|
||||
yarn start # Run the electron app
|
||||
yarn start:browser # Run the web app
|
||||
```
|
||||
|
||||
## Code structure
|
||||
|
||||
The Actual app is split up into a few packages:
|
||||
The app is split up into a few packages:
|
||||
|
||||
* loot-core - The core application that runs on any platform
|
||||
* loot-design - The generic design components that make up the UI
|
||||
* desktop-client - The desktop UI
|
||||
* desktop-electron - The desktop app
|
||||
* mobile - The mobile app
|
||||
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).
|
||||
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
|
||||
|
|
20
docs/API.md
Normal file
20
docs/API.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
|
||||
Previous docs for the API are [here](https://actualbudget.com/docs/developers/using-the-API/). The API is currently being improved. Previously, the API connected to an existing running instance of Actual. Now the API is bundled and fully isolated, capable of running all of Actual itself. Setting up the API is different because of this.
|
||||
|
||||
You need to call `init` and pass it the directory where your files live. Call `load-budget` to load the file you want to work on. After that, you can use the same API as before.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
let actual = require('@actual-app/api');
|
||||
|
||||
await actual.init({
|
||||
config: {
|
||||
dataDir: join(__dirname, 'user-files')
|
||||
}
|
||||
});
|
||||
|
||||
await actual.internal.send('load-budget', { id: 'My-Finances' });
|
||||
|
||||
await actual.getAccounts();
|
||||
```
|
18
docs/Building-for-Windows.md
Normal file
18
docs/Building-for-Windows.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# How to build browser for Windows
|
||||
Many of the build scripts are bash scripts and not natively invokable in Windows. To solve this, you can build the project using Git Bash.
|
||||
1. Install [Git & Git Bash for Windows](https://git-scm.com/downloads)
|
||||
2. Install Node v16.x (latest version 17.x does not work due to issue with crypto package)
|
||||
3. Clone this repo
|
||||
4. From the root of this repo, run `sh` to launch a bash shell
|
||||
5. From inside the bash shell, run `yarn install`
|
||||
6. From still inside the shell, run `yarn start:browser`
|
||||
7. Open your browser to `localhost:3001`
|
||||
|
||||
# How to build electron for Windows
|
||||
1. Follow steps 1 - 5 above.
|
||||
2. Run `yarn start`
|
||||
3. If you get an error from electron, run `yarn rebuild-electron` and rerun `yarn start`;
|
||||
|
||||
## rsync: command not found
|
||||
If you run into this error, you will need to install the rsync binary to Git Bash. Follow the [directions here](https://prasaz.medium.com/add-rsync-to-windows-git-bash-f42736bae1b3). When you get to the final step - installing the libxxhash dll - rename the dll from `msys-xxhash-0.8.0.dll` to `msys-xxhash-0.dll`
|
||||
|
44
docs/releasing.md
Normal file
44
docs/releasing.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# How to cut a release
|
||||
|
||||
In the open-source version of Actual, all updates go through npm. There are two libraries:
|
||||
|
||||
* `@actual-app/api`: The API for the underlying functionality. This includes the entire backend of Actual, meant to be used with node.
|
||||
* `@actual-app/web`: A web build that will serve the app with a web frontend. This includes both the frontend and backend of Actual. It includes the backend as well because it's built to be used as a Web Worker.
|
||||
|
||||
Both the API and web libraries are versioned together. This may change in the future, but because the web library also brings along its own backend it's easier to maintain a single version for now. That makes it clear which version the backend is regardless of library.
|
||||
|
||||
## Releasing `@actual-app/api`
|
||||
|
||||
This generates a bundle for the API:
|
||||
|
||||
```
|
||||
cd packages/loot-core
|
||||
yarn build:api
|
||||
```
|
||||
|
||||
The built files live in `lib-dist`, so we need to copy them to the API package:
|
||||
|
||||
```
|
||||
cp lib-dist/bundle.api* ../api/app
|
||||
```
|
||||
|
||||
Next, bump the version on package.json. Finally, publish it:
|
||||
|
||||
```
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Releasing `@actual-app/web`
|
||||
|
||||
In the root of `actual` (not just `desktop-client`), run this:
|
||||
|
||||
```
|
||||
./bin/package-browser
|
||||
```
|
||||
|
||||
This will compile both the backend and the frontend into a single directory in `packages/desktop-client/build`. This directory is all the files that need to be published. After bumping the version, publish `desktop-client`:
|
||||
|
||||
```
|
||||
cd packages/desktop-client
|
||||
npm publish
|
||||
```
|
1
packages/api/.gitignore
vendored
1
packages/api/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
app/bundle.api.js*
|
66420
packages/api/app/bundle.api.js
Normal file
66420
packages/api/app/bundle.api.js
Normal file
File diff suppressed because one or more lines are too long
1
packages/api/app/bundle.api.js.map
Normal file
1
packages/api/app/bundle.api.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,8 @@
|
|||
let bundle = require('./app/bundle.api.js');
|
||||
let injected = require('./injected');
|
||||
let methods = require('./methods');
|
||||
let utils = require('./utils');
|
||||
|
||||
let injected = require('./injected');
|
||||
let actualApp;
|
||||
|
||||
async function init({ budgetId, config } = {}) {
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "4.1.5",
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"app",
|
||||
"default-db.sqlite",
|
||||
"index.js",
|
||||
"injected.js",
|
||||
"methods.js",
|
||||
"migrations",
|
||||
"utils.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.5.0",
|
||||
"node-fetch": "^1.6.3",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function amountToInteger(n) {
|
||||
return Math.round(n * 100);
|
||||
return Math.round(n * 100) | 0;
|
||||
}
|
||||
|
||||
function integerToAmount(n) {
|
||||
|
|
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']
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "22.12.03",
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
|
@ -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,5 +1,4 @@
|
|||
default-db.sqlite
|
||||
migrations/.force-copy-windows
|
||||
migrations/1548957970627_remove-db-version.sql
|
||||
migrations/1550601598648_payees.sql
|
||||
migrations/1555786194328_remove_category_group_unique.sql
|
||||
|
@ -15,3 +14,4 @@ migrations/1615745967948_meta.sql
|
|||
migrations/1616167010796_accounts_order.sql
|
||||
migrations/1618975177358_schedules.sql
|
||||
migrations/1632571489012_remove_cache.js
|
||||
migrations/.force-copy-windows
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -35,7 +36,6 @@ import {
|
|||
Stack
|
||||
} from 'loot-design/src/components/common';
|
||||
import { KeyHandlers } from 'loot-design/src/components/KeyHandlers';
|
||||
import NotesButton from 'loot-design/src/components/NotesButton';
|
||||
import CellValue from 'loot-design/src/components/spreadsheet/CellValue';
|
||||
import format from 'loot-design/src/components/spreadsheet/format';
|
||||
import useSheetValue from 'loot-design/src/components/spreadsheet/useSheetValue';
|
||||
|
@ -68,6 +68,8 @@ import {
|
|||
} from './TransactionsTable';
|
||||
|
||||
function EmptyMessage({ onAdd }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
@ -87,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>
|
||||
|
@ -105,9 +105,10 @@ function EmptyMessage({ onAdd }) {
|
|||
}
|
||||
|
||||
function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let cleared = useSheetValue({
|
||||
name: balanceQuery.name + '-cleared',
|
||||
value: 0,
|
||||
query: balanceQuery.query.filter({ cleared: true })
|
||||
});
|
||||
let targetDiff = targetBalance - cleared;
|
||||
|
@ -144,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>
|
||||
|
@ -174,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];
|
||||
|
@ -185,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>
|
||||
|
@ -198,7 +197,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
|
|||
/>
|
||||
</InitialFocus>
|
||||
)}
|
||||
<Button primary>Reconcile</Button>
|
||||
<Button primary>{t('account.reconcile')}</Button>
|
||||
</form>
|
||||
</View>
|
||||
</Tooltip>
|
||||
|
@ -241,6 +240,7 @@ function AccountMenu({
|
|||
onMenuSelect
|
||||
}) {
|
||||
let [tooltip, setTooltip] = useState('default');
|
||||
const { t } = useTranslation();
|
||||
|
||||
return tooltip === 'reconcile' ? (
|
||||
<ReconcileTooltip
|
||||
|
@ -261,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>
|
||||
|
@ -281,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={{
|
||||
|
@ -304,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>
|
||||
);
|
||||
}
|
||||
|
@ -340,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 }) {
|
||||
|
@ -355,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>
|
||||
);
|
||||
}
|
||||
|
@ -456,6 +474,7 @@ function SelectedTransactionsButton({
|
|||
}) {
|
||||
let selectedItems = useSelectedItems();
|
||||
let history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
let types = useMemo(() => {
|
||||
let items = [...selectedItems];
|
||||
|
@ -496,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 => {
|
||||
|
@ -618,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) {
|
||||
|
@ -671,46 +697,30 @@ const AccountHeader = React.memo(
|
|||
/>
|
||||
</InitialFocus>
|
||||
) : isNameEditable ? (
|
||||
<View
|
||||
<Button
|
||||
bare
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
'& .hover-visible': {
|
||||
opacity: 0,
|
||||
transition: 'opacity .25s'
|
||||
},
|
||||
'&:hover .hover-visible': {
|
||||
opacity: 1
|
||||
}
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginLeft: -5,
|
||||
marginTop: -5,
|
||||
backgroundColor: 'transparent',
|
||||
'& svg': { display: 'none' },
|
||||
'&:hover svg': { display: 'unset' }
|
||||
}}
|
||||
onClick={() => onExposeName(true)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginRight: 5,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
{accountName}
|
||||
</View>
|
||||
{accountName}
|
||||
|
||||
<NotesButton id={`account-${account.id}`} />
|
||||
<Button
|
||||
bare
|
||||
className="hover-visible"
|
||||
onClick={() => onExposeName(true)}
|
||||
>
|
||||
<Pencil1
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
color: colors.n8
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
<Pencil1
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
marginLeft: 5,
|
||||
color: colors.n4
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<View
|
||||
style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}
|
||||
|
@ -746,7 +756,7 @@ const AccountHeader = React.memo(
|
|||
}
|
||||
style={{ color: 'currentColor', marginRight: 4 }}
|
||||
/>{' '}
|
||||
Sync
|
||||
{t('general.sync')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -755,7 +765,7 @@ const AccountHeader = React.memo(
|
|||
height={13}
|
||||
style={{ color: 'currentColor', marginRight: 4 }}
|
||||
/>{' '}
|
||||
Import
|
||||
{t('general.import')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
@ -767,7 +777,7 @@ const AccountHeader = React.memo(
|
|||
height={10}
|
||||
style={{ color: 'inherit', marginRight: 3 }}
|
||||
/>{' '}
|
||||
Add New
|
||||
{t('general.addNew')}
|
||||
</Button>
|
||||
)}
|
||||
<View>
|
||||
|
@ -788,7 +798,7 @@ const AccountHeader = React.memo(
|
|||
}
|
||||
inputRef={searchInput}
|
||||
value={search}
|
||||
placeholder="Search"
|
||||
placeholder={t('general.search')}
|
||||
getStyle={focused => [
|
||||
{
|
||||
backgroundColor: 'transparent',
|
||||
|
@ -826,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' ? (
|
||||
|
@ -1205,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']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -1234,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')
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1352,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) {
|
||||
|
@ -1405,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 } }
|
||||
});
|
||||
};
|
||||
|
@ -1588,7 +1608,8 @@ class AccountInternal extends React.PureComponent {
|
|||
replaceModal,
|
||||
showExtraBalances,
|
||||
expandSplits,
|
||||
accountId
|
||||
accountId,
|
||||
t
|
||||
} = this.props;
|
||||
let {
|
||||
transactions,
|
||||
|
@ -1723,7 +1744,7 @@ class AccountInternal extends React.PureComponent {
|
|||
fontStyle: 'italic'
|
||||
}}
|
||||
>
|
||||
No transactions
|
||||
{t('general.noTransaction_other')}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
|
@ -1753,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>
|
||||
|
@ -336,7 +342,7 @@ function StatusCell({
|
|||
? colors.y5
|
||||
: selected
|
||||
? colors.b7
|
||||
: colors.n7
|
||||
: colors.n6
|
||||
};
|
||||
|
||||
function onSelect() {
|
||||
|
@ -362,8 +368,7 @@ function StatusCell({
|
|||
':focus': {
|
||||
border: '1px solid ' + props.color,
|
||||
boxShadow: `0 1px 2px ${props.color}`
|
||||
},
|
||||
cursor: isClearedField ? 'pointer' : 'default'
|
||||
}
|
||||
},
|
||||
|
||||
isChild && { visibility: 'hidden' }
|
||||
|
@ -529,6 +534,7 @@ export const Transaction = React.memo(function Transaction(props) {
|
|||
onCreatePayee,
|
||||
onToggleSplit
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
let dispatchSelected = useSelectedDispatch();
|
||||
|
||||
|
@ -897,7 +903,7 @@ export const Transaction = React.memo(function Transaction(props) {
|
|||
/>
|
||||
)}
|
||||
<Text style={{ fontStyle: 'italic', userSelect: 'none' }}>
|
||||
Split
|
||||
{t('general.split')}
|
||||
</Text>
|
||||
</View>
|
||||
</CellButton>
|
||||
|
@ -911,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}
|
||||
|
@ -937,7 +943,7 @@ export const Transaction = React.memo(function Transaction(props) {
|
|||
'name'
|
||||
)
|
||||
: transaction.id
|
||||
? 'Categorize'
|
||||
? t('general.categorize')
|
||||
: ''
|
||||
}
|
||||
exposed={focusedField === 'category'}
|
||||
|
@ -1050,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) {
|
||||
|
@ -1065,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>
|
||||
);
|
||||
|
@ -1136,6 +1144,7 @@ function NewTransaction({
|
|||
}) {
|
||||
const error = transactions[0].error;
|
||||
const isDeposit = transactions[0].amount > 0;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
|
@ -1193,7 +1202,7 @@ function NewTransaction({
|
|||
onClick={() => onClose()}
|
||||
data-testid="cancel-button"
|
||||
>
|
||||
Cancel
|
||||
{t('general.cancel')}
|
||||
</Button>
|
||||
{error ? (
|
||||
<TransactionError
|
||||
|
@ -1208,7 +1217,7 @@ function NewTransaction({
|
|||
onClick={onAdd}
|
||||
data-testid="add-button"
|
||||
>
|
||||
Add
|
||||
{t('general.add')}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
@ -1533,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);
|
||||
}
|
||||
|
@ -238,9 +240,9 @@ function ScheduleValue({ value }) {
|
|||
field="rule"
|
||||
data={schedules}
|
||||
describe={s => {
|
||||
let payeeId = s._payee;
|
||||
return payeeId
|
||||
? `${byId[payeeId].name} (${s.next_date})`
|
||||
let { payee } = extractScheduleConds(s._conditions);
|
||||
return payee
|
||||
? `${byId[payee.value].name} (${s.next_date})`
|
||||
: `Next: ${s.next_date}`;
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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';
|
||||
|
@ -6,63 +7,67 @@ import { colors } from 'loot-design/src/style';
|
|||
import AlertTriangle from 'loot-design/src/svg/v2/AlertTriangle';
|
||||
import CalendarIcon from 'loot-design/src/svg/v2/Calendar';
|
||||
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
|
||||
import CheckCircleHollow from 'loot-design/src/svg/v2/CheckCircleHollow';
|
||||
import EditSkull1 from 'loot-design/src/svg/v2/EditSkull1';
|
||||
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;
|
||||
break;
|
||||
case 'cleared':
|
||||
color = colors.g5;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CheckCircle1;
|
||||
title = t('status.scheduled');
|
||||
break;
|
||||
default:
|
||||
color = colors.n1;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CheckCircleHollow;
|
||||
Icon = CheckCircle1;
|
||||
title = status;
|
||||
break;
|
||||
}
|
||||
|
||||
return { color, backgroundColor, Icon };
|
||||
return { title, color, backgroundColor, Icon };
|
||||
}
|
||||
|
||||
export function StatusIcon({ status }) {
|
||||
|
@ -72,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={[
|
||||
|
@ -96,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;
|
|
@ -362,8 +362,8 @@ ipcMain.on('screenshot', () => {
|
|||
let width = 1100;
|
||||
|
||||
// This is for the main screenshot inside the frame
|
||||
clientWin.setSize(width, Math.floor(width * (427 / 623)));
|
||||
// clientWin.setSize(width, Math.floor(width * (495 / 700)));
|
||||
clientWin.setSize(width, (width * (427 / 623)) | 0);
|
||||
// clientWin.setSize(width, (width * (495 / 700)) | 0);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "Actual",
|
||||
"author": "Shift Reset LLC",
|
||||
"description": "A simple and powerful personal finance system",
|
||||
"version": "22.12.03",
|
||||
"version": "4.0.2",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"build": "electron-builder",
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
const actual = require('@actual-app/api/methods');
|
||||
const { amountToInteger } = require('@actual-app/api/utils');
|
||||
const AdmZip = require('adm-zip');
|
||||
const d = require('date-fns');
|
||||
const normalizePathSep = require('slash');
|
||||
const uuid = require('uuid');
|
||||
const AdmZip = require('adm-zip');
|
||||
const actual = require('@actual-app/api');
|
||||
const amountToInteger = actual.utils.amountToInteger;
|
||||
|
||||
// Utils
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"bin": "./index.js",
|
||||
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab4#readme",
|
||||
"dependencies": {
|
||||
"@actual-app/api": "*",
|
||||
"@actual-app/api": "^1.0.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"date-fns": "2.0.0-alpha.27",
|
||||
"slash": "3.0.0",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
const actual = require('@actual-app/api/methods');
|
||||
const d = require('date-fns');
|
||||
const uuid = require('uuid');
|
||||
const actual = require('@actual-app/api');
|
||||
|
||||
function amountFromYnab(amount) {
|
||||
// ynabs multiplies amount by 1000 and actual by 100
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"bin": "./index.js",
|
||||
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab5#readme",
|
||||
"dependencies": {
|
||||
"@actual-app/api": "*",
|
||||
"@actual-app/api": "^1.0.0",
|
||||
"date-fns": "2.0.0-alpha.27",
|
||||
"uuid": "3.3.2"
|
||||
}
|
||||
|
|
|
@ -35,31 +35,31 @@ async function init() {
|
|||
for (let i = 0; i < 100; i++) {
|
||||
if (Math.random() < 0.02) {
|
||||
let parent = {
|
||||
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
|
||||
amount: Math.floor(Math.random() * 10000),
|
||||
date: '2020-01-' + pad((Math.random() * 30) | 0),
|
||||
amount: (Math.random() * 10000) | 0,
|
||||
account: accounts[0].id,
|
||||
notes: 'foo'
|
||||
};
|
||||
db.insertTransaction(parent);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
amount: (Math.random() * 1000) | 0
|
||||
})
|
||||
);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
amount: (Math.random() * 1000) | 0
|
||||
})
|
||||
);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
amount: (Math.random() * 1000) | 0
|
||||
})
|
||||
);
|
||||
} else {
|
||||
db.insertTransaction({
|
||||
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
|
||||
amount: Math.floor(Math.random() * 10000),
|
||||
date: '2020-01-' + pad((Math.random() * 30) | 0),
|
||||
amount: (Math.random() * 10000) | 0,
|
||||
account: accounts[0].id
|
||||
});
|
||||
}
|
||||
|
|
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",
|
||||
|
|
|
@ -24,12 +24,21 @@ export function applyBudgetAction(month, type, args) {
|
|||
case 'set-3-avg':
|
||||
await send('budget/set-3month-avg', { month });
|
||||
break;
|
||||
case 'set-all-future':
|
||||
await send('budget/set-all-future', { startMonth: month });
|
||||
break;
|
||||
case 'hold':
|
||||
await send('budget/hold-for-next-month', {
|
||||
month,
|
||||
amount: args.amount
|
||||
});
|
||||
break;
|
||||
case 'hold-all-future':
|
||||
await send('budget/hold-for-future-months', {
|
||||
startMonth: month,
|
||||
amount: args.amount
|
||||
});
|
||||
break;
|
||||
case 'reset-hold':
|
||||
await send('budget/reset-hold', { month });
|
||||
break;
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
import q from '../shared/query';
|
||||
import { currencyToAmount, amountToInteger } from '../shared/util';
|
||||
|
||||
function isInteger(num) {
|
||||
return (num | 0) === num;
|
||||
}
|
||||
|
||||
export function getAccountFilter(accountId, field = 'account') {
|
||||
if (accountId) {
|
||||
if (accountId === 'budgeted') {
|
||||
|
@ -78,7 +82,7 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
|
|||
amount: { $transform: '$abs', $eq: amountToInteger(amount) }
|
||||
},
|
||||
amount != null &&
|
||||
Number.isInteger(amount) && {
|
||||
isInteger(amount) && {
|
||||
amount: {
|
||||
$transform: { $abs: { $idiv: ['$', 100] } },
|
||||
$eq: amount
|
||||
|
|
|
@ -93,7 +93,7 @@ function initBasicServer(delay) {
|
|||
function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) {
|
||||
let data = [];
|
||||
for (let i = 0; i < dataLength; i++) {
|
||||
data.push({ id: i, date: subDays('2020-05-01', Math.floor(i / 5)) });
|
||||
data.push({ id: i, date: subDays('2020-05-01', (i / 5) | 0) });
|
||||
}
|
||||
|
||||
initServer({
|
||||
|
|
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}}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import * as monthUtils from '../shared/months';
|
|||
import q from '../shared/query';
|
||||
|
||||
function pickRandom(list) {
|
||||
return list[Math.floor(Math.random() * list.length) % list.length];
|
||||
return list[((Math.random() * list.length) | 0) % list.length];
|
||||
}
|
||||
|
||||
function number(start, end) {
|
||||
|
@ -19,7 +19,7 @@ function number(start, end) {
|
|||
}
|
||||
|
||||
function integer(start, end) {
|
||||
return Math.round(number(start, end));
|
||||
return number(start, end) | 0;
|
||||
}
|
||||
|
||||
function findMin(items, field) {
|
||||
|
@ -104,13 +104,13 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), Math.floor(i / 3)),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i / 3) | 0),
|
||||
category: category.id
|
||||
};
|
||||
transactions.push(transaction);
|
||||
|
||||
if (Math.random() < 0.2) {
|
||||
let a = Math.round(transaction.amount / 3);
|
||||
let a = (transaction.amount / 3) | 0;
|
||||
let pick = () =>
|
||||
payee === incomePayee
|
||||
? incomeGroup.categories.find(c => c.name === 'Income').id
|
||||
|
@ -244,7 +244,7 @@ async function fillChecking(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), i * 2),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i * 2) | 0),
|
||||
category: category.id
|
||||
});
|
||||
}
|
||||
|
@ -334,7 +334,7 @@ async function fillSavings(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), i * 5),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i * 5) | 0),
|
||||
category: category.id
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ export function generateAccount(name, isConnected, type, offbudget) {
|
|||
return {
|
||||
id: uuid.v4Sync(),
|
||||
name,
|
||||
balance_current: isConnected ? Math.floor(Math.random() * 100000) : null,
|
||||
bank: isConnected ? Math.floor(Math.random() * 10000) : null,
|
||||
bankId: isConnected ? Math.floor(Math.random() * 10000) : null,
|
||||
balance_current: isConnected ? (Math.random() * 100000) | 0 : null,
|
||||
bank: isConnected ? (Math.random() * 10000) | 0 : null,
|
||||
bankId: isConnected ? (Math.random() * 10000) | 0 : null,
|
||||
bankName: isConnected ? 'boa' : null,
|
||||
type: type || 'checking',
|
||||
offbudget: offbudget ? 1 : 0,
|
||||
|
@ -54,7 +54,7 @@ function _generateTransaction(data) {
|
|||
const id = data.id || uuid.v4Sync();
|
||||
return {
|
||||
id: id,
|
||||
amount: data.amount || Math.floor(Math.random() * 10000 - 7000),
|
||||
amount: data.amount || (Math.random() * 10000 - 7000) | 0,
|
||||
payee: data.payee || (Math.random() < 0.9 ? 'payed-to' : 'guy'),
|
||||
notes:
|
||||
Math.random() < 0.1 ? 'A really long note that should overflow' : 'Notes',
|
||||
|
|
|
@ -456,7 +456,7 @@ function compileLiteral(value) {
|
|||
} else if (typeof value === 'boolean') {
|
||||
return typed(value ? 1 : 0, 'boolean', { literal: true });
|
||||
} else if (typeof value === 'number') {
|
||||
return typed(value, Number.isInteger(value) ? 'integer' : 'float', {
|
||||
return typed(value, (value | 0) === value ? 'integer' : 'float', {
|
||||
literal: true
|
||||
});
|
||||
} else if (Array.isArray(value)) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export function convertInputType(value, type) {
|
|||
}
|
||||
return value;
|
||||
case 'integer':
|
||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
if (typeof value === 'number' && (value | 0) === value) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error("Can't convert to integer: " + JSON.stringify(value));
|
||||
|
|
|
@ -91,7 +91,7 @@ function expectTransactionOrder(data, fields) {
|
|||
}
|
||||
|
||||
async function expectPagedData(query, numTransactions, allData) {
|
||||
let pageCount = Math.max(Math.floor(numTransactions / 3), 3);
|
||||
let pageCount = Math.max((numTransactions / 3) | 0, 3);
|
||||
let pagedData = [];
|
||||
let done = false;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as monthUtils from '../../shared/months';
|
||||
import { safeNumber } from '../../shared/util';
|
||||
import * as db from '../db';
|
||||
import * as prefs from '../prefs';
|
||||
import * as sheet from '../sheet';
|
||||
|
@ -7,7 +6,7 @@ import { batchMessages } from '../sync';
|
|||
|
||||
async function getSheetValue(sheetName, cell) {
|
||||
const node = await sheet.getCell(sheetName, cell);
|
||||
return safeNumber(typeof node.value === 'number' ? node.value : 0);
|
||||
return typeof node.value === 'number' ? node.value : 0;
|
||||
}
|
||||
|
||||
// We want to only allow the positive movement of money back and
|
||||
|
@ -72,7 +71,9 @@ export function getBudget({ category, month }) {
|
|||
}
|
||||
|
||||
export function setBudget({ category, month, amount }) {
|
||||
amount = safeNumber(typeof amount === 'number' ? amount : 0);
|
||||
if (typeof amount !== 'number') {
|
||||
amount = 0;
|
||||
}
|
||||
const table = getBudgetTable();
|
||||
|
||||
let existing = db.firstSync(
|
||||
|
@ -184,12 +185,32 @@ export async function set3MonthAvg({ month }) {
|
|||
'sum-amount-' + cat.id
|
||||
);
|
||||
|
||||
const avg = Math.round((spent1 + spent2 + spent3) / 3);
|
||||
const avg = ((spent1 + spent2 + spent3) / 3) | 0;
|
||||
setBudget({ category: cat.id, month, amount: -avg });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function setAllFuture({ startMonth }) {
|
||||
if (!isReflectBudget()) {
|
||||
throw new Error('setAllFuture only applies to report budget type');
|
||||
}
|
||||
let table = getBudgetTable();
|
||||
let budgetData = await getBudgetData(table, dbMonth(startMonth));
|
||||
let months = getAllMonths(monthUtils.addMonths(startMonth, 1));
|
||||
|
||||
batchMessages(() => {
|
||||
for (let month of months) {
|
||||
budgetData.forEach(budget => {
|
||||
if (budget.is_income === 1 && !isReflectBudget()) {
|
||||
return;
|
||||
}
|
||||
setBudget({ category: budget.category, month, amount: budget.amount });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function holdForNextMonth({ month, amount }) {
|
||||
let row = await db.first(
|
||||
'SELECT buffered FROM zero_budget_months WHERE id = ?',
|
||||
|
@ -212,6 +233,18 @@ export async function holdForNextMonth({ month, amount }) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export async function holdForFutureMonths({ startMonth, amount }) {
|
||||
let months = getAllMonths(startMonth);
|
||||
|
||||
await batchMessages(async () => {
|
||||
for (let month of months) {
|
||||
if (!(await holdForNextMonth({ month, amount }))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetHold({ month }) {
|
||||
await setBuffer(month, 0);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,15 @@ app.method(
|
|||
);
|
||||
app.method('budget/set-zero', mutator(undoable(actions.setZero)));
|
||||
app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg)));
|
||||
app.method('budget/set-all-future', mutator(undoable(actions.setAllFuture)));
|
||||
app.method(
|
||||
'budget/hold-for-next-month',
|
||||
mutator(undoable(actions.holdForNextMonth))
|
||||
);
|
||||
app.method(
|
||||
'budget/hold-for-future-months',
|
||||
mutator(undoable(actions.holdForFutureMonths))
|
||||
);
|
||||
app.method('budget/reset-hold', mutator(undoable(actions.resetHold)));
|
||||
app.method(
|
||||
'budget/cover-overspending',
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { safeNumber } from '../../shared/util';
|
||||
import * as sheet from '../sheet';
|
||||
import { number, sumAmounts } from './util';
|
||||
|
||||
|
@ -26,17 +25,17 @@ export async function createCategory(cat, sheetName, prevSheetName) {
|
|||
],
|
||||
run: (budgeted, sumAmount, prevCarryover, prevLeftover) => {
|
||||
if (cat.is_income) {
|
||||
return safeNumber(
|
||||
return (
|
||||
number(budgeted) -
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
);
|
||||
}
|
||||
|
||||
return safeNumber(
|
||||
return (
|
||||
number(budgeted) +
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -51,7 +50,7 @@ export async function createCategory(cat, sheetName, prevSheetName) {
|
|||
refresh: true,
|
||||
run: (budgeted, sumAmount, carryover) => {
|
||||
return carryover
|
||||
? Math.max(0, safeNumber(number(budgeted) + number(sumAmount)))
|
||||
? Math.max(0, number(budgeted) + number(sumAmount))
|
||||
: sumAmount;
|
||||
}
|
||||
});
|
||||
|
@ -110,7 +109,7 @@ export function createSummary(groups, categories, sheetName) {
|
|||
initialValue: 0,
|
||||
dependencies: ['total-income', 'total-spent'],
|
||||
run: (income, spent) => {
|
||||
return safeNumber(income - -spent);
|
||||
return income - -spent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as monthUtils from '../../shared/months';
|
||||
import { safeNumber } from '../../shared/util';
|
||||
import * as sheet from '../sheet';
|
||||
import { number, sumAmounts, flatten2, unflatten2 } from './util';
|
||||
|
||||
|
@ -52,10 +51,10 @@ export function createCategory(cat, sheetName, prevSheetName) {
|
|||
`${prevSheetName}!leftover-pos-${cat.id}`
|
||||
],
|
||||
run: (budgeted, spent, prevCarryover, prevLeftover, prevLeftoverPos) => {
|
||||
return safeNumber(
|
||||
return (
|
||||
number(budgeted) +
|
||||
number(spent) +
|
||||
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
|
||||
number(spent) +
|
||||
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -79,7 +78,7 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
sheet.get().createDynamic(sheetName, 'from-last-month', {
|
||||
initialValue: 0,
|
||||
dependencies: [`${prevSheetName}!to-budget`, `${prevSheetName}!buffered`],
|
||||
run: (toBudget, buffered) => safeNumber(number(toBudget) + number(buffered))
|
||||
run: (toBudget, buffered) => number(toBudget) + number(buffered)
|
||||
});
|
||||
|
||||
// Alias the group income total to `total-income`
|
||||
|
@ -92,8 +91,7 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
sheet.get().createDynamic(sheetName, 'available-funds', {
|
||||
initialValue: 0,
|
||||
dependencies: ['total-income', 'from-last-month'],
|
||||
run: (income, fromLastMonth) =>
|
||||
safeNumber(number(income) + number(fromLastMonth))
|
||||
run: (income, fromLastMonth) => number(income) + number(fromLastMonth)
|
||||
});
|
||||
|
||||
sheet.get().createDynamic(sheetName, 'last-month-overspent', {
|
||||
|
@ -106,14 +104,12 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
),
|
||||
run: (...data) => {
|
||||
data = unflatten2(data);
|
||||
return safeNumber(
|
||||
data.reduce((total, [leftover, carryover]) => {
|
||||
if (carryover) {
|
||||
return total;
|
||||
}
|
||||
return total + Math.min(0, number(leftover));
|
||||
}, 0)
|
||||
);
|
||||
return data.reduce((total, [leftover, carryover]) => {
|
||||
if (carryover) {
|
||||
return total;
|
||||
}
|
||||
return total + Math.min(0, number(leftover));
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -139,11 +135,11 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
'buffered'
|
||||
],
|
||||
run: (available, lastOverspent, totalBudgeted, buffered) => {
|
||||
return safeNumber(
|
||||
return (
|
||||
number(available) +
|
||||
number(lastOverspent) +
|
||||
number(totalBudgeted) -
|
||||
number(buffered)
|
||||
number(lastOverspent) +
|
||||
number(totalBudgeted) -
|
||||
number(buffered)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { safeNumber } from '../../shared/util';
|
||||
import { number } from '../spreadsheet/globals';
|
||||
|
||||
export { number } from '../spreadsheet/globals';
|
||||
|
||||
export function sumAmounts(...amounts) {
|
||||
return safeNumber(
|
||||
amounts.reduce((total, amount) => {
|
||||
return total + number(amount);
|
||||
}, 0)
|
||||
);
|
||||
return amounts.reduce((total, amount) => {
|
||||
return total + number(amount);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function flatten2(arr) {
|
||||
|
|
|
@ -22,7 +22,7 @@ export function keyToTimestamp(key) {
|
|||
|
||||
export function insert(trie, timestamp) {
|
||||
let hash = timestamp.hash();
|
||||
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
|
||||
let key = Number((timestamp.millis() / 1000 / 60) | 0).toString(3);
|
||||
|
||||
trie = Object.assign({}, trie, { hash: trie.hash ^ hash });
|
||||
return insertKey(trie, key, hash);
|
||||
|
|
|
@ -34,7 +34,7 @@ export function shoveSortOrders(items, targetId) {
|
|||
} else {
|
||||
if (target.sort_order - (before ? before.sort_order : 0) <= 2) {
|
||||
let next = to;
|
||||
let order = Math.floor(items[next].sort_order) + SORT_INCREMENT;
|
||||
let order = (items[next].sort_order | 0) + SORT_INCREMENT;
|
||||
while (next < items.length) {
|
||||
// No need to update it if it's already greater than the current
|
||||
// order. This can happen because there may already be large
|
||||
|
|
|
@ -171,7 +171,7 @@ function shuffle(arr) {
|
|||
let shuffled = new Array(src.length);
|
||||
let item;
|
||||
while ((item = src.pop())) {
|
||||
let idx = Math.floor(Math.random() * shuffled.length);
|
||||
let idx = (Math.random() * shuffled.length) | 0;
|
||||
if (shuffled[idx]) {
|
||||
src.push(item);
|
||||
} else {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -199,5 +199,5 @@ export function makeValue(value, cond) {
|
|||
}
|
||||
|
||||
export function getApproxNumberThreshold(number) {
|
||||
return Math.round(Math.abs(number) * 0.075);
|
||||
return (Math.abs(number) * 0.075) | 0;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -221,7 +276,7 @@ export function extractScheduleConds(conditions) {
|
|||
|
||||
export function getScheduledAmount(amount) {
|
||||
if (amount && typeof amount !== 'number') {
|
||||
return Math.round((amount.num1 + amount.num2) / 2);
|
||||
return ((amount.num1 + amount.num2) / 2) | 0;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -298,30 +298,6 @@ export function getNumberFormat() {
|
|||
|
||||
setNumberFormat('comma-dot');
|
||||
|
||||
// Number utilities
|
||||
|
||||
// We dont use `Number.MAX_SAFE_NUMBER` and such here because those
|
||||
// numbers are so large that it's not safe to convert them to floats
|
||||
// (i.e. N / 100). For example, `9007199254740987 / 100 ===
|
||||
// 90071992547409.88`. While the internal arithemetic would be correct
|
||||
// because we always do that on numbers, the app would potentially
|
||||
// display wrong numbers. Instead of `2**53` we use `2**51` which
|
||||
// gives division more room to be correct
|
||||
const MAX_SAFE_NUMBER = 2 ** 51 - 1;
|
||||
const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER;
|
||||
|
||||
export function safeNumber(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error('safeNumber: number is not an integer: ' + value);
|
||||
}
|
||||
if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) {
|
||||
throw new Error(
|
||||
"safeNumber: can't safely perform arithmetic with number: " + value
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function toRelaxedNumber(value) {
|
||||
return integerToAmount(currencyToInteger(value) || 0);
|
||||
}
|
||||
|
@ -331,7 +307,8 @@ export function toRelaxedInteger(value) {
|
|||
}
|
||||
|
||||
export function integerToCurrency(n) {
|
||||
return numberFormat.formatter.format(safeNumber(n) / 100);
|
||||
// Awesome
|
||||
return numberFormat.formatter.format(n / 100);
|
||||
}
|
||||
|
||||
export function amountToCurrency(n) {
|
||||
|
@ -363,7 +340,7 @@ export function amountToInteger(n) {
|
|||
}
|
||||
|
||||
export function integerToAmount(n) {
|
||||
return parseFloat((safeNumber(n) / 100).toFixed(2));
|
||||
return parseFloat((n / 100).toFixed(2));
|
||||
}
|
||||
|
||||
// This is used when the input format could be anything (from
|
||||
|
|
|
@ -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
|
||||
|
@ -368,6 +368,10 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
{
|
||||
name: 'set-3-avg',
|
||||
text: 'Set budgets to 3 month avg'
|
||||
},
|
||||
{
|
||||
name: 'set-all-future',
|
||||
text: 'Apply to all future budgets'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -204,6 +204,10 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
|
|||
name: 'buffer',
|
||||
text: 'Hold for next month'
|
||||
},
|
||||
{
|
||||
name: 'buffer-future',
|
||||
text: 'Hold for all future months'
|
||||
},
|
||||
{
|
||||
name: 'reset-buffer',
|
||||
text: "Reset next month's buffer"
|
||||
|
@ -220,6 +224,14 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{state.menuOpen === 'buffer-future' && (
|
||||
<HoldTooltip
|
||||
onClose={() => setState({ menuOpen: null })}
|
||||
onSubmit={amount => {
|
||||
onBudgetAction(month, 'hold-all-future', { amount });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{state.menuOpen === 'transfer' && (
|
||||
<TransferTooltip
|
||||
initialAmountName="leftover"
|
||||
|
@ -259,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;
|
||||
|
||||
|
@ -325,7 +340,7 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
currentMonth === month && { textDecoration: 'underline' }
|
||||
])}
|
||||
>
|
||||
{monthUtils.format(month, 'MMMM')}
|
||||
{monthUtils.nonLocalizedFormat(month, 'MMMM')}
|
||||
</div>
|
||||
|
||||
<View
|
||||
|
|
|
@ -938,6 +938,10 @@ export function ModalButtons({
|
|||
style
|
||||
]}
|
||||
>
|
||||
{/* Add a dummy button first so that when a user
|
||||
presses "enter" they do a normal submit, instead of
|
||||
activating the back button */}
|
||||
<Button data-hidden={true} style={{ display: 'none' }} />
|
||||
{leftContent}
|
||||
<View style={{ flex: 1 }} />
|
||||
{children}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -116,7 +116,7 @@ function CloseAccount({
|
|||
<View>
|
||||
<P>
|
||||
This account has a balance of{' '}
|
||||
<strong>{integerToCurrency(balance)}</strong>. To close
|
||||
<strong>${integerToCurrency(balance)}</strong>. To close
|
||||
this account, select a different account to transfer this
|
||||
balance to:
|
||||
</P>
|
||||
|
|
|
@ -146,9 +146,7 @@ function CreateLocalAccount({ modalProps, actions, history }) {
|
|||
)}
|
||||
|
||||
<ModalButtons>
|
||||
<Button onClick={() => modalProps.onBack()} type="button">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => modalProps.onBack()}>Back</Button>
|
||||
<Button primary style={{ marginLeft: 10 }}>
|
||||
Create
|
||||
</Button>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -397,9 +397,6 @@ const MenuButton = withRouter(function MenuButton({ history }) {
|
|||
case 'settings':
|
||||
history.push('/settings');
|
||||
break;
|
||||
case 'help':
|
||||
window.open('https://actualbudget.github.io/docs', '_blank');
|
||||
break;
|
||||
case 'close':
|
||||
dispatch(closeBudget());
|
||||
break;
|
||||
|
@ -414,7 +411,6 @@ const MenuButton = withRouter(function MenuButton({ history }) {
|
|||
{ name: 'repair-splits', text: 'Repair split transactions' },
|
||||
Menu.line,
|
||||
{ name: 'settings', text: 'Settings' },
|
||||
{ name: 'help', text: 'Help' },
|
||||
{ name: 'close', text: 'Close File' }
|
||||
];
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function useSheetValue(binding, onChange) {
|
|||
let spreadsheet = useContext(SpreadsheetContext);
|
||||
let [result, setResult] = useState({
|
||||
name: sheetName + '!' + binding.name,
|
||||
value: binding.value === undefined ? null : binding.value,
|
||||
value: binding.value,
|
||||
query: binding.query
|
||||
});
|
||||
let latestOnChange = useRef(onChange);
|
||||
|
|
|
@ -7,7 +7,7 @@ let groups = ['y', 'r', 'b', 'n', 'g', 'p'];
|
|||
let colors = {};
|
||||
|
||||
list.forEach((color, idx) => {
|
||||
const group = Math.floor(idx / 11);
|
||||
const group = (idx / 11) | 0;
|
||||
const n = idx % 11;
|
||||
|
||||
colors[groups[group] + (n + 1)] = color;
|
||||
|
|
|
@ -24,11 +24,11 @@ global.Date.now = () => 123456789;
|
|||
|
||||
let seqId = 1;
|
||||
uuid.v4 = function() {
|
||||
return Promise.resolve('testing-uuid-' + Math.floor(Math.random() * 1000000));
|
||||
return Promise.resolve('testing-uuid-' + ((Math.random() * 1000000) | 0));
|
||||
};
|
||||
|
||||
uuid.v4Sync = function() {
|
||||
return 'testing-uuid-' + Math.floor(Math.random() * 1000000);
|
||||
return 'testing-uuid-' + ((Math.random() * 1000000) | 0);
|
||||
};
|
||||
|
||||
global.__resetWorld = () => {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const SvgCheckCircleHollow = props => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: '#242134',
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M 12 0 C 1.3084197 0 -4.0435475 12.925204 3.515625 20.484375 C 11.074797 28.043547 24 22.69158 24 12 C 23.992285 5.3757944 18.624205 0.0077147446 12 0 z M 12.009766 1.9882812 C 17.531104 1.9947115 22.005288 6.4688953 22.011719 11.990234 C 22.011719 20.90177 11.238144 25.363144 4.9375 19.0625 C -1.3631434 12.761856 3.0982293 1.9882812 12.009766 1.9882812 z M 18.244141 6.5761719 A 1 1 0 0 0 17.316406 7.0175781 L 11.089844 15.46875 L 7.0136719 12.207031 A 1.0004882 1.0004882 0 1 0 5.7636719 13.769531 L 10.652344 17.677734 A 1.011 1.011 0 0 0 12.082031 17.488281 L 18.927734 8.1992188 A 1 1 0 0 0 18.244141 6.5761719 z "
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgCheckCircleHollow;
|
|
@ -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)}>
|
||||
|
@ -330,6 +333,11 @@ class Budget extends React.Component {
|
|||
case 3:
|
||||
this.onBudgetAction('set-3-avg');
|
||||
break;
|
||||
case 4:
|
||||
if (budgetType === 'report') {
|
||||
this.onBudgetAction('set-all-future');
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue