Compare commits

..

88 Commits
2.1.1 ... 2.2.2

Author SHA1 Message Date
vabene1111
2cba0e18af Merge branch 'develop' 2025-09-19 16:20:56 +02:00
vabene1111
ec6e81316a fixed unwanted redirect to start page 2025-09-19 16:20:48 +02:00
vabene1111
b72897b222 Merge branch 'develop' 2025-09-18 18:17:58 +02:00
vabene1111
bca1ebbf99 various fixes 2025-09-18 18:10:58 +02:00
vabene1111
f0342d4568 fixed some tests 2025-09-17 18:04:02 +02:00
vabene1111
81f62de500 Merge pull request #4050 from TandoorRecipes/revert-4040-ipv6
Revert "feat: make nginx respect ipv6 disable fixes #3996"
2025-09-17 16:20:13 +02:00
vabene1111
f783949a61 Revert "feat: make nginx respect ipv6 disable fixes #3996" 2025-09-17 16:20:03 +02:00
vabene1111
820fad1b5c Merge pull request #4040 from wilmardo/ipv6
feat: make nginx respect ipv6 disable fixes #3996
2025-09-17 16:19:44 +02:00
vabene1111
1169abd942 mealie docs update 2025-09-17 07:55:29 +02:00
vabene1111
48e175f58f mealie importer working with settings 2025-09-17 07:50:56 +02:00
wilmardo
5450e18342 feat: make nginx respect ipv6 disable fixes #3996
Signed-off-by: wilmardo <info@wilmardenouden.nl>
2025-09-16 15:01:54 +02:00
vabene1111
ea590f8e49 mealie importer options 2025-09-16 08:00:22 +02:00
vabene1111
13626ca11b mealie importer improvements 2025-09-16 07:48:58 +02:00
vabene1111
f53fe1e3c4 import comments 2025-09-15 22:12:52 +02:00
vabene1111
d177316b47 mealie 1.0 importer WIP 2025-09-15 22:05:15 +02:00
vabene1111
338db1fac2 fixed default properties view 2025-09-15 21:26:51 +02:00
vabene1111
377619473c small fixes 2025-09-15 07:49:17 +02:00
vabene1111
000962c5bb moved create space to its own file 2025-09-14 12:05:09 +02:00
vabene1111
9228c1d59f fixed ai import layout 2025-09-14 11:55:58 +02:00
vabene1111
27007de7a0 improved start page with little recipes 2025-09-14 11:12:46 +02:00
vabene1111
29c99b66a1 fixed thermomix special symbol parser 2025-09-14 11:11:22 +02:00
vabene1111
bc179f430d Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-09-14 11:03:53 +02:00
vabene1111
58c412ad95 space and user space api updates 2025-09-14 09:57:57 +02:00
vabene1111
4f248afe76 overhauld space management and settings system 2025-09-14 08:48:49 +02:00
vabene1111
f722d24eaa wip space editor 2025-09-14 07:37:31 +02:00
vabene1111
723b74509f moved space stuff to database and reworked invite link backend logic 2025-09-11 21:44:40 +02:00
vabene1111
ad4b1393dd various improvements 2025-09-11 18:58:44 +02:00
vabene1111
04bab7072c WIP stepper and language select component 2025-09-11 07:55:06 +02:00
vabene1111
6391cee9eb Merge pull request #4025 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-7.1.5
Bump vite from 7.1.4 to 7.1.5 in /vue3
2025-09-11 07:07:50 +02:00
vabene1111
14884fc0d4 Merge pull request #3985 from TandoorRecipes/dependabot/github_actions/awalsh128/cache-apt-pkgs-action-1.5.3
Bump awalsh128/cache-apt-pkgs-action from 1.5.1 to 1.5.3
2025-09-11 07:07:44 +02:00
vabene1111
f2191f79dd auto space creation and redirect to welcome page 2025-09-10 22:18:09 +02:00
vabene1111
c2533d9ea2 add migration shortcut 2025-09-10 21:28:10 +02:00
dependabot[bot]
db72fdb1bb Bump vite from 7.1.4 to 7.1.5 in /vue3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.4 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 19:03:01 +00:00
dependabot[bot]
78252662cb Bump awalsh128/cache-apt-pkgs-action from 1.5.1 to 1.5.3
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.5.1...v1.5.3)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 18:59:37 +00:00
vabene1111
4e078bf477 updated to django 5 2025-09-10 20:56:47 +02:00
vabene1111
2e9e226fe0 Merge pull request #3994 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-plugin-pwa-1.0.3
Bump vite-plugin-pwa from 1.0.2 to 1.0.3 in /vue3
2025-09-10 20:44:54 +02:00
dependabot[bot]
18cfbd80ab Bump vite-plugin-pwa from 1.0.2 to 1.0.3 in /vue3
Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v1.0.2...v1.0.3)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-version: 1.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 18:42:09 +00:00
vabene1111
4d284b4fff Merge pull request #3984 from TandoorRecipes/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-09-10 20:42:06 +02:00
vabene1111
b1128dd134 Merge pull request #3986 from TandoorRecipes/dependabot/pip/django-storages-1.14.6
Bump django-storages from 1.14.2 to 1.14.6
2025-09-10 20:41:46 +02:00
vabene1111
3aebf58406 Merge pull request #3987 from TandoorRecipes/dependabot/pip/djangorestframework-3.16.1
Bump djangorestframework from 3.15.2 to 3.16.1
2025-09-10 20:41:39 +02:00
vabene1111
f3816a77df Merge pull request #3988 from TandoorRecipes/dependabot/pip/django-prometheus-2.4.1
Bump django-prometheus from 2.3.1 to 2.4.1
2025-09-10 20:41:31 +02:00
vabene1111
e4183d79ab Merge pull request #3989 from TandoorRecipes/dependabot/pip/drf-spectacular-sidecar-2025.8.1
Bump drf-spectacular-sidecar from 2025.7.1 to 2025.8.1
2025-09-10 20:41:25 +02:00
vabene1111
f4aa1a083f Merge pull request #3990 from TandoorRecipes/dependabot/pip/python-dotenv-1.1.1
Bump python-dotenv from 1.0.0 to 1.1.1
2025-09-10 20:41:18 +02:00
vabene1111
ed5508b576 Merge pull request #3991 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue-i18n-11.1.11
Bump vue-i18n from 11.1.10 to 11.1.11 in /vue3
2025-09-10 20:40:57 +02:00
vabene1111
040e247487 Merge pull request #3992 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue-tsc-3.0.6
Bump vue-tsc from 2.2.10 to 3.0.6 in /vue3
2025-09-10 20:40:50 +02:00
vabene1111
5d28c7b17d Merge pull request #3995 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vuetify-3.9.6
Bump vuetify from 3.9.3 to 3.9.6 in /vue3
2025-09-10 20:40:39 +02:00
vabene1111
15b2df07f2 Merge pull request #3993 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-7.1.3
Bump vite from 6.3.5 to 7.1.3 in /vue3
2025-09-10 20:40:26 +02:00
vabene1111
ed8f97e9e0 Merge branch 'develop'
# Conflicts:
#	vue3/src/locales/de.json
2025-09-10 20:36:33 +02:00
vabene1111
034f68fc28 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-09-10 20:34:55 +02:00
vabene1111
0158087a0b fixed ai test 2025-09-10 20:34:51 +02:00
vabene1111
cb6bfd741d Merge pull request #4023 from TandoorRecipes/dependabot/pip/django-4.2.24
Bump django from 4.2.22 to 4.2.24
2025-09-10 20:34:21 +02:00
vabene1111
afeee5f7cb fixed link for subpath setups 2025-09-10 20:16:22 +02:00
vabene1111
b43d6e08d4 food batch editor implementation 2025-09-10 16:52:35 +02:00
vabene1111
1188624376 food batch update dialog and first api functions 2025-09-10 07:54:42 +02:00
dependabot[bot]
9ac837c969 Bump django from 4.2.22 to 4.2.24
Bumps [django](https://github.com/django/django) from 4.2.22 to 4.2.24.
- [Commits](https://github.com/django/django/compare/4.2.22...4.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 4.2.24
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 02:19:20 +00:00
vabene1111
fc4b017d30 food batch update endpoint 2025-09-09 16:54:29 +02:00
vabene1111
4636ac28f9 ai system improvements 2025-09-09 16:30:54 +02:00
vabene1111
397912e87f WIP AI system 2025-09-09 14:58:32 +02:00
vabene1111
d0b860e623 shorter create message for model select 2025-09-09 13:36:05 +02:00
vabene1111
8a90ed1274 fixed ingredient parser error 2025-09-09 13:33:18 +02:00
vabene1111
163c2a53b6 fixed space overview 2025-09-09 07:55:31 +02:00
vabene1111
286d707347 lots of AI provider stuff 2025-09-09 07:54:59 +02:00
vabene1111
98d308aee9 fixed space overview 2025-09-09 07:54:45 +02:00
vabene1111
a7c5240227 ai provider system 2025-09-08 22:15:57 +02:00
vabene1111
75fcff8e70 font show sponsor link on hosted edition 2025-09-08 21:15:20 +02:00
vabene1111
2f27cf4deb fixed meal plan loading 2025-09-08 21:13:33 +02:00
vabene1111
686b595f45 fixed demo auto login 2025-09-08 20:55:20 +02:00
vabene1111
0f9f9e8f7c Merge pull request #4013 from tomtjes/fix-logo-font
Make logo consistent across end user systems
2025-09-08 20:15:19 +02:00
dependabot[bot]
7be7c5b954 Bump vite from 6.3.5 to 7.1.3 in /vue3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 7.1.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:53:43 +00:00
dependabot[bot]
0853a9ec64 Bump vuetify from 3.9.3 to 3.9.6 in /vue3
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.9.3 to 3.9.6.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.9.6/packages/vuetify)

---
updated-dependencies:
- dependency-name: vuetify
  dependency-version: 3.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:51:27 +00:00
dependabot[bot]
fa3daee965 Bump vue-tsc from 2.2.10 to 3.0.6 in /vue3
Bumps [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) from 2.2.10 to 3.0.6.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.0.6/packages/tsc)

---
updated-dependencies:
- dependency-name: vue-tsc
  dependency-version: 3.0.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:42:43 +00:00
vabene1111
e6abdf8cd4 fixed vite config 2025-09-08 08:38:46 +02:00
vabene1111
741e9eb370 plugin and hosted fixes 2025-09-08 08:31:01 +02:00
tomtjes
7db523d8c4 convert text to path 2025-09-07 19:34:11 +00:00
vabene1111
41f0060c43 basics of AI provider system 2025-09-05 21:36:43 +02:00
vabene1111
5572833f64 dont display 0 amount in ingredient table 2025-09-05 18:02:09 +02:00
vabene1111
780e441a3b fixed migration tree 2025-09-05 17:50:02 +02:00
vabene1111
c4fd2d0b4e fixed timer localization 2025-09-05 17:46:18 +02:00
vabene1111
1c6618f452 Merge pull request #3999 from icedieler/patch-1
Update nginx configuration for manual setup
2025-09-05 17:30:00 +02:00
vabene1111
8c96a75a1e basics of ai database 2025-09-05 14:50:10 +02:00
Matthias Lange
f099e2e5d3 Update nginx configuration for manual setup
Since v2 tandoor added user session tracking which requires the reverse proxy to add an extra header.

This change adds the `X-Forwarded-For` header to the example nginx configuration. This header fixes the issue described in #3943.
2025-09-03 10:22:49 +02:00
dependabot[bot]
774c05e76f Bump vue-i18n from 11.1.10 to 11.1.11 in /vue3
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.1.10 to 11.1.11.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.1.11/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 01:07:24 +00:00
dependabot[bot]
b08c39e284 Bump python-dotenv from 1.0.0 to 1.1.1
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.0.0 to 1.1.1.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.1.1)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:39 +00:00
dependabot[bot]
ae036cfa9a Bump drf-spectacular-sidecar from 2025.7.1 to 2025.8.1
Bumps [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) from 2025.7.1 to 2025.8.1.
- [Commits](https://github.com/tfranzel/drf-spectacular-sidecar/compare/2025.7.1...2025.8.1)

---
updated-dependencies:
- dependency-name: drf-spectacular-sidecar
  dependency-version: 2025.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:35 +00:00
dependabot[bot]
37628c1735 Bump django-prometheus from 2.3.1 to 2.4.1
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.3.1 to 2.4.1.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/django-commons/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/compare/v2.3.1...v2.4.1)

---
updated-dependencies:
- dependency-name: django-prometheus
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:31 +00:00
dependabot[bot]
530a6db35c Bump djangorestframework from 3.15.2 to 3.16.1
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.15.2 to 3.16.1.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.15.2...3.16.1)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-version: 3.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:28 +00:00
dependabot[bot]
2930093da0 Bump django-storages from 1.14.2 to 1.14.6
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.14.2 to 1.14.6.
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.14.2...1.14.6)

---
updated-dependencies:
- dependency-name: django-storages
  dependency-version: 1.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:24 +00:00
dependabot[bot]
b7e63a466b Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:02:38 +00:00
127 changed files with 6804 additions and 988 deletions

View File

@@ -21,7 +21,7 @@ jobs:
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get version number
id: get_version

View File

@@ -12,8 +12,8 @@ jobs:
python-version: ["3.12"]
node-version: ["22"]
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@v1.5.1
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
version: 1.0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository_owner == 'TandoorRecipes' && ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: 3.x

View File

@@ -15,14 +15,14 @@
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
<a href="https://app.tandoor.dev/e/demo-auto-login/" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>
<p align="center">
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>

View File

@@ -17,7 +17,7 @@ from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, Im
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, ConnectorConfig)
ViewLog, ConnectorConfig, AiProvider, AiLog)
admin.site.login = secure_admin_login(admin.site.login)
@@ -90,6 +90,20 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
admin.site.register(SearchPreference, SearchPreferenceAdmin)
class AiProviderAdmin(admin.ModelAdmin):
list_display = ('name', 'space', 'model',)
search_fields = ('name', 'space', 'model',)
admin.site.register(AiProvider, AiProviderAdmin)
class AiLogAdmin(admin.ModelAdmin):
list_display = ('ai_provider', 'function', 'credit_cost', 'created_by', 'created_at',)
admin.site.register(AiLog, AiLogAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
search_fields = ('name',)

View File

@@ -26,6 +26,7 @@ class ImportExportBase(forms.Form):
PAPRIKA = 'PAPRIKA'
NEXTCLOUD = 'NEXTCLOUD'
MEALIE = 'MEALIE'
MEALIE1 = 'MEALIE1'
CHOWDOWN = 'CHOWDOWN'
SAFFRON = 'SAFFRON'
CHEFTAP = 'CHEFTAP'
@@ -46,7 +47,7 @@ class ImportExportBase(forms.Form):
PDF = 'PDF'
GOURMET = 'GOURMET'
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'),
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (MEALIE1, 'Mealie1'), (CHOWDOWN, 'Chowdown'),
(SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'),
(DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
@@ -75,6 +76,11 @@ class ImportForm(ImportExportBase):
files = MultipleFileField(required=True)
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'),
required=False)
meal_plans = forms.BooleanField(required=False)
shopping_lists = forms.BooleanField(required=False)
nutrition_per_serving = forms.BooleanField(required=False) # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False)

View File

@@ -0,0 +1,83 @@
from decimal import Decimal
from django.utils import timezone
from django.db.models import Sum
from litellm import CustomLogger
from cookbook.models import AiLog
from recipes import settings
def get_monthly_token_usage(space):
"""
returns the number of credits the space has used in the current month
"""
token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
if token_usage is None:
token_usage = 0
return token_usage
def has_monthly_token(space):
"""
checks if the monthly credit limit has been exceeded
"""
return get_monthly_token_usage(space) < space.ai_credits_monthly
def can_perform_ai_request(space):
return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled
class AiCallbackHandler(CustomLogger):
space = None
user = None
ai_provider = None
def __init__(self, space, user, ai_provider):
super().__init__()
self.space = space
self.user = user
self.ai_provider = ai_provider
def log_pre_api_call(self, model, messages, kwargs):
pass
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
pass
def log_success_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def create_ai_log(self, kwargs, response_obj, start_time, end_time):
credit_cost = 0
credits_from_balance = False
if self.ai_provider.log_credit_cost:
credit_cost = kwargs.get("response_cost", 0) * 100
if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
if remaining_balance < 0:
remaining_balance = 0
if settings.HOSTED:
self.space.ai_enabled = False
self.space.ai_credits_balance = remaining_balance
credits_from_balance = True
self.space.save()
AiLog.objects.create(
created_by=self.user,
space=self.space,
ai_provider=self.ai_provider,
start_time=start_time,
end_time=end_time,
input_tokens=response_obj['usage']['prompt_tokens'],
output_tokens=response_obj['usage']['completion_tokens'],
function=AiLog.F_FILE_IMPORT,
credit_cost=credit_cost,
credits_from_balance=credits_from_balance,
)

View File

@@ -0,0 +1,22 @@
def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
"""
given a model, the base and related field and the base and related ids, bulk create relation objects
"""
relation_objects = []
for b in base_ids:
for r in related_ids:
relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r}))
relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,))
def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete()
def remove_all_from_relation(relation_model, base_field_name, base_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete()
def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
remove_all_from_relation(relation_model, base_field_name, base_ids)
add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids)

View File

@@ -3,17 +3,19 @@ import inspect
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
from oauth2_provider.models import AccessToken
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import Recipe, ShareLink, UserSpace
import random
from cookbook.models import Recipe, ShareLink, UserSpace, Space
def get_allowed_groups(groups_required):
@@ -331,6 +333,25 @@ class CustomRecipePermission(permissions.BasePermission):
or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomAiProviderPermission(permissions.BasePermission):
"""
Custom permission class for the AiProvider api endpoint
users: can read all
admins: can read and write
superusers: can read and write + write providers without a space
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # user is either at least a user and the request is safe
return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser)
# editing of global providers allowed for superusers, space providers by admins and users can read only access
def has_object_permission(self, request, view, obj):
return ((obj.space is None and request.user.is_superuser)
or (obj.space == request.space and has_group_permission(request.user, ['admin']))
or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS))
class CustomUserPermission(permissions.BasePermission):
"""
Custom permission class for user api endpoint
@@ -437,3 +458,36 @@ class IsReadOnlyDRF(permissions.BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class IsCreateDRF(permissions.BasePermission):
message = 'You cannot interact with this object, you can only create'
def has_permission(self, request, view):
return request.method == 'POST'
def create_space_for_user(user, name=None):
with scopes_disabled():
if not name:
name = f"{user.username}'s Space"
if Space.objects.filter(name=name).exists():
name = f'{name} #{random.randrange(1, 10 ** 5)}'
created_space = Space(name=name,
created_by=user,
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,
space_setup_completed=False, )
created_space.save()
UserSpace.objects.filter(user=user).update(active=False)
user_space = UserSpace.objects.create(space=created_space, user=user, active=True)
user_space.groups.add(Group.objects.filter(name='admin').get())
return user_space

View File

@@ -319,10 +319,10 @@ def clean_instruction_string(instruction):
.replace("", _('reverse rotation')) \
.replace("", _('careful rotation')) \
.replace("", _('knead')) \
.replace("Andicken ", _('thicken')) \
.replace("Erwärmen ", _('warm up')) \
.replace("Fermentieren ", _('ferment')) \
.replace("Sous-vide ", _("sous-vide"))
.replace("", _('thicken')) \
.replace("", _('warm up')) \
.replace("", _('ferment')) \
.replace("", _("sous-vide"))
def parse_instructions(instructions):
@@ -403,6 +403,8 @@ def parse_servings_text(servings):
def parse_time(recipe_time):
if not recipe_time:
return 0
if type(recipe_time) not in [int, float]:
try:
recipe_time = float(re.search(r'\d+', recipe_time).group())

View File

@@ -1,8 +1,15 @@
from django.contrib.auth.models import Group
from django.http import HttpResponseRedirect
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from psycopg2.errors import UniqueViolation
from rest_framework.exceptions import AuthenticationFailed
import random
from cookbook.helper.permission_helper import create_space_for_user
from cookbook.models import Space, UserSpace
from cookbook.views import views
from recipes import settings
@@ -34,16 +41,28 @@ class ScopeMiddleware:
if request.path.startswith(prefix + '/switch-space/'):
return self.get_response(request)
with scopes_disabled():
if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path:
return views.space_overview(request)
if request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
user_space = request.user.userspace_set.filter(active=True).first()
if not user_space:
return views.space_overview(request)
if not user_space and request.user.userspace_set.count() > 0:
# if the users has a userspace but nothing is active, activate the first one
user_space = request.user.userspace_set.first()
if user_space:
user_space.active = True
user_space.save()
if not user_space:
if 'signup_token' in request.session:
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
else:
# if user does not yet have a space create one for him
user_space = create_space_for_user(request.user)
# TODO remove the need for this view
if user_space.groups.count() == 0 and not reverse('account_logout') in request.path:
return views.no_groups(request)

View File

@@ -26,6 +26,12 @@ class Integration:
files = None
export_type = None
ignored_recipes = []
import_log = None
import_duplicates = False
import_meal_plans = True
import_shopping_lists = True
nutrition_per_serving = False
def __init__(self, request, export_type):
"""
@@ -102,7 +108,7 @@ class Integration:
"""
return True
def do_import(self, files, il, import_duplicates):
def do_import(self, files, il, import_duplicates, meal_plans=True, shopping_lists=True, nutrition_per_serving=False):
"""
Imports given files
:param import_duplicates: if true duplicates are imported as well
@@ -111,6 +117,12 @@ class Integration:
:return: HttpResponseRedirect to the recipe search showing all imported recipes
"""
with scope(space=self.request.space):
self.import_log = il
self.import_duplicates = import_duplicates
self.import_meal_plans = meal_plans
self.import_shopping_lists = shopping_lists
self.nutrition_per_serving = nutrition_per_serving
try:
self.files = files
@@ -166,20 +178,24 @@ class Integration:
il.total_recipes = len(new_file_list)
file_list = new_file_list
for z in file_list:
try:
if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += self.get_recipe_processed_msg(recipe)
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
if isinstance(self, cookbook.integration.mealie1.Mealie1):
# since the mealie 1.0 export is a backup and not a classic recipe export we treat it a bit differently
recipes = self.get_recipe_from_file(import_zip)
else:
for z in file_list:
try:
if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += self.get_recipe_processed_msg(recipe)
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close()
elif '.json' in f['name'] or '.xml' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
data_list = self.split_recipe_file(f['file'])

View File

@@ -0,0 +1,342 @@
import json
import re
import traceback
import uuid
from decimal import Decimal
from io import BytesIO
from zipfile import ZipFile
from gettext import gettext as _
from django.db import transaction
from cookbook.helper import ingredient_parser
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step, Food, Unit, SupermarketCategory, PropertyType, Property, MealType, MealPlan, CookLog, ShoppingListEntry
class Mealie1(Integration):
"""
integration for mealie past version 1.0
"""
def get_recipe_from_file(self, file):
mealie_database = json.loads(BytesIO(file.read('database.json')).getvalue().decode("utf-8"))
self.import_log.total_recipes = len(mealie_database['recipes'])
self.import_log.msg += f"Importing {len(mealie_database["categories"]) + len(mealie_database["tags"])} tags and categories as keywords...\n"
self.import_log.save()
keywords_categories_dict = {}
for c in mealie_database['categories']:
if keyword := Keyword.objects.filter(name=c['name'], space=self.request.space).first():
keywords_categories_dict[c['id']] = keyword.pk
else:
keyword = Keyword.objects.create(name=c['name'], space=self.request.space)
keywords_categories_dict[c['id']] = keyword.pk
keywords_tags_dict = {}
for t in mealie_database['tags']:
if keyword := Keyword.objects.filter(name=t['name'], space=self.request.space).first():
keywords_tags_dict[t['id']] = keyword.pk
else:
keyword = Keyword.objects.create(name=t['name'], space=self.request.space)
keywords_tags_dict[t['id']] = keyword.pk
self.import_log.msg += f"Importing {len(mealie_database["multi_purpose_labels"])} multi purpose labels as supermarket categories...\n"
self.import_log.save()
supermarket_categories_dict = {}
for m in mealie_database['multi_purpose_labels']:
if supermarket_category := SupermarketCategory.objects.filter(name=m['name'], space=self.request.space).first():
supermarket_categories_dict[m['id']] = supermarket_category.pk
else:
supermarket_category = SupermarketCategory.objects.create(name=m['name'], space=self.request.space)
supermarket_categories_dict[m['id']] = supermarket_category.pk
self.import_log.msg += f"Importing {len(mealie_database["ingredient_foods"])} foods...\n"
self.import_log.save()
foods_dict = {}
for f in mealie_database['ingredient_foods']:
if food := Food.objects.filter(name=f['name'], space=self.request.space).first():
foods_dict[f['id']] = food.pk
else:
food = {'name': f['name'],
'plural_name': f['plural_name'],
'description': f['description'],
'space': self.request.space}
if f['label_id'] and f['label_id'] in supermarket_categories_dict:
food['supermarket_category_id'] = supermarket_categories_dict[f['label_id']]
food = Food.objects.create(**food)
if f['on_hand']:
food.onhand_users.add(self.request.user)
foods_dict[f['id']] = food.pk
self.import_log.msg += f"Importing {len(mealie_database["ingredient_units"])} units...\n"
self.import_log.save()
units_dict = {}
for u in mealie_database['ingredient_units']:
if unit := Unit.objects.filter(name=u['name'], space=self.request.space).first():
units_dict[u['id']] = unit.pk
else:
unit = Unit.objects.create(name=u['name'], plural_name=u['plural_name'], description=u['description'], space=self.request.space)
units_dict[u['id']] = unit.pk
recipes_dict = {}
recipe_property_factor_dict = {}
recipes = []
recipe_keyword_relation = []
for r in mealie_database['recipes']:
if Recipe.objects.filter(space=self.request.space, name=r['name']).exists() and not self.import_duplicates:
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
self.import_log.save()
else:
recipe = Recipe.objects.create(
waiting_time=parse_time(r['perform_time']),
working_time=parse_time(r['prep_time']),
description=r['description'][:512],
name=r['name'],
source_url=r['org_url'],
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1,
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "",
internal=True,
created_at=r['created_at'],
space=self.request.space,
created_by=self.request.user,
)
if not self.nutrition_per_serving:
recipe_property_factor_dict[r['id']] = recipe.servings
self.import_log.msg += self.get_recipe_processed_msg(recipe)
self.import_log.imported_recipes += 1
self.import_log.save()
recipes.append(recipe)
recipes_dict[r['id']] = recipe.pk
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipe.pk, keyword_id=self.keyword.pk))
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
self.import_log.msg += f"Importing {len(mealie_database["recipe_instructions"])} instructions...\n"
self.import_log.save()
steps_relation = []
first_step_of_recipe_dict = {}
for s in mealie_database['recipe_instructions']:
if s['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
order=s['position'],
name=s['title'],
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
if s['recipe_id'] not in first_step_of_recipe_dict:
first_step_of_recipe_dict[s['recipe_id']] = step.pk
for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'],
name=n['title'],
order=100,
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk))
Recipe.steps.through.objects.bulk_create(steps_relation)
ingredient_parser = IngredientParser(self.request, True)
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
self.import_log.save()
ingredients_relation = []
for i in mealie_database['recipes_ingredients']:
if i['recipe_id'] in recipes_dict:
if i['title']:
title_ingredient = Ingredient.objects.create(
note=i['title'],
is_header=True,
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=title_ingredient.pk))
if i['food_id']:
ingredient = Ingredient.objects.create(
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
original_text=i['original_text'],
order=i['position'],
amount=i['quantity'],
note=i['note'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
elif i['note'].strip():
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
ingredient = Ingredient.objects.create(
food=f,
unit=u,
amount=amount,
note=note,
original_text=i['original_text'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
Step.ingredients.through.objects.bulk_create(ingredients_relation)
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
self.import_log.save()
recipe_keyword_relation = []
for rC in mealie_database['recipes_to_categories']:
if rC['recipe_id'] in recipes_dict:
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rC['recipe_id']], keyword_id=keywords_categories_dict[rC['category_id']]))
for rT in mealie_database['recipes_to_tags']:
if rT['recipe_id'] in recipes_dict:
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rT['recipe_id']], keyword_id=keywords_tags_dict[rT['tag_id']]))
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
self.import_log.msg += f"Importing {len(mealie_database["recipe_nutrition"])} properties...\n"
self.import_log.save()
property_types_dict = {
'calories': PropertyType.objects.get_or_create(name=_('Calories'), space=self.request.space, defaults={'unit': 'kcal', 'fdc_id': 1008})[0],
'carbohydrate_content': PropertyType.objects.get_or_create(name=_('Carbohydrates'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1005})[0],
'cholesterol_content': PropertyType.objects.get_or_create(name=_('Cholesterol'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1253})[0],
'fat_content': PropertyType.objects.get_or_create(name=_('Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1004})[0],
'fiber_content': PropertyType.objects.get_or_create(name=_('Fiber'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1079})[0],
'protein_content': PropertyType.objects.get_or_create(name=_('Protein'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1003})[0],
'saturated_fat_content': PropertyType.objects.get_or_create(name=_('Saturated Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1258})[0],
'sodium_content': PropertyType.objects.get_or_create(name=_('Sodium'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1093})[0],
'sugar_content': PropertyType.objects.get_or_create(name=_('Sugar'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1063})[0],
'trans_fat_content': PropertyType.objects.get_or_create(name=_('Trans Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1257})[0],
'unsaturated_fat_content': PropertyType.objects.get_or_create(name=_('Unsaturated Fat'), space=self.request.space, defaults={'unit': 'g'})[0],
}
with transaction.atomic():
recipe_properties_relation = []
properties_relation = []
for r in mealie_database['recipe_nutrition']:
if r['recipe_id'] in recipes_dict:
for key in property_types_dict:
if r[key]:
properties_relation.append(
Property(property_type_id=property_types_dict[key].pk,
property_amount=Decimal(str(r[key])) / (
Decimal(str(recipe_property_factor_dict[r['recipe_id']])) if r['recipe_id'] in recipe_property_factor_dict else 1),
open_data_food_slug=r['recipe_id'],
space=self.request.space))
properties = Property.objects.bulk_create(properties_relation)
property_ids = []
for p in properties:
recipe_properties_relation.append(Recipe.properties.through(recipe_id=recipes_dict[p.open_data_food_slug], property_id=p.pk))
property_ids.append(p.pk)
Recipe.properties.through.objects.bulk_create(recipe_properties_relation, ignore_conflicts=True)
Property.objects.filter(id__in=property_ids).update(open_data_food_slug=None)
# delete unused property types
for pT in property_types_dict:
try:
property_types_dict[pT].delete()
except:
pass
self.import_log.msg += f"Importing {len(mealie_database["recipe_comments"]) + len(mealie_database["recipe_timeline_events"])} comments and cook logs...\n"
self.import_log.save()
cook_log_list = []
for c in mealie_database['recipe_comments']:
if c['recipe_id'] in recipes_dict:
cook_log_list.append(CookLog(
recipe_id=recipes_dict[c['recipe_id']],
comment=c['text'],
created_at=c['created_at'],
created_by=self.request.user,
space=self.request.space,
))
for c in mealie_database['recipe_timeline_events']:
if c['recipe_id'] in recipes_dict:
if c['event_type'] == 'comment':
cook_log_list.append(CookLog(
recipe_id=recipes_dict[c['recipe_id']],
comment=c['message'],
created_at=c['created_at'],
created_by=self.request.user,
space=self.request.space,
))
CookLog.objects.bulk_create(cook_log_list)
if self.import_meal_plans:
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
self.import_log.save()
meal_types_dict = {}
meal_plans = []
for m in mealie_database['group_meal_plans']:
if m['recipe_id'] in recipes_dict:
if not m['entry_type'] in meal_types_dict:
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
meal_types_dict[m['entry_type']] = meal_type.pk
meal_plans.append(MealPlan(
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
title=m['title'] if m['title'] else "",
note=m['text'] if m['text'] else "",
from_date=m['date'],
to_date=m['date'],
meal_type_id=meal_types_dict[m['entry_type']],
created_by=self.request.user,
space=self.request.space,
))
MealPlan.objects.bulk_create(meal_plans)
if self.import_shopping_lists:
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
self.import_log.save()
shopping_list_items = []
for sli in mealie_database['shopping_list_items']:
if not sli['checked']:
if sli['food_id']:
shopping_list_items.append(ShoppingListEntry(
amount=sli['quantity'],
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
created_by=self.request.user,
space=self.request.space,
))
elif not sli['food_id'] and sli['note'].strip():
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
shopping_list_items.append(ShoppingListEntry(
amount=amount,
unit=u,
food=f,
created_by=self.request.user,
space=self.request.space,
))
ShoppingListEntry.objects.bulk_create(shopping_list_items)
self.import_log.msg += f"Importing Images. This might take some time ...\n"
self.import_log.save()
for r in mealie_database['recipes']:
try:
if recipe := Recipe.objects.filter(pk=recipes_dict[r['id']]).first():
self.import_recipe_image(recipe, BytesIO(file.read(f'data/recipes/{str(uuid.UUID(str(r['id'])))}/images/original.webp')), filetype='.webp')
except Exception:
pass
return recipes
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
# Generated by Django 4.2.22 on 2025-09-05 06:51
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0223_auto_20250831_1111'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_credits_balance',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=100),
),
migrations.CreateModel(
name='AiProvider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('description', models.TextField(blank=True)),
('api_key', models.CharField(max_length=2048)),
('model_name', models.CharField(max_length=256)),
('url', models.CharField(blank=True, max_length=2048, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('space', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
),
migrations.CreateModel(
name='AiLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('function', models.CharField(max_length=64)),
('credit_cost', models.DecimalField(decimal_places=4, max_digits=16)),
('credits_from_balance', models.BooleanField(default=False)),
('input_tokens', models.IntegerField(default=0)),
('output_tokens', models.IntegerField(default=0)),
('start_time', models.DateTimeField(null=True)),
('end_time', models.DateTimeField(null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ai_provider', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.aiprovider')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.22 on 2025-09-08 19:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0224_space_ai_credits_balance_space_ai_credits_monthly_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_enabled',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.22 on 2025-09-08 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0225_space_ai_enabled'),
]
operations = [
migrations.AddField(
model_name='aiprovider',
name='log_credit_cost',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=10000),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.2.22 on 2025-09-09 11:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0226_aiprovider_log_credit_cost_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_default_provider',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_ai_default_provider', to='cookbook.aiprovider'),
),
migrations.AlterField(
model_name='space',
name='ai_credits_balance',
field=models.DecimalField(decimal_places=4, default=0, max_digits=16),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-09-10 20:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0001_squashed_0227_space_ai_default_provider_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='space_setup_completed',
field=models.BooleanField(default=True),
),
]

View File

@@ -329,6 +329,13 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
space_setup_completed = models.BooleanField(default=True)
ai_enabled = models.BooleanField(default=True)
ai_credits_monthly = models.IntegerField(default=100)
ai_credits_balance = models.DecimalField(default=0, max_digits=16, decimal_places=4)
ai_default_provider = models.ForeignKey("AiProvider", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_ai_default_provider')
internal_note = models.TextField(blank=True, null=True)
def safe_delete(self):
@@ -341,6 +348,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
BookmarkletImport.objects.filter(space=self).delete()
CustomFilter.objects.filter(space=self).delete()
AiLog.objects.filter(space=self).delete()
AiProvider.objects.filter(space=self).delete()
Property.objects.filter(space=self).delete()
PropertyType.objects.filter(space=self).delete()
@@ -393,6 +403,41 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
return self.name
class AiProvider(models.Model):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
# AiProviders can be global, so space=null is allowed (configurable by superusers)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
api_key = models.CharField(max_length=2048)
model_name = models.CharField(max_length=256)
url = models.CharField(max_length=2048, blank=True, null=True)
log_credit_cost = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class AiLog(models.Model, PermissionModelMixin):
F_FILE_IMPORT = 'FILE_IMPORT'
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
function = models.CharField(max_length=64)
credit_cost = models.DecimalField(max_digits=16, decimal_places=4)
# if credits from balance were used, else its from monthly quota
credits_from_balance = models.BooleanField(default=False)
input_tokens = models.IntegerField(default=0)
output_tokens = models.IntegerField(default=0)
start_time = models.DateTimeField(null=True)
end_time = models.DateTimeField(null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class ConnectorConfig(models.Model, PermissionModelMixin):
HOMEASSISTANT = 'HomeAssistant'
CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),)

View File

@@ -24,8 +24,9 @@ from rest_framework.fields import IntegerField
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.ai_helper import get_monthly_token_usage
from cookbook.helper.image_processing import is_file_type_allowed
from cookbook.helper.permission_helper import above_space_limit
from cookbook.helper.permission_helper import above_space_limit, create_space_for_user
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
@@ -36,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields)
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
@@ -325,12 +326,53 @@ class UserFileViewSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'file', 'file_download', 'file_size_kb', 'preview', 'created_by', 'created_at')
class AiProviderSerializer(serializers.ModelSerializer):
api_key = serializers.CharField(required=False, write_only=True)
def create(self, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().update(instance, validated_data)
def handle_global_space_logic(self, validated_data):
"""
allow superusers to create AI providers without a space but make sure everyone else only uses their own space
"""
if ('space' not in validated_data or not validated_data['space']) and self.context['request'].user.is_superuser:
validated_data['space'] = None
else:
validated_data['space'] = self.context['request'].space
return validated_data
class Meta:
model = AiProvider
fields = ('id', 'name', 'description', 'api_key', 'model_name', 'url', 'log_credit_cost', 'space', 'created_at', 'updated_at')
read_only_fields = ('created_at', 'updated_at',)
class AiLogSerializer(serializers.ModelSerializer):
ai_provider = AiProviderSerializer(read_only=True)
class Meta:
model = AiLog
fields = ('id', 'ai_provider', 'function', 'credit_cost', 'credits_from_balance', 'input_tokens', 'output_tokens', 'start_time', 'end_time', 'created_by', 'created_at',
'updated_at')
read_only_fields = ('__all__',)
class SpaceSerializer(WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True)
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True)
user_count = serializers.SerializerMethodField('get_user_count', read_only=True)
recipe_count = serializers.SerializerMethodField('get_recipe_count', read_only=True)
file_size_mb = serializers.SerializerMethodField('get_file_size_mb', read_only=True)
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used', read_only=True)
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
food_inherit = FoodInheritFieldSerializer(many=True, required=False)
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
@@ -350,6 +392,10 @@ class SpaceSerializer(WritableNestedModelSerializer):
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
@extend_schema_field(int)
def get_ai_monthly_credits_used(self, obj):
return get_monthly_token_usage(obj)
@extend_schema_field(float)
def get_file_size_mb(self, obj):
try:
@@ -358,7 +404,36 @@ class SpaceSerializer(WritableNestedModelSerializer):
return 0
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
if Space.objects.filter(created_by=self.context['request'].user).count() >= self.context['request'].user.userpreference.max_owned_spaces:
raise serializers.ValidationError(
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({self.context['request'].user.userpreference.max_owned_spaces})')
name = None
if 'name' in validated_data:
name = validated_data['name']
user_space = create_space_for_user(self.context['request'].user, name)
return user_space.space
def update(self, instance, validated_data):
validated_data = self.filter_superuser_parameters(validated_data)
if 'name' in validated_data:
if Space.objects.filter(Q(name=validated_data['name']), ~Q(pk=instance.pk)).exists():
raise ValidationError(_('Space Name must be unique.'))
return super().update(instance, validated_data)
def filter_superuser_parameters(self, validated_data):
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_enabled']
if 'ai_credits_monthly' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_monthly']
if 'ai_credits_balance' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_balance']
return validated_data
class Meta:
model = Space
@@ -366,10 +441,11 @@ class SpaceSerializer(WritableNestedModelSerializer):
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color',
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg', 'ai_credits_monthly',
'ai_credits_balance', 'ai_monthly_credits_used', 'ai_enabled', 'ai_default_provider', 'space_setup_completed')
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',)
'demo', 'ai_monthly_credits_used')
class UserSpaceSerializer(WritableNestedModelSerializer):
@@ -1038,7 +1114,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'private','servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
'internal', 'private', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
# TODO find a solution (custom schema?) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
@@ -1134,6 +1210,35 @@ class RecipeBatchUpdateSerializer(serializers.Serializer):
clear_description = serializers.BooleanField(required=False, allow_null=True)
class FoodBatchUpdateSerializer(serializers.Serializer):
foods = serializers.ListField(child=serializers.IntegerField())
category = serializers.IntegerField(required=False, allow_null=True)
substitute_add = serializers.ListField(child=serializers.IntegerField())
substitute_remove = serializers.ListField(child=serializers.IntegerField())
substitute_set = serializers.ListField(child=serializers.IntegerField())
substitute_remove_all = serializers.BooleanField(default=False)
inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove_all = serializers.BooleanField(default=False)
child_inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove_all = serializers.BooleanField(default=False)
substitute_children = serializers.BooleanField(required=False, allow_null=True)
substitute_siblings = serializers.BooleanField(required=False, allow_null=True)
ignore_shopping = serializers.BooleanField(required=False, allow_null=True)
on_hand = serializers.BooleanField(required=False, allow_null=True)
parent_remove = serializers.BooleanField(required=False, allow_null=True)
parent_set = serializers.IntegerField(required=False, allow_null=True)
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserSerializer(many=True, required=False)
@@ -1564,7 +1669,6 @@ class ServerSettingsSerializer(serializers.Serializer):
# TODO add all other relevant settings including path/url related ones?
shopping_min_autosync_interval = serializers.CharField()
enable_pdf_export = serializers.BooleanField()
enable_ai_import = serializers.BooleanField()
disable_external_connectors = serializers.BooleanField()
terms_url = serializers.CharField()
privacy_url = serializers.CharField()
@@ -1788,6 +1892,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer):
class AiImportSerializer(serializers.Serializer):
ai_provider_id = serializers.IntegerField()
file = serializers.FileField(allow_null=True)
text = serializers.CharField(allow_null=True, allow_blank=True)
recipe_id = serializers.CharField(allow_null=True, allow_blank=True)

View File

@@ -51,11 +51,6 @@
{# {% endif %}#}
<p class="card-text"><small
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
{% if us.space.created_by != us.user %}
<p class="card-text"><small
class="text-muted"><a
href="{% url 'delete_user_space' us.pk %}">{% trans 'Leave Space' %}</a></small>
{% endif %}
<!--TODO add direct link to management page -->
</p>
</div>

View File

@@ -0,0 +1,168 @@
import json
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import MealType, PropertyType, AiProvider
LIST_URL = 'api:aiprovider-list'
DETAIL_URL = 'api:aiprovider-detail'
@pytest.fixture()
def obj_1(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture
def obj_2(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_2', space=None)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
obj_1.space = space_2
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
obj_1.space = None
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 403],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 403],
['s1_s1', 200],
])
def test_update_global(arg, request, obj_2):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_2.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test', 'api_key': 'test', 'model_name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(a1_s1, a1_s2, obj_1):
# admins cannot delete foreign space providers
r = a1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
# admins can delete their space providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0
def test_delete_global(a1_s1, s1_s1, obj_2):
# admins cant delete global providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 403
# superusers can delete global providers
r = s1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0

View File

@@ -7,6 +7,7 @@ from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import UserSpace
from recipes import settings
LIST_URL = 'api:space-list'
DETAIL_URL = 'api:space-detail'
@@ -45,7 +46,6 @@ def test_list_multiple(u1_s1, space_1, space_2):
assert u1_response['id'] == space_1.id
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
@@ -70,9 +70,9 @@ def test_update(arg, request, space_1, a1_s1):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 405],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
@@ -90,3 +90,59 @@ def test_delete(u1_s1, u1_s2, a1_s1, space_1):
# event the space owner cannot delete his space over the api (this might change later but for now it's only available in the UI)
r = a1_s1.delete(reverse(DETAIL_URL, args={space_1.id}))
assert r.status_code == 405
def test_superuser_parameters(space_1, a1_s1, s1_s1):
# ------- test as normal user -------
response = a1_s1.post(reverse(LIST_URL), {'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 201
response = json.loads(response.content)
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
assert response['ai_credits_balance'] == 0
space_1.created_by = auth.get_user(a1_s1)
space_1.ai_enabled = False
space_1.ai_credits_monthly = 0
space_1.ai_credits_balance = 0
space_1.save()
response = a1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 200
space_1.refresh_from_db()
assert space_1.ai_enabled == False
assert space_1.ai_credits_monthly == 0
assert space_1.ai_credits_balance == 0
# ------- test as superuser -------
response = s1_s1.post(reverse(LIST_URL),
{'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 201
response = json.loads(response.content)
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
assert response['ai_credits_balance'] == 0
space_1.created_by = auth.get_user(s1_s1)
space_1.ai_enabled = False
space_1.ai_credits_monthly = 0
space_1.ai_credits_balance = 0
space_1.save()
response = s1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 200
space_1.refresh_from_db()
assert space_1.ai_enabled == True
assert space_1.ai_credits_monthly == 100
assert space_1.ai_credits_balance == 100

View File

@@ -5,6 +5,8 @@ from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import UserSpace
LIST_URL = 'api:userspace-list'
DETAIL_URL = 'api:userspace-detail'
@@ -13,10 +15,10 @@ DETAIL_URL = 'api:userspace-detail'
['a_u', 403, 0],
['g1_s1', 200, 1], # sees only own user space
['u1_s1', 200, 1],
['a1_s1', 200, 3], # sees user space of all users in space
['a2_s1', 200, 1],
['a1_s1', 200, 4], # admins can see all other members
['a2_s1', 200, 4],
])
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1, a2_s1):
space_1.created_by = auth.get_user(a1_s1)
space_1.save()
@@ -27,6 +29,18 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
assert len(json.loads(result.content)['results']) == arg[2]
def test_list_all_personal(space_2, u1_s1):
result = u1_s1.get(reverse('api:userspace-all-personal'))
assert result.status_code == 200
assert len(json.loads(result.content)) == 1
UserSpace.objects.create(user=auth.get_user(u1_s1), space=space_2)
result = u1_s1.get(reverse('api:userspace-all-personal'))
assert result.status_code == 200
assert len(json.loads(result.content)) == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],

View File

@@ -298,3 +298,11 @@ def a1_s2(client, space_2):
@pytest.fixture()
def a2_s2(client, space_2):
return create_user(client, space_2, group='admin')
@pytest.fixture()
def s1_s1(client, space_1):
client = create_user(client, space_1, group='admin')
user = auth.get_user(client)
user.is_superuser = True
user.save()
return client

View File

@@ -61,6 +61,8 @@ router.register(r'search-preference', api.SearchPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
router.register(r'access-token', api.AccessTokenViewSet)
router.register(r'ai-provider', api.AiProviderViewSet)
router.register(r'ai-log', api.AiLogViewSet)
router.register(r'localization', api.LocalizationViewSet, basename='localization')
router.register(r'server-settings', api.ServerSettingsViewSet, basename='server-settings')
@@ -76,10 +78,11 @@ urlpatterns = [
path('setup/', views.setup, name='view_setup'),
path('no-group/', views.no_groups, name='view_no_group'),
path('space-overview/', views.space_overview, name='view_space_overview'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm/', views.no_perm, name='view_no_perm'),
#path('space-overview/', views.space_overview, name='view_space_overview'),
#path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
#path('no-perm/', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('invite/<slug:token>/', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'),
path('plugin/update/', views.plugin_update, name='view_plugin_update'),

View File

@@ -18,8 +18,6 @@ import litellm
import redis
import requests
from PIL import UnidentifiedImageError
from PIL.ImImagePlugin import number
from PIL.features import check
from django.contrib import messages
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import TrigramSimilarity
@@ -35,7 +33,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.utils.datetime_safe import date
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from drf_spectacular.types import OpenApiTypes
@@ -65,6 +62,8 @@ from cookbook.connectors.connector_manager import ConnectorManager, ActionType
from cookbook.forms import ImportForm, ImportExportBase
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.HelperFunctions import str2bool, validate_import_url
from cookbook.helper.ai_helper import has_monthly_token, can_perform_ai_request, AiCallbackHandler
from cookbook.helper.batch_edit_helper import add_to_relation, remove_from_relation, remove_all_from_relation, set_relation
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter
@@ -74,7 +73,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, Cus
CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF,
above_space_limit,
group_required, has_group_permission, is_space_owner,
switch_user_active_space
switch_user_active_space, CustomAiProviderPermission, IsCreateDRF
)
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
@@ -85,7 +84,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
RecipeBookEntry, ShareLink, ShoppingListEntry,
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider
)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
@@ -110,12 +109,13 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer
)
from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration
from recipes import settings
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT, AI_API_KEY, AI_MODEL_NAME
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT
DateExample = OpenApiExample('Date Format', value='1972-12-05', request_only=True)
BeforeDateExample = OpenApiExample('Before Date Format', value='-1972-12-05', request_only=True)
@@ -131,7 +131,7 @@ class LoggingMixin(object):
if settings.REDIS_HOST:
try:
d = date.today().isoformat()
d = timezone.now().isoformat()
space = request.space
endpoint = request.resolver_match.url_name
@@ -179,7 +179,10 @@ class StandardFilterModelViewSet(viewsets.ModelViewSet):
queryset = self.queryset
query = self.request.query_params.get('query', None)
if query is not None:
queryset = queryset.filter(name__icontains=query)
try:
queryset = queryset.filter(name__icontains=query)
except FieldError:
pass
updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None:
@@ -541,9 +544,9 @@ class GroupViewSet(LoggingMixin, viewsets.ModelViewSet):
class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Space.objects
serializer_class = SpaceSerializer
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
permission_classes = [((IsReadOnlyDRF | IsCreateDRF) & CustomIsGuest) | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
http_method_names = ['get', 'patch']
http_method_names = ['get', 'post', 'put', 'patch']
def get_queryset(self):
return self.queryset.filter(
@@ -562,7 +565,7 @@ class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserSpace.objects
serializer_class = UserSpaceSerializer
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
permission_classes = [(CustomIsSpaceOwner | (IsReadOnlyDRF & CustomIsUser) | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
http_method_names = ['get', 'put', 'patch', 'delete']
pagination_class = DefaultPagination
@@ -576,10 +579,23 @@ class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note)
if is_space_owner(self.request.user, self.request.space):
# >= admins can see all users, guest/user can only see themselves
if has_group_permission(self.request.user, ['admin']):
return self.queryset.filter(space=self.request.space)
else:
return self.queryset.filter(user=self.request.user, space=self.request.space)
return self.queryset.filter(space=self.request.space, user=self.request.user)
@extend_schema(responses=UserSpaceSerializer(many=True))
@decorators.action(detail=False, pagination_class=DefaultPagination, methods=['GET'], serializer_class=UserSpaceSerializer, )
def all_personal(self, request):
"""
return all userspaces for the user requesting the endpoint
:param request:
:return:
"""
with scopes_disabled():
self.queryset = self.queryset.filter(user=self.request.user)
return Response(self.serializer_class(self.queryset.all(), many=True, context={'request': self.request}).data)
class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
@@ -617,6 +633,29 @@ class SearchPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user)
class AiProviderViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiProvider.objects
serializer_class = AiProviderSerializer
permission_classes = [CustomAiProviderPermission & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
def get_queryset(self):
# read only access to all space and global AiProviders
with scopes_disabled():
return self.queryset.filter(Q(space=self.request.space) | Q(space__isnull=True))
class AiLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiLog.objects
serializer_class = AiLogSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
http_method_names = ['get']
pagination_class = DefaultPagination
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class StorageViewSet(LoggingMixin, viewsets.ModelViewSet):
# TODO handle delete protect error and adjust test
queryset = Storage.objects
@@ -915,6 +954,94 @@ class FoodViewSet(LoggingMixin, TreeMixin):
content = {'error': True, 'msg': e.args[0]}
return Response(content, status=status.HTTP_403_FORBIDDEN)
@decorators.action(detail=False, methods=['PUT'], serializer_class=FoodBatchUpdateSerializer)
def batch_update(self, request):
serializer = self.serializer_class(data=request.data, partial=True)
if serializer.is_valid():
foods = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space)
safe_food_ids = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space).values_list('id', flat=True)
if 'category' in serializer.validated_data:
foods.update(supermarket_category_id=serializer.validated_data['category'])
if 'ignore_shopping' in serializer.validated_data and serializer.validated_data['ignore_shopping'] is not None:
foods.update(ignore_shopping=serializer.validated_data['ignore_shopping'])
if 'on_hand' in serializer.validated_data and serializer.validated_data['on_hand'] is not None:
if serializer.validated_data['on_hand']:
user_relation = []
for f in safe_food_ids:
user_relation.append(Food.onhand_users.through(food_id=f, user_id=request.user.id))
Food.onhand_users.through.objects.bulk_create(user_relation, ignore_conflicts=True, unique_fields=('food_id', 'user_id',))
else:
Food.onhand_users.through.objects.filter(food_id__in=safe_food_ids, user_id=request.user.id).delete()
if 'substitute_children' in serializer.validated_data and serializer.validated_data['substitute_children'] is not None:
foods.update(substitute_children=serializer.validated_data['substitute_children'])
if 'substitute_siblings' in serializer.validated_data and serializer.validated_data['substitute_siblings'] is not None:
foods.update(substitute_siblings=serializer.validated_data['substitute_siblings'])
# ---------- substitutes -------------
if 'substitute_add' in serializer.validated_data:
add_to_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_add'])
if 'substitute_remove' in serializer.validated_data:
remove_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_remove'])
if 'substitute_set' in serializer.validated_data and len(serializer.validated_data['substitute_set']) > 0:
set_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_set'])
if 'substitute_remove_all' in serializer.validated_data and serializer.validated_data['substitute_remove_all']:
remove_all_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids)
# ---------- inherit fields -------------
if 'inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_add'])
if 'inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_remove'])
if 'inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['inherit_fields_set']) > 0:
set_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_set'])
if 'inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['inherit_fields_remove_all']:
remove_all_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids)
# ---------- child inherit fields -------------
if 'child_inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_add'])
if 'child_inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_remove'])
if 'child_inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['child_inherit_fields_set']) > 0:
set_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_set'])
if 'child_inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['child_inherit_fields_remove_all']:
remove_all_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids)
# ------- parent --------
if self.model.node_order_by:
node_location = 'sorted'
else:
node_location = 'last'
if 'parent_remove' in serializer.validated_data and serializer.validated_data['parent_remove']:
for f in foods:
f.move(Food.get_first_root_node(), f'{node_location}-sibling')
if 'parent_set' in serializer.validated_data:
parent_food = Food.objects.filter(space=request.space, id=serializer.validated_data['parent_set']).first()
if parent_food:
for f in foods:
f.move(parent_food, f'{node_location}-child')
return Response({}, 200)
return Response(serializer.errors, 400)
@extend_schema_view(list=extend_schema(parameters=[
OpenApiParameter(name='order_field', description='Field to order recipe books on', type=str,
@@ -1108,7 +1235,19 @@ class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
return self.serializer_class
def get_queryset(self):
queryset = self.queryset.filter(step__recipe__space=self.request.space)
queryset = self.queryset.prefetch_related('food',
'food__properties',
'food__properties__property_type',
'food__inherit_fields',
'food__supermarket_category',
'food__onhand_users',
'food__substitute',
'food__child_inherit_fields',
'unit',
'unit__unit_conversion_base_relation',
'unit__unit_conversion_base_relation__base_unit',
'unit__unit_conversion_converted_relation',
'unit__unit_conversion_converted_relation__converted_unit', ).filter(step__recipe__space=self.request.space)
food = self.request.query_params.get('food', None)
if food and re.match(r'^(\d)+$', food):
queryset = queryset.filter(food_id=food)
@@ -1779,8 +1918,8 @@ class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note)
unused = self.request.query_params.get('unused', False)
if unused:
used = self.request.query_params.get('used', False)
if not used:
self.queryset = self.queryset.filter(used_by=None)
if is_space_owner(self.request.user, self.request.space):
@@ -2000,6 +2139,24 @@ class AiImportView(APIView):
if serializer.is_valid():
# TODO max file size check
if 'ai_provider_id' not in serializer.validated_data:
response = {
'error': True,
'msg': _('You must select an AI provider to perform your request.'),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
if not can_perform_ai_request(request.space):
response = {
'error': True,
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider)]
messages = []
uploaded_file = serializer.validated_data['file']
@@ -2068,7 +2225,15 @@ class AiImportView(APIView):
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
try:
ai_response = completion(api_key=AI_API_KEY, model=AI_MODEL_NAME, response_format={"type": "json_object"}, messages=messages, )
ai_request = {
'api_key': ai_provider.api_key,
'model': ai_provider.model_name,
'response_format': {"type": "json_object"},
'messages': messages,
}
if ai_provider.url:
ai_request['api_base'] = ai_provider.url
ai_response = completion(**ai_request)
except BadRequestError as err:
response = {
'error': True,
@@ -2127,7 +2292,13 @@ class AppImportView(APIView):
files = []
for f in request.FILES.getlist('files'):
files.append({'file': io.BytesIO(f.read()), 'name': f.name})
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
t = threading.Thread(target=integration.do_import,
args=[files, il, form.cleaned_data['duplicates']],
kwargs={'meal_plans': form.cleaned_data['meal_plans'],
'shopping_lists': form.cleaned_data['shopping_lists'],
'nutrition_per_serving': form.cleaned_data['nutrition_per_serving']
}
)
t.setDaemon(True)
t.start()
@@ -2373,7 +2544,6 @@ class ServerSettingsViewSet(viewsets.GenericViewSet):
# Attention: No login required, do not return sensitive data
s['shopping_min_autosync_interval'] = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
s['enable_pdf_export'] = settings.ENABLE_PDF_EXPORT
s['enable_ai_import'] = settings.AI_API_KEY != ''
s['disable_external_connectors'] = settings.DISABLE_EXTERNAL_CONNECTORS
s['terms_url'] = settings.TERMS_URL
s['privacy_url'] = settings.PRIVACY_URL
@@ -2546,10 +2716,9 @@ def ingredient_from_string(request):
if unit:
if unit_obj := Unit.objects.filter(space=request.space).filter(Q(name=unit) | Q(plural_name=unit)).first():
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
else:
unit_obj = Unit.objects.create(space=request.space, name=unit)
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
ingredient['unit'] = {'name': unit.name, 'id': unit.id}
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
return JsonResponse(ingredient, status=200)

View File

@@ -17,6 +17,7 @@ from cookbook.integration.copymethat import CopyMeThat
from cookbook.integration.default import Default
from cookbook.integration.domestica import Domestica
from cookbook.integration.mealie import Mealie
from cookbook.integration.mealie1 import Mealie1
from cookbook.integration.mealmaster import MealMaster
from cookbook.integration.melarecipes import MelaRecipes
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
@@ -45,6 +46,8 @@ def get_integration(request, export_type):
return NextcloudCookbook(request, export_type)
if export_type == ImportExportBase.MEALIE:
return Mealie(request, export_type)
if export_type == ImportExportBase.MEALIE1:
return Mealie1(request, export_type)
if export_type == ImportExportBase.CHOWDOWN:
return Chowdown(request, export_type)
if export_type == ImportExportBase.SAFFRON:

View File

@@ -54,7 +54,7 @@ def hook(request, token):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
ShoppingListEntry.objects.create(food=f, unit=u, amount=max(1, amount), created_by=request.user, space=request.space)
return JsonResponse({'data': data['message']['text']})
except Exception:

View File

@@ -21,7 +21,7 @@ from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.templatetags.static import static
from django.urls import reverse, reverse_lazy
from django.utils.datetime_safe import date
from django.utils import timezone
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from drf_spectacular.views import SpectacularRedocView, SpectacularSwaggerView
@@ -42,6 +42,9 @@ def index(request, path=None, resource=None):
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
return HttpResponseRedirect(reverse_lazy('view_setup'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
return render(request, 'frontend/tandoor.html', {})
else:
@@ -97,7 +100,8 @@ def space_overview(request):
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
)
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY, )
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get())
@@ -222,7 +226,7 @@ def system(request):
total_stats = ['All', int(r.get('api:request-count'))]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
api_stats[0].append(d)
api_space_stats[0].append(d)
total_stats.append(int(r.get(f'api:request-count:{d}')) if r.get(f'api:request-count:{d}') else 0)
@@ -233,7 +237,7 @@ def system(request):
endpoint = x[0].decode('utf-8')
endpoint_stats = [endpoint, x[1]]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
endpoint_stats.append(r.zscore(f'api:endpoint-request-count:{d}', endpoint))
api_stats.append(endpoint_stats)
@@ -242,7 +246,7 @@ def system(request):
if space := Space.objects.filter(pk=s).first():
space_stats = [space.name, x[1]]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
api_space_stats.append(space_stats)
@@ -321,7 +325,7 @@ def invite_link(request, token):
try:
token = UUID(token, version=4)
except ValueError:
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
print('Malformed Invite Link supplied!')
return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
@@ -330,22 +334,17 @@ def invite_link(request, token):
link.used_by = request.user
link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
if request.user.userspace_set.count() == 1:
user_space.active = True
user_space.save()
UserSpace.objects.filter(user=request.user).update(active=False)
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=True)
user_space.groups.add(link.group)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
else:
request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
def report_share_abuse(request, token):

18
docs/features/ai.md Normal file
View File

@@ -0,0 +1,18 @@
Tandoor has several AI based features. To allow maximum flexibility, you can configure different AI providers and select them based on the task you want to perform.
To prevent accidental cost escalation Tandoor has a robust system of tracking and limiting AI costs.
## Default Configuration
By default the AI features are enabled for every space. Each space has a spending limit of roughly 1 USD per month.
This can be changed using the [configuration variables](https://docs.tandoor.dev/system/configuration/#ai-integration)
You can change these settings any time using the django admin. If you do not care about AI cost you can enter a very high limit or disable cost tracking for your providers.
The limit resets on the first of every month.
## Configure AI Providers
When AI support is enabled for a space every user in a space can configure AI providers.
The models shown in the editor have been tested and work with Tandoor. Most other models that can parse images/files and return text should also work.
Superusers also have the ability to configure global AI providers that every space can use.
## AI Log
The AI Log allows you to track the usage of AI calls. Here you can also see the usage.

View File

@@ -97,10 +97,17 @@ Follow these steps to import your recipes
Mealie provides structured data similar to nextcloud.
!!! WARNING "Versions"
There are two different versions of the Mealie importer. One for all backups created prior to Version 1.0 and one for all after.
!!! INFO "Versions"
The Mealie UI does not indicate weather or not nutrition information is stored per serving or per recipe. This choice is left to the user. During the import you will have to choose
how Tandoor should treat your nutrition data.
To migrate your recipes
1. Go to your Mealie settings and create a new Backup.
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server).
1. Go to your Mealie admin settings and create a new backup.
2. Download the backup.
3. Upload the entire `.zip` file to the importer page and import everything.
## Chowdown

View File

@@ -196,6 +196,7 @@ server {
proxy_set_header Host $http_host;
proxy_pass http://unix:/var/www/recipes/recipes.sock;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
```

View File

@@ -472,15 +472,20 @@ S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/Tandoor
#### AI Integration
To use AI to perform different tasks you need to configure an API key and the AI provider. [LiteLLM](https://www.litellm.ai/) is used
to make a standardized request to different AI providers of your liking.
Configuring this via environment parameters is a temporary solution. In the future I plan on adding support for multiple AI providers per Tandoor instance
with the option to select them for various tasks. For now only gemini 2.0 flash has been tested but feel free to try out other models.
Most AI features are configured trough the AI Provider settings in the Tandoor web interface. Some defaults can be set for new spaces on your instance.
Enables AI features for spaces by default
```
SPACE_AI_ENABLED=1
```
Sets the monthly default credit limit for AI usage
```
SPACE_AI_CREDITS_MONTHLY=100
```
Ratelimit for AI API
```
AI_API_KEY=
AI_MODEL_NAME=gemini/gemini-2.0-flash
AI_RATELIMIT=60/hour
```

View File

@@ -59,6 +59,8 @@ SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = extract_bool('SPACE_DEFAULT_ALLOW_SHARING', True)
SPACE_AI_ENABLED = extract_bool('SPACE_AI_ENABLED', True)
SPACE_AI_CREDITS_MONTHLY = int(os.getenv('SPACE_AI_CREDITS_MONTHLY', 10000))
INTERNAL_IPS = extract_comma_list('INTERNAL_IPS', '127.0.0.1')
@@ -137,8 +139,6 @@ HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
AI_API_KEY = os.getenv('AI_API_KEY', '')
AI_MODEL_NAME = os.getenv('AI_MODEL_NAME', 'gemini/gemini-2.0-flash')
AI_RATELIMIT = os.getenv('AI_RATELIMIT', '60/hour')
SHARING_ABUSE = extract_bool('SHARING_ABUSE', False)
@@ -565,8 +565,6 @@ else:
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
@@ -594,8 +592,18 @@ LANGUAGES = [
AWS_ENABLED = True if os.getenv('S3_ACCESS_KEY', False) else False
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
# Serve static files with gzip
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
if os.getenv('S3_ACCESS_KEY', ''):
DEFAULT_FILE_STORAGE = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
STORAGES['default']['BACKEND'] = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
AWS_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY', '')
AWS_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', '')
@@ -610,14 +618,9 @@ if os.getenv('S3_ACCESS_KEY', ''):
if os.getenv('S3_CUSTOM_DOMAIN', ''):
AWS_S3_CUSTOM_DOMAIN = os.getenv('S3_CUSTOM_DOMAIN', '')
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
else:
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
# Serve static files with gzip
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
# settings for cross site origin (CORS)
# all origins allowed to support bookmarklet

View File

@@ -1,12 +1,12 @@
Django==4.2.22
Django==5.2.6
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0
django-crispy-forms==2.4
crispy-bootstrap4==2025.6
djangorestframework==3.15.2
djangorestframework==3.16.1
drf-spectacular==0.27.1
drf-spectacular-sidecar==2025.7.1
drf-spectacular-sidecar==2025.8.1
drf-writable-nested==0.7.2
django-oauth-toolkit==2.4.0
django-debug-toolbar==4.3.0
@@ -16,7 +16,7 @@ lxml==5.3.1
Markdown==3.7
Pillow==11.3.0
psycopg2-binary==2.9.10
python-dotenv==1.0.0
python-dotenv==1.1.1
requests==2.32.4
six==1.17.0
webdavclient3==3.14.6
@@ -33,9 +33,9 @@ recipe-scrapers==15.8.0
django-scopes==2.0.0
django-treebeard==4.7.1
django-cors-headers==4.6.0
django-storages==1.14.2
django-storages==1.14.6
boto3==1.28.75
django-prometheus==2.3.1
django-prometheus==2.4.1
django-hCaptcha==0.2.0
python-ldap==3.4.4
django-auth-ldap==4.6.0

View File

@@ -19,11 +19,11 @@
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.10",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.0",
"vue-simple-calendar": "7.1.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.9.3"
"vuetify": "^3.9.7"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
@@ -35,10 +35,10 @@
"esbuild-register": "^3.6.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "6.3.5",
"vite-plugin-pwa": "^1.0.2",
"vite": "7.1.5",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8",
"vue-tsc": "^3.0.6",
"workbox-background-sync": "^7.3.0",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",

View File

@@ -156,13 +156,20 @@ const router = useRouter()
const isPrintMode = useMediaQuery('print')
onMounted(() => {
useUserPreferenceStore()
useUserPreferenceStore().init().then(() => {
if (useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined && !useUserPreferenceStore().activeSpace.spaceSetupCompleted) {
router.push({name: 'WelcomePage'})
}
})
})
/**
* global title update handler, might be overridden by page specific handlers
*/
router.afterEach((to, from) => {
if(to.name == 'StartPage' && useUserPreferenceStore().initCompleted && !useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined &&!useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
router.push({name: 'WelcomePage'})
}
nextTick(() => {
if (to.meta.title) {
title.value = t(to.meta.title)

View File

@@ -18,6 +18,7 @@ let routes = [
{path: '/', component: () => import("@/pages/StartPage.vue"), name: 'StartPage' },
{path: '/search', redirect: {name: 'StartPage'}},
{path: '/test', component: () => import("@/pages/TestPage.vue"), name: 'view_test'},
{path: '/welcome', component: () => import("@/pages/WelcomePage.vue"), name: 'WelcomePage', meta: {title: 'Welcome'}},
{path: '/help', component: () => import("@/pages/HelpPage.vue"), name: 'HelpPage', meta: {title: 'Help'}},
{
path: '/settings', component: () => import("@/pages/SettingsPage.vue"), name: 'SettingsPage', redirect: '/settings/account',
@@ -28,8 +29,6 @@ let routes = [
{path: 'meal-plan', component: () => import("@/components/settings/MealPlanSettings.vue"), name: 'MealPlanSettings', meta: {title: 'Settings'}},
{path: 'search', component: () => import("@/components/settings/SearchSettings.vue"), name: 'SearchSettings', meta: {title: 'Settings'}},
{path: 'space', component: () => import("@/components/settings/SpaceSettings.vue"), name: 'SpaceSettings', meta: {title: 'Settings'}},
{path: 'space-members', component: () => import("@/components/settings/SpaceMemberSettings.vue"), name: 'SpaceMemberSettings', meta: {title: 'Settings'}},
{path: 'user-space', component: () => import("@/components/settings/UserSpaceSettings.vue"), name: 'UserSpaceSettings', meta: {title: 'Settings'}},
{path: 'open-data-import', component: () => import("@/components/settings/OpenDataImportSettings.vue"), name: 'OpenDataImportSettings', meta: {title: 'Settings'}},
{path: 'export', component: () => import("@/components/settings/ExportDataSettings.vue"), name: 'ExportDataSettings', meta: {title: 'Settings'}},
{path: 'api', component: () => import("@/components/settings/ApiSettings.vue"), name: 'ApiSettings', meta: {title: 'Settings'}},

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,186 @@
<template>
<v-dialog max-width="1200px" :activator="props.activator" v-model="dialog">
<v-card :loading="loading">
<v-closable-card-title
:title="$t('BatchEdit')"
:sub-title="$t('BatchEditUpdatingItemsCount', {type: $t('Foods'), count: updateItems.length})"
:icon="TFood.icon"
v-model="dialog"
></v-closable-card-title>
<v-divider></v-divider>
<v-card-text>
<v-form>
<v-row>
<v-col cols="12" md="6">
<v-card :title="$t('Miscellaneous')" prepend-icon="fa-solid fa-list" variant="flat">
<v-card-text>
<model-select model="SupermarketCategory" v-model="batchUpdateRequest.foodBatchUpdate.category" :object="false" allow-create mode="single">
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('Ignore_Shopping')" clearable v-model="batchUpdateRequest.foodBatchUpdate.ignoreShopping"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('OnHand')" clearable v-model="batchUpdateRequest.foodBatchUpdate.onHand"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('Substitutes')"></v-label>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('Substitutes')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.substituteRemoveAll"></v-checkbox>
<v-select :items="boolUpdateOptions" :label="$t('substitute_siblings')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteChildren"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('substitute_children')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteSiblings"></v-select>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :title="$t('Hierarchy')" prepend-icon="fa-solid fa-folder-tree" variant="flat">
<v-card-text>
<model-select model="Food" :label="$t('Parent')" :object="false" allow-create clearable v-model="batchUpdateRequest.foodBatchUpdate.parentSet">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('RemoveParent')" clearable v-model="batchUpdateRequest.foodBatchUpdate.parentRemove"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('InheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('InheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemoveAll"></v-checkbox>
<v-spacer></v-spacer>
<v-label :text="$t('ChildInheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('ChildInheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemoveAll"></v-checkbox>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn :disabled="loading" @click="dialog = false">{{ $t('Cancel') }}</v-btn>
<v-btn color="warning" :loading="loading" @click="batchUpdateFoods()" :disabled="updateItems.length < 1">{{ $t('Update') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString, TFood, TKeyword, TRecipe} from "@/types/Models.ts";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {useI18n} from "vue-i18n";
import {ApiApi, ApiFoodBatchUpdateUpdateRequest, ApiRecipeBatchUpdateUpdateRequest, Food, Recipe, RecipeOverview} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const emit = defineEmits(['change'])
const props = defineProps({
items: {type: Array as PropType<Array<Food>>, required: true},
activator: {type: String, default: 'parent'},
})
const {t} = useI18n()
const dialog = defineModel<boolean>({default: false})
const loading = ref(false)
const updateItems = ref([] as Food[])
const batchUpdateRequest = ref({foodBatchUpdate: {}} as ApiFoodBatchUpdateUpdateRequest)
const boolUpdateOptions = ref([
{value: true, title: t('Yes')},
{value: false, title: t('No')},
])
/**
* copy prop when dialog opens so that items remain when parent is updated after change is emitted
*/
watch(dialog, (newValue, oldValue) => {
if (!oldValue && newValue && props.items != undefined) {
batchUpdateRequest.value.foodBatchUpdate.foods = props.items.flatMap(r => r.id!)
updateItems.value = JSON.parse(JSON.stringify(props.items))
}
})
/**
* perform batch request to update recipes
*/
function batchUpdateFoods() {
let api = new ApiApi()
loading.value = true
api.apiFoodBatchUpdateUpdate(batchUpdateRequest.value).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
emit('change')
loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -14,6 +14,7 @@
<v-list-item link title="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item>
<v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item>
<v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item>
<v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item>
<v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item>
<v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item>
@@ -45,7 +46,7 @@
<v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub
</v-btn>
<v-alert class="mt-3" border="start" variant="tonal" color="success">
<v-alert class="mt-3" border="start" variant="tonal" color="success" v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert-title>Did you know?</v-alert-title>
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
@@ -105,6 +106,35 @@
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="ai">
<p class="mt-3">Tandoor has several functions that allow you to use AI to automatically perform certain tasks like importing recipes from a PDFs or images.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To use AI you must first configure an AI Provider. This can also be done globally for all spaces by the person operating your Tandoor Server.
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Some AI Providers are available globally for every space to use. You can also configure additional AI Providers for your space only.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space (either monthly or using a balance).
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance
that will be used once your monthly limit is reached.
</p>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiProvider'}}">
{{ $t('AiProvider') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
{{ $t('AiLog') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="unit">
<p class="mt-3">Units allow you to measure how much of something you need in a recipe or on a shopping list.
@@ -337,6 +367,7 @@
<script setup lang="ts">
import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const drawer = defineModel()
const window = ref('start')

View File

@@ -1,6 +1,6 @@
<template>
<v-row v-if=" props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-col>
<v-progress-linear :model-value="(props.importLog.importedRecipes/props.importLog.totalRecipes)*100" height="24" color="primary">
{{ props.importLog.importedRecipes }} / {{ props.importLog.totalRecipes }}
@@ -8,9 +8,9 @@
</v-col>
</v-row>
<v-row>
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-col>
<v-textarea :model-value="importLog.msg" max-rows="25" auto-grow></v-textarea>
<v-textarea :model-value="importLog.msg" max-rows="25" :loading="importLog.running" auto-grow></v-textarea>
</v-col>
</v-row>

View File

@@ -36,11 +36,16 @@
<td style="width: 1%; text-wrap: nowrap" class="pa-0 d-print-none" v-if="showCheckbox">
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)" v-if="!i.noAmount"></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-if="i.noAmount"></td>
<!-- display calculated food amount or empty cell -->
<td style="width: 1%; text-wrap: nowrap"
class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"
v-if="!i.noAmount && i.amount != 0">
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-else></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1">
<template v-if="i.unit && !i.noAmount"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
<template v-if="i.unit && !i.noAmount && i.amount != 0"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
</td>
<td>
<template v-if="i.food">

View File

@@ -11,7 +11,7 @@
<recipe-image :height="itemHeight" :width="itemHeight" :recipe="mealPlan.recipe"></recipe-image>
</div>
<div class="flex-column flex-grow-0 pa-1">
<span class="font-light" :class="{'two-line-text': detailedItems,'one-line-text': !detailedItems,}">
<span class="font-light" :class="{'three-line-text': detailedItems,'one-line-text': !detailedItems,}">
<i class="fas fa-shopping-cart fa-xs float-left" v-if="mealPlan.shopping"/>
{{ itemTitle }}
</span>
@@ -82,4 +82,13 @@ const itemTitle = computed(() => {
overflow: hidden;
text-overflow: ellipsis;
}
.three-line-text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -98,9 +98,9 @@ const planItems = computed(() => {
*/
const calendarItemHeight = computed(() => {
if (lgAndUp.value && useUserPreferenceStore().deviceSettings.mealplan_displayPeriod == 'week') {
return '2.6rem'
return '3.5rem'
} else {
return '1.3rem'
return '1.6rem'
}
})

View File

@@ -120,6 +120,7 @@ const hasFoodProperties = computed(() => {
let propertiesFound = false
for (const [key, fp] of Object.entries(recipe.value.foodProperties)) {
if (fp.total_value !== 0) {
console.log(fp, fp.total_value)
propertiesFound = true
}
}
@@ -189,7 +190,7 @@ const dialogProperty = ref<undefined | PropertyWrapper>(undefined)
const loading = ref(false)
onMounted(() => {
if (!hasFoodProperties) {
if (!hasFoodProperties.value) {
sourceSelectedToShow.value = "recipe"
}
})

View File

@@ -33,9 +33,8 @@
</template>
<v-list-item-title class="font-weight-bold">
{{ c.createdBy.displayName }}
</v-list-item-title>
<v-list-item-subtitle>{{ c.comment }}</v-list-item-subtitle>
<span>{{ c.comment }}</span>
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
@@ -49,7 +48,7 @@
<template #append>
<v-list-item-action class="flex-column align-end">
<v-rating density="comfortable" size="x-small" color="tandoor" v-model="c.rating" half-increments readonly
v-if="c.rating != undefined"></v-rating>
v-if="c.rating != undefined" style="overflow: hidden"></v-rating>
<v-spacer></v-spacer>
<v-tooltip location="top" :text="DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_MED)" v-if="c.createdAt != undefined">
<template v-slot:activator="{ props }">
@@ -121,6 +120,7 @@ function recLoadCookLog(recipeId: number, page: number = 1) {
* reset new cook log from with proper defaults
*/
function resetForm() {
newCookLog.value = {} as CookLog
newCookLog.value.servings = props.recipe.servings
newCookLog.value.createdAt = new Date()
newCookLog.value.recipe = props.recipe.id!

View File

@@ -121,11 +121,16 @@
<template v-if="recipe.filePath">
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
<v-card :title="$t('AI')" prepend-icon="$ai" @click="aiConvertRecipe()" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading"
<v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
v-if="!recipe.internal">
<v-card-text>
Convert the recipe using AI
{{$t('ConvertUsingAI')}}
<model-select model="AiProvider" v-model="selectedAiProvider">
<template #append>
<v-btn @click="aiConvertRecipe()" icon="fa-solid fa-person-running" color="success"></v-btn>
</template>
</model-select>
</v-card-text>
</v-card>
</template>
@@ -191,7 +196,7 @@
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {ApiApi, Recipe} from "@/openapi"
import {AiProvider, ApiApi, Recipe} from "@/openapi"
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
import StepsOverview from "@/components/display/StepsOverview.vue";
import RecipeActivity from "@/components/display/RecipeActivity.vue";
@@ -207,6 +212,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useFileApi} from "@/composables/useFileApi.ts";
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const {request, release} = useWakeLock()
const {doAiImport, fileApiLoading} = useFileApi()
@@ -217,6 +223,8 @@ const recipe = defineModel<Recipe>({required: true})
const servings = ref(1)
const showFullRecipeName = ref(false)
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
/**
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
@@ -249,7 +257,7 @@ onBeforeUnmount(() => {
function aiConvertRecipe() {
let api = new ApiApi()
doAiImport(null, '', recipe.value.id!).then(r => {
doAiImport(selectedAiProvider.value.id!,null, '', recipe.value.id!).then(r => {
if (r.recipe) {
recipe.value.internal = true
recipe.value.steps = r.recipe.steps

View File

@@ -1,5 +1,5 @@
<template>
<v-list-item class="swipe-container border-t-sm" :id="itemContainerId" @touchend="handleSwipe()"
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
>
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
@@ -7,15 +7,15 @@
<!-- </div>-->
<div class="flex-grow-1 p-2" @click="dialog = true;">
<div class="flex-grow-1 p-2">
<div class="d-flex">
<div class="d-flex flex-column pr-2">
<div class="d-flex flex-column pr-2 pl-4">
<span v-for="a in amounts" v-bind:key="a.key">
<span>
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
<span :class="{'text-disabled': a.checked || a.delayed}" class="text-no-wrap">
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1)">{{ $n(a.amount) }}</span>
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1) || a.unit">{{ $n(a.amount) }}</span>
<span class="ms-1" v-if="a.unit">{{ pluralString(a.unit, a.amount) }}</span>
</span>
</b>
@@ -30,10 +30,13 @@
</div>
</div>
<template v-slot:[checkBtnSlot]>
<v-btn color="success" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
<div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);">
<v-btn color="success" size="large"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
</div>
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
</template>

View File

@@ -0,0 +1,72 @@
<template>
<v-row v-if="props.space.name != undefined">
<v-col cols="12" md="4">
<v-card :to="{name: 'SearchPage'}">
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(props.space.recipeCount) }} / {{ props.space.maxRecipes == 0 ? '∞' : $n(props.space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.recipeCount / props.space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserSpace'}}">
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(props.space.userCount) }} / {{ props.space.maxUsers == 0 ? '∞' : $n(props.space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.userCount / props.space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserFile'}}">
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="props.space.maxFileStorageMb > -1">{{ $n(Math.round(props.space.fileSizeMb)) }} /
{{ props.space.maxFileStorageMb == 0 ? '' : $n(props.space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="props.space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="props.space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.fileSizeMb / props.space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('MonthlyCredits') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiMonthlyCreditsUsed) }} / {{ $n(props.space.aiCreditsMonthly) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear :model-value="props.space.aiMonthlyCreditsUsed" :max="props.space.aiCreditsMonthly" height="10"
></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('AiCreditsBalance') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiCreditsBalance) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear height="10"
></v-progress-linear>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {PropType} from "vue";
import {Space} from "@/openapi";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
space: {type: {} as PropType<Space>, required: true},
})
</script>
<style scoped>
</style>

View File

@@ -71,7 +71,7 @@ const mergedIngredients = computed(() => {
// Add ingredients from steps
props.steps.forEach(step => {
step.ingredients.forEach(ingredient => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
if (ingredient.food && !ingredient.isHeader ) {
ingredients.push(ingredient);
}
});
@@ -80,7 +80,7 @@ const mergedIngredients = computed(() => {
if (step.stepRecipeData) {
step.stepRecipeData.steps?.forEach((subStep: Step) => {
subStep.ingredients.forEach((ingredient: Ingredient) => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
if (ingredient.food && !ingredient.isHeader) {
ingredients.push(ingredient);
}
});

View File

@@ -0,0 +1,47 @@
<template>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
</template>
<script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
</script>
<style scoped>
</style>

View File

@@ -15,6 +15,8 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
const emit = defineEmits(['stop'])
import {computed, onMounted, ref} from "vue";
@@ -24,6 +26,8 @@ const props = defineProps({
seconds: {type: Number, required: true}
})
const {t} = useI18n()
const initialDurationSeconds = ref(props.seconds)
const durationSeconds = ref(initialDurationSeconds.value)
const timerRunning = ref(true)

View File

@@ -0,0 +1,51 @@
<template>
<v-select
:label="$t('Language')"
v-model="$i18n.locale"
:items="availableLocalizations"
item-title="language"
item-value="code"
@update:model-value="updateLanguage()"
></v-select>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, Localization} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useI18n} from "vue-i18n";
const availableLocalizations = ref([] as Localization[])
const {locale} = useI18n()
onMounted(() => {
const api = new ApiApi()
api.apiLocalizationList().then(r => {
availableLocalizations.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* update the django language cookie
* this is used by django to inject the language into the template which in turn
* sets the frontend language in i18n.ts when the frontend is initialized
*/
function updateLanguage() {
const expires = new Date();
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
location.reload()
}
</script>
<style scoped>
</style>

View File

@@ -58,7 +58,6 @@
<template v-if="hasMoreItems && !loading" #afterlist>
<span class="text-disabled font-italic text-caption ms-3">{{ $t('ModelSelectResultsHelp') }}</span>
</template>
</Multiselect>
<template #append v-if="$slots.append">
@@ -73,7 +72,7 @@
import {computed, onBeforeMount, onMounted, PropType, ref, useTemplateRef} from "vue"
import {EditorSupportedModels, GenericModel, getGenericModelFromString} from "@/types/Models"
import Multiselect from '@vueform/multiselect'
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
import {ErrorMessageType, MessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
@@ -171,7 +170,7 @@ function search(query: string) {
*/
async function createObject(object: any, select$: Multiselect) {
return await modelClass.value.create({name: object[itemLabel.value]}).then((createdObj: any) => {
useMessageStore().addMessage(MessageType.SUCCESS, 'Created', 5000, createdObj)
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS, createdObj)
emit('create', object)
return createdObj
}).catch((err: any) => {

View File

@@ -98,7 +98,7 @@
<script setup lang="ts">
import {ApiApi, UserFile, UserFileFromJSON} from "@/openapi";
import {onMounted, ref} from "vue";
import {onMounted, ref, watch} from "vue";
import {DateTime} from "luxon";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {getCookie} from "@/utils/cookie";
@@ -131,8 +131,13 @@ const tableHeaders = ref([
])
onMounted(() => {
//TODO move to open function of file tab
loadFiles()
})
watch(() => dialog.value, (value, oldValue) => {
if (value && !oldValue) {
loadFiles()
}
})
function loadFiles() {

View File

@@ -0,0 +1,105 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<v-text-field :label="$t('APIKey')" v-model="editingObj.apiKey"></v-text-field>
<v-combobox :label="$t('Model')" :items="aiModels" v-model="editingObj.modelName" hide-details>
</v-combobox>
<p class="mt-2 mb-2">{{ $t('AiModelHelp') }} <a href="https://docs.litellm.ai/docs/providers" target="_blank">LiteLLM</a></p>
<v-checkbox :label="$t('LogCredits')" :hint="$t('LogCreditsHelp')" v-model="editingObj.logCreditCost" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
<v-text-field :label="$t('Url')" v-model="editingObj.url"></v-text-field>
<v-checkbox :label="$t('Global')" :hint="$t('GlobalHelp')" v-model="globalProvider" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {AiProvider} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor";
const props = defineProps({
item: {type: {} as PropType<AiProvider>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<AiProvider>, required: false, default: {} as AiProvider},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<AiProvider>('AiProvider', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
const aiModels = ref(['gemini/gemini-2.5-pro', 'gemini/gemini-2.5-flash', 'gemini/gemini-2.5-flash-lite', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'])
const globalProvider = ref(false)
watch(() => globalProvider.value, () => {
if (globalProvider.value) {
editingObj.value.space = undefined
} else {
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
}
})
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {
itemDefaults: props.itemDefaults,
newItemFunction: () => {
editingObj.value.logCreditCost = true
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
},
}).then(() => {
globalProvider.value = editingObj.value.space == undefined
})
}
</script>
<style scoped>
</style>

View File

@@ -54,7 +54,7 @@
<properties-editor v-model="editingObj.properties" :amount-for="propertiesAmountFor"></properties-editor>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-form>
</v-tabs-window-item>
@@ -106,7 +106,7 @@
</v-card>
</v-form>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="hierarchy">
@@ -119,6 +119,9 @@
mode="tags"></ModelSelect>
<ModelSelect model="FoodInheritField" v-model="editingObj.childInheritFields" :label="$t('ChildInheritFields')" :hint="$t('ChildInheritFields_help')"
mode="tags"></ModelSelect>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 100px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="misc">

View File

@@ -16,7 +16,7 @@
<v-date-input :label="$t('Valid Until')" v-model="editingObj.validUntil"></v-date-input>
<v-textarea :label="$t('Note')" v-model="editingObj.internalNote"></v-textarea>
<v-checkbox :label="$t('Reusable')" v-model="editingObj.reusable"></v-checkbox>
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)">
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)" v-if="isUpdate()">
<template #append-inner>
<btn-copy variant="plain" color="undefined" :copy-value="inviteLinkUrl(editingObj)"></btn-copy>
</template>
@@ -37,6 +37,7 @@ import {DateTime} from "luxon";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const props = defineProps({
@@ -91,7 +92,7 @@ function initializeEditor(){
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}

View File

@@ -0,0 +1,142 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text class="pa-0">
<v-tabs v-model="tab" :disabled="loading" grow>
<v-tab value="space">{{ $t('Space') }}</v-tab>
<v-tab value="cosmetic">{{ $t('Cosmetic') }}</v-tab>
<v-tab value="ai">{{ $t('AI') }}</v-tab>
</v-tabs>
</v-card-text>
<v-card-text>
<v-tabs-window v-model="tab">
<v-tabs-window-item value="space">
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<user-file-field v-model="editingObj.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="editingObj.message" :label="$t('Message')" clearable></v-textarea>
<space-limits-info :space="editingObj" :show-thank-you="false" v-if="isUpdate()"></space-limits-info>
</v-form>
</v-tabs-window-item>
<v-tabs-window-item value="cosmetic">
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="editingObj.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="editingObj.navBgColor = ''">{{ $t('Reset') }}</v-btn>
<user-file-field v-model="editingObj.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
<user-file-field v-model="editingObj.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
<user-file-field v-model="editingObj.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
<user-file-field v-model="editingObj.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
</v-tabs-window-item>
<v-tabs-window-item value="ai">
<p class="text-disabled font-italic text-body-2">
<span v-if="useUserPreferenceStore().serverSettings.hosted">
{{ $t('AISettingsHostedHelp') }}
</span>
<span v-else>
{{ $t('SettingsOnlySuperuser') }}
</span>
</p>
<v-checkbox v-model="editingObj.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
<template v-if="editingObj.aiEnabled">
<model-select model="AiProvider" :label="$t('Default')" v-model="editingObj.aiDefaultProvider"></model-select>
<v-number-input v-model="editingObj.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
<v-number-input v-model="editingObj.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
</template>
</v-tabs-window-item>
</v-tabs-window>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {ApiApi, ConnectorConfig, Space} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
const props = defineProps({
item: {type: {} as PropType<Space>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<Space>, required: false, default: {} as Space},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {
setupState,
deleteObject,
saveObject,
isUpdate,
editingObjName,
loading,
editingObj,
editingObjChanged,
modelClass
} = useModelEditorFunctions<Space>('Space', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
const tab = ref("space")
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

View File

@@ -20,9 +20,9 @@
<v-text-field :label="$t('Username')" v-model="editingObj.username" v-if="editingObj.method == 'NEXTCLOUD' || editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Password')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.password" v-if="editingObj.method == 'NEXTCLOUD'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
</v-form>
</v-card-text>
@@ -33,7 +33,7 @@
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import { Storage } from "@/openapi";
import {Storage} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
@@ -64,7 +64,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
function initializeEditor(){
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}

View File

@@ -2,7 +2,10 @@
<v-form>
<p class="text-h6">{{ $t('Profile') }}</p>
<v-divider class="mb-3"></v-divider>
<v-text-field :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<thank-you-note></thank-you-note>
<v-text-field class="mt-3" :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<!-- <v-label>Avatar</v-label><br/>-->
<!-- <v-avatar class="mt-3 mb-3" style="height: 10vh; width: 10vh" color="info">V</v-avatar> Feature coming in a future Version of Tandoor.-->
@@ -39,6 +42,7 @@ import {ApiApi, User} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import ThankYouNote from "@/components/display/ThankYouNote.vue";
const {getDjangoUrl} = useDjangoUrls()

View File

@@ -3,14 +3,7 @@
<p class="text-h6">{{ $t('Cosmetic') }}</p>
<v-divider class="mb-3"></v-divider>
<v-select
:label="$t('Language')"
v-model="$i18n.locale"
:items="availableLocalizations"
item-title="language"
item-value="code"
@update:model-value="updateLanguage()"
></v-select>
<language-select></language-select>
<v-label>{{$t('Nav_Color')}}</v-label>
<v-color-picker v-model="useUserPreferenceStore().userSettings.navBgColor" mode="hex" :modes="['hex']" show-swatches :swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
@@ -54,10 +47,10 @@ import {ApiApi, Localization} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import LanguageSelect from "@/components/inputs/LanguageSelect.vue";
const {locale, t} = useI18n()
const {t} = useI18n()
const availableLocalizations = ref([] as Localization[])
const availableDefaultPages = ref([
{page: 'SEARCH', label: t('Search')},
{page: 'SHOPPING', label: t('Shopping_list')},
@@ -65,29 +58,10 @@ const availableDefaultPages = ref([
{page: 'BOOKS', label: t('Books')},
])
onMounted(() => {
const api = new ApiApi()
api.apiLocalizationList().then(r => {
availableLocalizations.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* update the django language cookie
* this is used by django to inject the language into the template which in turn
* sets the frontend language in i18n.ts when the frontend is initialized
*/
function updateLanguage() {
const expires = new Date();
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
location.reload()
}
</script>
<style scoped>

View File

@@ -1,10 +1,9 @@
<template>
<p class="text-h6">{{ $t('Open_Data_Import') }}</p>
<p class="text-h4">{{ $t('Open_Data_Import') }}</p>
<v-divider></v-divider>
<p class="text-subtitle-2">{{ $t('Data_Import_Info') }}</p>
<v-btn href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</v-btn>
<p class="text-subtitle-1">{{ $t('Data_Import_Info') }} <a href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</a></p>
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-2" v-model="requestData.selectedVersion" :loading="loading"></v-select>
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-4" v-model="requestData.selectedVersion" :loading="loading"></v-select>
<v-row v-if="requestData.selectedVersion">
<v-col>
@@ -29,10 +28,10 @@
<td>{{ metadata[requestData.selectedVersion][d] }}</td>
<td>
<template v-if="responseData[d]">
<i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }} <br/>
<i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }} <br/>
<i class="fas fa-forward"></i> {{ responseData[d].totalUntouched}} {{ $t('Unchanged') }} <br/>
<i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}
<p v-if="responseData[d].totalCreated > 0" ><i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }}</p>
<p v-if="responseData[d].totalUpdated > 0"><i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }}</p>
<p v-if="responseData[d].totalUntouched > 0"><i class="fas fa-forward"></i> {{ responseData[d].totalUntouched }} {{ $t('Unchanged') }}</p>
<p v-if="responseData[d].totalErrored > 0"><i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}</p>
</template>
</td>
</tr>
@@ -102,7 +101,6 @@ function importOpenData() {
})
api.apiImportOpenDataCreate({importOpenData: requestData.value}).then(r => {
console.log(r)
responseData.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)

View File

@@ -1,121 +0,0 @@
<template>
<v-form>
<p class="text-h6">{{ $t('SpaceMembers') }}</p>
<v-divider></v-divider>
<p class="text-subtitle-2">{{ $t('SpaceMemberHelp') }}</p>
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10" class="mt-3">
<template #item.groups="{item}">
<span v-for="g in item.groups">{{ g.name }}&nbsp;</span>
</template>
<template #item.edit="{item}">
<v-btn color="edit" size="small" v-if="item.user.id != useUserPreferenceStore().activeSpace.createdBy.id">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="UserSpace" :item="item" @delete="deleteUserSpace(item)" class="mt-2"></model-edit-dialog>
</v-btn>
<v-chip color="edit" v-else>{{ $t('Owner') }}</v-chip>
</template>
</v-data-table>
<p class="text-h6 mt-3">{{ $t('Invites') }}
<v-btn size="small" class="float-right" prepend-icon="$create" color="create">
{{ $t('New') }}
<model-edit-dialog model="InviteLink" @delete="deleteInviteLink" @create="item => spaceInviteLinks.push(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</p>
<v-divider class="mb-3"></v-divider>
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
<template #item.reusable="{item}">
<v-icon icon="fa-solid fa-check" color="success" v-if="item.reusable"></v-icon>
<v-icon icon="fa-solid fa-times" color="error" v-if="!item.reusable"></v-icon>
</template>
<template #item.edit="{item}">
<btn-copy size="small" :copy-value="inviteLinkUrl(item)" class="me-1"></btn-copy>
<v-btn color="edit" size="small">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="InviteLink" :item="item" @delete="deleteInviteLink(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</template>
</v-data-table>
</v-form>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, InviteLink, UserSpace} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
const {t} = useI18n()
const spaceUserSpaces = ref([] as UserSpace[])
const spaceInviteLinks = ref([] as InviteLink[])
const userTableHeaders = [
{title: t('Username'), key: 'user.username'},
{title: t('Role'), key: 'groups'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
const inviteTableHeaders = [
{title: 'ID', key: 'id'},
{title: t('Email'), key: 'email'},
{title: t('Role'), key: 'group.name'},
{title: t('Reusable'), key: 'reusable'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
onMounted(() => {
const api = new ApiApi()
api.apiUserSpaceList().then(r => {
spaceUserSpaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
api.apiInviteLinkList({unused: true}).then(r => {
spaceInviteLinks.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* delete userspace from client list (database handled by editor)
* @param userSpace UserSpace object that was deleted
*/
function deleteUserSpace(userSpace: UserSpace) {
spaceUserSpaces.value.splice(spaceUserSpaces.value.indexOf(userSpace) - 1, 1)
}
/**
* delete invite link from client list (database handled by editor)
* @param inviteLink InviteLink object that was deleted
*/
function deleteInviteLink(inviteLink: InviteLink) {
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink) - 1, 1)
}
/**
* returns url for invite link
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
}
</script>
<style scoped>
</style>

View File

@@ -3,144 +3,17 @@
<p class="text-h6">{{ useUserPreferenceStore().activeSpace.name }}</p>
<v-divider class="mb-3"></v-divider>
<v-row v-if="space.name != undefined">
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(space.recipeCount) }} / {{ space.maxRecipes == 0 ? '∞' : $n(space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.recipeCount / space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(space.userCount) }} / {{ space.maxUsers == 0 ? '∞' : $n(space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.userCount / space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="space.maxFileStorageMb > -1">{{ $n(Math.round(space.fileSizeMb)) }} / {{ space.maxFileStorageMb == 0 ? '' : $n(space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.fileSizeMb / space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
</v-row>
<v-divider class="mt-3 mb-3"></v-divider>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
<p class="text-h6 mt-2">{{ $t('Settings') }}</p>
<v-divider class="mb-2"></v-divider>
<user-file-field v-model="space.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
<!-- <model-select v-model="space.foodInherit" model="FoodInheritField" mode="tags"></model-select>-->
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<v-divider class="mt-4 mb-2"></v-divider>
<h2>{{$t('Cosmetic')}}</h2>
<span>{{$t('Space_Cosmetic_Settings')}}</span>
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="space.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="space.navBgColor = ''">{{$t('Reset')}}</v-btn>
<user-file-field v-model="space.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
<user-file-field v-model="space.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
<user-file-field v-model="space.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
<user-file-field v-model="space.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
<user-file-field v-model="space.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
<user-file-field v-model="space.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
<user-file-field v-model="space.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
<user-file-field v-model="space.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
<user-file-field v-model="space.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<space-editor :item-id="useUserPreferenceStore().activeSpace.id!"></space-editor>
</v-form>
</template>
<script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {onMounted, ref} from "vue";
import {ApiApi, Space} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils";
const space = ref({} as Space)
onMounted(() => {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
space.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
function updateSpace() {
let api = new ApiApi()
api.apiSpacePartialUpdate({id: space.value.id, patchedSpace: space.value}).then(r => {
space.value = r
useUserPreferenceStore().activeSpace = Object.assign({}, space.value)
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS, space.value)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
import SpaceEditor from "@/components/model_editors/SpaceEditor.vue";
</script>
<style scoped>

View File

@@ -1,53 +0,0 @@
<template>
<v-row>
<v-col>
<p class="text-h6">
{{ $t('YourSpaces') }}
<v-btn color="create" prepend-icon="$add" class="float-right" size="small" :href="getDjangoUrl('space-overview')">{{$t('New')}}</v-btn>
</p>
<v-divider></v-divider>
</v-col>
</v-row>
<v-row>
<v-col cols="6" v-for="s in spaces" :key="s.id">
<v-card @click="useUserPreferenceStore().switchSpace(s)">
<v-img height="200px" cover :src="(s.image !== undefined) ? s.image?.preview : recipeDefaultImage" :alt="$t('Image')"></v-img>
<v-card-title>{{ s.name }}
<v-chip variant="tonal" density="compact" color="error" v-if="s.id == useUserPreferenceStore().activeSpace.id">{{ $t('active') }}</v-chip>
</v-card-title>
<v-card-subtitle>{{ $t('created_by') }} {{ s.createdBy.displayName }}</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, Space} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import recipeDefaultImage from '../../assets/recipe_no_image.svg'
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
const {getDjangoUrl} = useDjangoUrls()
const spaces = ref([] as Space[])
onMounted(() => {
const api = new ApiApi()
api.apiSpaceList().then(r => {
spaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
</script>
<style scoped>
</style>

View File

@@ -1,7 +1,8 @@
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {ref} from "vue";
import {getCookie} from "@/utils/cookie";
import {RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {tr} from "vuetify/locale";
/**
@@ -86,7 +87,7 @@ export function useFileApi() {
* @param text text to import
* @param recipeId id of a recipe to use as import base (for external recipes
*/
function doAiImport(file: File | null, text: string = '', recipeId: string = '') {
function doAiImport(providerId: number, file: File | null, text: string = '', recipeId: string = '') {
let formData = new FormData()
if (file != null) {
@@ -96,6 +97,7 @@ export function useFileApi() {
}
formData.append('text', text)
formData.append('recipe_id', recipeId)
formData.append('ai_provider_id', providerId)
fileApiLoading.value = true
return fetch(getDjangoUrl(`api/ai-import/`), {
@@ -116,12 +118,18 @@ export function useFileApi() {
* @param files array to import
* @param app app to import
* @param includeDuplicates if recipes that were found as duplicates should be imported as well
* @param mealPlans if meal plans should be imported
* @param shoppingLists if shopping lists should be imported
* @param nutritionPerServing if nutrition information should be treated as per serving (if false its treated as per recipe)
* @returns Promise resolving to the import ID of the app import
*/
function doAppImport(files: File[], app: string, includeDuplicates: boolean) {
function doAppImport(files: File[], app: string, includeDuplicates: boolean, mealPlans: boolean = true, shoppingLists: boolean = true, nutritionPerServing: boolean = false,) {
let formData = new FormData()
formData.append('type', app);
formData.append('duplicates', includeDuplicates ? 'true' : 'false')
formData.append('meal_plans', mealPlans ? 'true' : 'false')
formData.append('shopping_lists', shoppingLists ? 'true' : 'false')
formData.append('nutrition_per_serving', nutritionPerServing ? 'true' : 'false')
files.forEach(file => {
formData.append('files', file)
})
@@ -140,4 +148,4 @@ export function useFileApi() {
}
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, doAiImport, doAppImport}
}
}

View File

@@ -1,6 +1,8 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -14,6 +16,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -44,6 +52,7 @@
"Color": "",
"Coming_Soon": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -51,6 +60,8 @@
"CountMore": "",
"Create": "",
"Create Food": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -58,6 +69,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -102,10 +114,14 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -121,6 +137,9 @@
"IgnoredFood": "",
"Image": "",
"Import": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -146,8 +165,11 @@
"Keyword": "",
"Keyword_Alias": "",
"Keywords": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Make_Header": "",
@@ -165,6 +187,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -193,6 +217,8 @@
"NotInShopping": "",
"Note": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -256,6 +282,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",
@@ -267,8 +294,13 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",
@@ -315,6 +347,8 @@
"Website": "",
"Week": "",
"Week_Numbers": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -1,6 +1,8 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "Добави",
"AddChild": "",
"AddFoodToShopping": "Добавете {food} към списъка си за пазаруване",
@@ -14,6 +16,12 @@
"Added_by": "Добавено от",
"Added_on": "Добавено",
"Advanced": "Разширено",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "Приложение",
"Apply": "",
"Are_You_Sure": "Сигурен ли си?",
@@ -44,17 +52,21 @@
"Color": "Цвят",
"Coming_Soon": "Очаквайте скоро",
"Completed": "Завършено",
"ConvertUsingAI": "",
"Copy": "Копиране",
"Copy_template_reference": "Копирайте препратка към шаблона",
"CountMore": "...+{count} още",
"Create": "Създаване",
"Create Food": "Създайте храна",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Създайте запис за план за хранене",
"Create_New_Food": "Добавете нова храна",
"Create_New_Keyword": "Добавяне на нова ключова дума",
"Create_New_Meal_Type": "Добавете нов тип хранене",
"Create_New_Shopping Category": "Създайте нова категория за пазаруване",
"Create_New_Unit": "Добавяне на нова единица",
"Credits": "",
"Current_Period": "Текущ период",
"Custom Filter": "Персонализиран филтър",
"DELETE_ERROR": "",
@@ -99,10 +111,14 @@
"FoodOnHand": "Имате {храна} под ръка.",
"Food_Alias": "Псевдоним на храната",
"Foods": "Храни",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи",
@@ -118,6 +134,9 @@
"IgnoredFood": "{food} е настроен да игнорира пазаруването.",
"Image": "Изображение",
"Import": "Импортиране",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Възникна грешка по време на импортирането ви. Моля, разгънете подробностите в долната част на страницата, за да ги видите.",
"Import_Not_Yet_Supported": "Импортирането все още не се поддържа",
"Import_Result_Info": "Импортирани са {imported} от {total} рецепти",
@@ -141,8 +160,11 @@
"Keyword": "Ключова дума",
"Keyword_Alias": "Псевдоним на ключова дума",
"Keywords": "Ключови думи",
"LeaveSpace": "",
"Link": "Връзка",
"Load_More": "Зареди още",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Дневник на Готвене",
"Log_Recipe_Cooking": "Дневник на Рецепта за готвене",
"Make_Header": "Направете заглавие",
@@ -159,6 +181,8 @@
"Merge_Keyword": "Обединяване на ключова дума",
"MissingProperties": "",
"Month": "Месец",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Премести",
"MoveCategory": "Премести към: ",
"Move_Down": "Премести надолу",
@@ -186,6 +210,8 @@
"NotInShopping": "{food} не е в списъка ви за пазаруване.",
"Note": "Бележка",
"Nutrition": "Хранителни стойности",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Вие сте офлайн, списъкът за пазаруване може да не се синхронизира.",
"Ok": "Отвори",
"OnHand": "В момента под ръка",
@@ -249,6 +275,7 @@
"Selected": "Избрано",
"Servings": "Порции",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Споделяне",
"Shopping_Categories": "Категории за пазаруване",
"Shopping_Category": "Категория за пазаруване",
@@ -260,8 +287,13 @@
"Show_as_header": "Показване като заглавка",
"Single": "Единичен",
"Size": "Размер",
"Skip": "",
"Sort_by_new": "Сортиране по ново",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "Начален ден от седмицата",
"StartsWith": "",
"StartsWithHelp": "",
@@ -306,6 +338,8 @@
"Website": "уебсайт",
"Week": "Седмица",
"Week_Numbers": "Номера на седмиците",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Година",
"Yes": "",
"add_keyword": "Добавяне на ключова дума",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Compte",
"Active": "",
"Add": "Afegir",
"AddChild": "",
"AddFoodToShopping": "Afegeix {food} a la llista de la compra",
@@ -16,6 +18,12 @@
"Added_by": "Afegit per",
"Added_on": "Afegit el",
"Advanced": "Avançat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineació",
"Amount": "Quantitat",
"App": "Aplicació",
@@ -57,6 +65,7 @@
"Comments_setting": "Mostrar comentaris",
"Completed": "Completat",
"Conversion": "Conversió",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Enllaç",
"Copy Token": "Copiar Token",
@@ -66,6 +75,8 @@
"Create": "Crear",
"Create Food": "Crear aliment/ingredient",
"Create Recipe": "Crear una recepta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crear una entrada de la planificació d'àpats",
"Create_New_Food": "Afegir nou ingredient",
"Create_New_Keyword": "Afegir nova Paraula Clau",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "Afegir nova Categoria de Compres",
"Create_New_Unit": "Afegir nova unitat",
"Created": "Creada",
"Credits": "",
"Current_Period": "Període Actual",
"Custom Filter": "Filtre Personalitzat",
"CustomImageHelp": "Carregar una imatge per mostrar a la vista general de lespai.",
@@ -143,10 +155,14 @@
"Food_Alias": "Àlies per l'aliment",
"Food_Replace": "Aliment equivalent",
"Foods": "Aliments",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau",
@@ -165,6 +181,9 @@
"Image": "Imatge",
"Import": "Importar",
"Import Recipe": "Importar Recepta",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "S'ha produït un error durant la importació. Si us plau, amplia els detalls a la part inferior de la pàgina per veure'l.",
"Import_Not_Yet_Supported": "Importació encara no suportada",
"Import_Result_Info": "{imported} de {total} receptes s'han importat",
@@ -195,8 +214,11 @@
"Language": "Llenguatge",
"Last_name": "Cognoms",
"Learn_More": "Saber-me més",
"LeaveSpace": "",
"Link": "Enllaç",
"Load_More": "Carregueu-ne més",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreu el que s'ha cuinat",
"Log_Recipe_Cooking": "Registre de receptes",
"Logo": "Logotip",
@@ -216,6 +238,8 @@
"Message": "Missatge",
"MissingProperties": "",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Moure",
"MoveCategory": "Moure a: ",
"Move_Down": "Moveu avall",
@@ -252,6 +276,8 @@
"Note": "Nota",
"Number of Objects": "Nombre d'Objectes",
"Nutrition": "Valors nutricionals",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Estàs desconnectat, la llista de la compra no pot actualitzar-se.",
"Ok": "Ok",
"OnHand": "Ja en tinc",
@@ -328,6 +354,7 @@
"Selected": "Seleccionat",
"Servings": "Racions",
"Settings": "Opcions",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShoppingBackgroundSyncWarning": "Error de la connexió, esperant per sincronitzar ...",
"Shopping_Categories": "Categoria de compres",
@@ -344,9 +371,14 @@
"Show_as_header": "Mostreu com a títol",
"Single": "Únic/a",
"Size": "Mida",
"Skip": "",
"Social_Authentication": "Identificació amb Xarxes Socials",
"Sort_by_new": "Ordenar a partir del més nou",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.",
"Split_All_Steps": "Dividir totes les files en passos separats.",
"StartDate": "Data d'inici",
@@ -413,6 +445,8 @@
"Week": "Setmana",
"Week_Numbers": "Números de la setmana",
"Welcome": "Benvingut/da",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Any",
"Yes": "",
"add_keyword": "Afegir Paraula Clau",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Účet",
"Active": "",
"Add": "Přidat",
"AddChild": "",
"AddFoodToShopping": "Přidat {food} na váš nákupní seznam",
@@ -16,6 +18,12 @@
"Added_by": "Přidáno uživatelem",
"Added_on": "Přidáno v",
"Advanced": "Rozšířené",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Zarovnání",
"Amount": "Množství",
"App": "Aplikace",
@@ -57,6 +65,7 @@
"Comments_setting": "Zobrazit komentáře",
"Completed": "Dokončeno",
"Conversion": "Převod",
"ConvertUsingAI": "",
"Copy": "Kopírovat",
"Copy Link": "Kopírovat odkaz",
"Copy Token": "Kopírovat token",
@@ -66,6 +75,8 @@
"Create": "Vytvořit",
"Create Food": "Vytvořit potravinu",
"Create Recipe": "Vytvořit recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Vytvořit položku v jídelníčku",
"Create_New_Food": "Přidat novou potravinu",
"Create_New_Keyword": "Přidat nový štítek",
@@ -73,6 +84,7 @@
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
"Create_New_Shopping_Category": "Přidat novou nákupní kategorii",
"Create_New_Unit": "Přidat novou jednotku",
"Credits": "",
"Current_Period": "Současné období",
"Custom Filter": "Uživatelský filtr",
"CustomImageHelp": "Nahrajte obrázek, který se zobrazí v přehledu prostoru.",
@@ -142,10 +154,14 @@
"Food_Alias": "Přezdívka potraviny",
"Food_Replace": "Nahrazení v potravině",
"Foods": "Potraviny",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky",
@@ -164,6 +180,9 @@
"Image": "Obrázek",
"Import": "Import",
"Import Recipe": "Importovat recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Během importu došlo k chybě. Pro více informací rozbalte Detaily na konci stránky.",
"Import_Not_Yet_Supported": "Import není zatím podporován",
"Import_Result_Info": "{imported} z {total} receptů naimportováno",
@@ -193,8 +212,11 @@
"Language": "Jazyk",
"Last_name": "Příjmení",
"Learn_More": "Zjistit víc",
"LeaveSpace": "",
"Link": "Odkaz",
"Load_More": "Načíst další",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zaznamenat vaření",
"Log_Recipe_Cooking": "Záznam vaření receptu",
"Logo": "Logo",
@@ -214,6 +236,8 @@
"Message": "Zpráva",
"MissingProperties": "",
"Month": "Měsíc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Přesunout",
"MoveCategory": "Přesunout do: ",
"Move_Down": "Dolů",
@@ -249,6 +273,8 @@
"Note": "Poznámka",
"Number of Objects": "Počet Objektů",
"Nutrition": "Výživové hodnoty",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Jste offline, nákupní seznam nemusí být synchronizován.",
"Ok": "Ok",
"OnHand": "Momentálně k dispozici",
@@ -325,6 +351,7 @@
"Selected": "Vybrané",
"Servings": "Porce",
"Settings": "Nastavení",
"SettingsOnlySuperuser": "",
"Share": "Sdílet",
"Shopping_Categories": "Kategorie nákupního seznamu",
"Shopping_Category": "Kategorie nákupního seznamu",
@@ -339,9 +366,14 @@
"Show_as_header": "Nastav jako nadpis",
"Single": "Jednoduchý",
"Size": "Velikost",
"Skip": "",
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
"Sort_by_new": "Seřadit od nejnovějšího",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"StartDate": "Počáteční datum",
@@ -405,6 +437,8 @@
"Week": "Týden",
"Week_Numbers": "Číslo týdne",
"Welcome": "Vítejte",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Rok",
"Yes": "",
"add_keyword": "Přidat štítek",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Bruger",
"Active": "",
"Add": "Tilføj",
"AddChild": "",
"AddFoodToShopping": "Tilføj {food} til indkøbsliste",
@@ -16,6 +18,12 @@
"Added_by": "Tilføjet af",
"Added_on": "Tilføjet den",
"Advanced": "Avanceret",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mængde",
"App": "App",
@@ -57,6 +65,7 @@
"Comments_setting": "Vis kommentarer",
"Completed": "Afsluttet",
"Conversion": "Konversion",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier link",
"Copy Token": "Kopier token",
@@ -66,6 +75,8 @@
"Create": "Opret",
"Create Food": "Opret mad",
"Create Recipe": "Opret opskrift",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Indsæt punkt i madplan",
"Create_New_Food": "Tilføj ny mad",
"Create_New_Keyword": "Tilføj nyt nøgleord",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "Opret ny indkøbskategori",
"Create_New_Unit": "Tilføj ny enhed",
"Created": "Skabt",
"Credits": "",
"Current_Period": "Nuværende periode",
"Custom Filter": "Tilpasset filter",
"CustomImageHelp": "Upload et billede for at vise dets plade i område-oversigten.",
@@ -143,10 +155,14 @@
"Food_Alias": "Alternativt navn til mad",
"Food_Replace": "Erstat ingrediens",
"Foods": "Mad",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord",
@@ -165,6 +181,9 @@
"Image": "Billede",
"Import": "Importer",
"Import Recipe": "Importer opskrift",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Der opstod en fejl under din importering. Udvid detaljerne i bunden af siden for at se fejlen.",
"Import_Not_Yet_Supported": "Import endnu ikke understøttet",
"Import_Result_Info": "{imported} af {total} opskrifter blev importeret",
@@ -195,8 +214,11 @@
"Language": "Sprog",
"Last_name": "Efternavn",
"Learn_More": "Lær mere",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Indlæs mere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Noter tilberedning",
"Log_Recipe_Cooking": "Noter tilberedning af opskrift",
"Logo": "Logo",
@@ -216,6 +238,8 @@
"Message": "Besked",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flyt",
"MoveCategory": "Flyt til: ",
"Move_Down": "Flyt ned",
@@ -252,6 +276,8 @@
"Note": "Note",
"Number of Objects": "Antal objekter",
"Nutrition": "Næring",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du er offline, indkøbslisten er måske ikke synkroniseret.",
"Ok": "Åben",
"OnHand": "Til rådighed",
@@ -328,6 +354,7 @@
"Selected": "Valgt",
"Servings": "Serveringer",
"Settings": "Indstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårligt netværk, afventer synkronisering ...",
"Shopping_Categories": "Indkøbskategorier",
@@ -344,9 +371,14 @@
"Show_as_header": "Vis som rubrik",
"Single": "Enkel",
"Size": "Størrelse",
"Skip": "",
"Social_Authentication": "Social authenticering",
"Sort_by_new": "Sorter efter nylige",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.",
"Split_All_Steps": "Opdel rækker i separate trin.",
"StartDate": "Startdato",
@@ -413,6 +445,8 @@
"Week": "Uge",
"Week_Numbers": "Ugenumre",
"Welcome": "Velkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "Tilføj nøgleord",

View File

@@ -1,16 +1,16 @@
{
"AI": "AI",
"AIImportSubtitle": "Verwende AI um Fotos von Rezepten zu importieren.",
"AISettingsHostedHelp": "AI Verfügbarkeit und Credit Limits können über die Tarifverwaltung geändert werden. ",
"API": "API",
"APIKey": "API Schlüssel",
"Summary": "Zusammenfassung",
"Structured": "Strukturiert",
"API_Browser": "API Browser",
"API_Documentation": "API Dokumentation",
"AccessTokenHelp": "Zugriffsschlüssel für die REST Schnittstelle.",
"Access_Token": "Zugriffstoken",
"Account": "Konto",
"Actions": "Aktionen",
"Active": "Aktiv",
"Activity": "Aktivität",
"Add": "Hinzufügen",
"AddAll": "Alle Hinzufügen",
@@ -31,6 +31,12 @@
"Admin": "Admin",
"Advanced": "Erweitert",
"Advanced Search Settings": "Erweiterte Sucheinstellungen",
"AiCreditsBalance": "Credit Guthaben",
"AiLog": "AI Protokoll",
"AiLogHelp": "Eine Übersicht der AI Anfragen.",
"AiModelHelp": "Die Liste enthält Modelle die offiziell Unterstützt und getestet wurden. Weitere modelle können manuell eingetragen werden.",
"AiProvider": "AI Anbieter",
"AiProviderHelp": "Je nach Präferenz können verschiedene AI Anbieter angelegt werden. Diese können auch Space übergreifend sein.",
"Alignment": "Ausrichtung",
"AllRecipes": "Alle Rezepte",
"Amount": "Menge",
@@ -92,6 +98,7 @@
"Continue": "Weiter",
"Conversion": "Umrechnung",
"ConversionsHelp": "Mit Umrechnungen kann die Menge eines Lebensmittels in verschiedenen Einheiten ausgerechnet werden. Aktuell wird dies nur zur berechnung von Eigenschaften verwendet, später jedoch sollen auch andere Funktionen von Tandoor davon profitieren. ",
"ConvertUsingAI": "Mithilfe von AI Umwandeln",
"CookLog": "Kochprotokoll",
"CookLogHelp": "Einträge im Kochprotokoll für Rezepte. ",
"Cooked": "Gekocht",
@@ -105,6 +112,8 @@
"Create": "Erstellen",
"Create Food": "Zutat erstellen",
"Create Recipe": "Rezept erstellen",
"CreateFirstRecipe": "Erstelle dein erstes Rezept mit dem Rezepteditor.",
"CreateInvitation": "Einladung erstellen",
"Create_Meal_Plan_Entry": "Neuer Eintrag",
"Create_New_Food": "Neues Lebensmittel hinzufügen",
"Create_New_Keyword": "Neues Schlagwort hinzufügen",
@@ -114,6 +123,7 @@
"Create_New_Unit": "Neue Einheit hinzufügen",
"Created": "Erstellt",
"CreatedBy": "Erstellt von",
"Credits": "Credits",
"Ctrl+K": "Strg+K",
"Current_Period": "Aktueller Zeitraum",
"Custom Filter": "Benutzerdefinierter Filter",
@@ -207,11 +217,15 @@
"Food_Replace": "Essen Ersetzen",
"Foods": "Lebensmittel",
"Friday": "Freitag",
"FromBalance": "Guthaben verwendet",
"Fulltext": "Volltext",
"FulltextHelp": "Felder welche im Volltext durchsucht werden sollen. Tipp: Die Suchtypen 'web', 'raw' und 'phrase' funktionieren nur mit Volltext-Feldern.",
"Fuzzy": "Unscharf",
"FuzzySearchHelp": "Verwende unscharfe Suche um Einträge auch bei Unterschieden in der Schreibweise zu finden.",
"GettingStarted": "Erste Schritte",
"Global": "Global",
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
"Group": "Gruppe",
"GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift",
@@ -237,7 +251,10 @@
"Import": "Importieren",
"Import Recipe": "Rezept importieren",
"ImportAll": "Alle importieren",
"ImportFirstRecipe": "Importiere dein erstes Rezept von einer von tausenden Websites oder nutze einen der anderen Importer um bestehende Sammlungen, Dokumente oder URL Listen zu importieren. ",
"ImportIntoTandoor": "In Tandoor importieren",
"ImportMealPlans": "Speisepläne importieren",
"ImportShoppingList": "Einkaufslisten importieren",
"Import_Error": "Es ist ein Fehler beim Importieren aufgetreten. Bitte sieh dir die ausgeklappten Details unten auf der Seite an.",
"Import_Not_Yet_Supported": "Importieren wird noch nicht unterstützt",
"Import_Result_Info": "{imported} von insgesamt {total} Rezepten wurden importiert",
@@ -276,9 +293,12 @@
"Last": "Letztes",
"Last_name": "Nachname",
"Learn_More": "Mehr erfahren",
"LeaveSpace": "Space verlassen",
"Link": "Link",
"Load": "Laden",
"Load_More": "Weitere laden",
"LogCredits": "Credits Protokollieren",
"LogCreditsHelp": "Protokolliere die Credit Kosten der AI Anfragen. Ohne diese Protokollierung können Nutzer unbgerenzt viele Anfragen stellen.",
"Log_Cooking": "Kochen protokollieren",
"Log_Recipe_Cooking": "Kochen protokollieren",
"Logo": "Logo",
@@ -308,6 +328,8 @@
"ModelSelectResultsHelp": "Für mehr Ergebnisse suchen",
"Monday": "Montag",
"Month": "Monat",
"MonthlyCredits": "Monatliche Credits",
"MonthlyCreditsUsed": "Monatliche Credits verwendet",
"More": "Mehr",
"Move": "Verschieben",
"MoveCategory": "Verschieben nach: ",
@@ -349,6 +371,8 @@
"Note": "Notiz",
"Number of Objects": "Anzahl von Objekten",
"Nutrition": "Nährwerte",
"NutritionsPerServing": "Nährwerte pro Portion",
"NutritionsPerServingHelp": "Manche Anwendungen spezifizieren nicht, ob Nährwerte pro Portion oder pro Rezept anzugeben sind. Standardmäßig werden Sie daher pro Rezept importiert. Wähle diese Option um Sie als pro Portion zu behandeln.",
"OfflineAlert": "Du bist offline. Deine Einkaufsliste wird nicht synchronisiert.",
"Ok": "Ok",
"OnHand": "Aktuell vorrätig",
@@ -461,6 +485,7 @@
"Servings": "Portionen",
"ServingsText": "Portionstext",
"Settings": "Einstellungen",
"SettingsOnlySuperuser": "Einige Einstellungen können nur vom Server Administrator verändert werden.",
"Share": "Teilen",
"ShopLater": "Später kaufen",
"ShopNow": "Jetzt kaufen",
@@ -484,17 +509,21 @@
"Show_as_header": "Als Überschrift",
"Single": "Einzeln",
"Size": "Größe",
"Skip": "Überspringen",
"Social_Authentication": "Login über Drittanbieter",
"Sort_by_new": "Nach Neueste sortieren",
"Source": "Quelle",
"SourceImportHelp": "Importiere JSON im schema.org/recipe format oder eine HTML Seite mit json+ld Rezept bzw. microdata.",
"SourceImportSubtitle": "Importiere JSON oder HTML manuell.",
"Space": "Space",
"SpaceHelp": "Alle deine Daten sind sicher in deinem Space gespeichert und können nur von dir und den anderen Mitgliedern genutzt werden.",
"SpaceLimitExceeded": "Dein Space hat ein Limit überschritten, manche Funktionen wurden eingeschränkt.",
"SpaceLimitReached": "Dieser Space hat ein Limit erreicht. Es können keine neuen Objekte von diesem Typ angelegt werden.",
"SpaceMemberHelp": "Füge Benutzer hinzu indem du Einladungen erstellst und Sie an die gewünschte Person sendest.",
"SpaceMembers": "Space Mitglieder",
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. ",
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. Füge weitere Nutzer mit Einladungslinks hinzu.",
"SpaceName": "Space Name",
"SpacePrivateObjectsHelp": "Einige Objekte sind Standardmäßig privat, können aber mit Mitgliedern deines Spaces geteilt werden.",
"SpaceSettings": "Space Einstellungen",
"Space_Cosmetic_Settings": "Kosmetische Einstellungen auf Space Ebene überschreiben die Einstellungen der einzelnen Nutzer.",
"Split": "Aufteilen",
@@ -606,6 +635,8 @@
"Week": "Woche",
"Week_Numbers": "Kalenderwochen",
"Welcome": "Willkommen",
"WelcomeSettingsHelp": "Bitte wähle die grundlegenden Einstellungen für deinen Space. Du kannst Sie später jederzeit in den Einstellungen ändern.",
"WelcometoTandoor": "Willkommen bei Tandoor",
"WorkingTime": "Arbeitszeit",
"Year": "Jahr",
"Yes": "Ja",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Λογαριασμός",
"Active": "",
"Add": "Προσθήκη",
"AddChild": "",
"AddFoodToShopping": "Προσθήκη του φαγητού {food} στη λίστα αγορών σας",
@@ -16,6 +18,12 @@
"Added_by": "Προστέθηκε από",
"Added_on": "Προστέθηκε στις",
"Advanced": "Για προχωρημένους",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Ευθυγράμμιση",
"Amount": "Ποσότητα",
"App": "Εφαρμογή",
@@ -57,6 +65,7 @@
"Comments_setting": "Εμφάνιση σχολίων",
"Completed": "Ολοκληρωμένο",
"Conversion": "Μετατροπή",
"ConvertUsingAI": "",
"Copy": "Αντιγραφή",
"Copy Link": "Αντιγραφή συνδέσμου",
"Copy Token": "Αντιγραφή token",
@@ -66,6 +75,8 @@
"Create": "Δημιουργία",
"Create Food": "Δημιουργία φαγητού",
"Create Recipe": "Δημιουργία συνταγής",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Δημιουργία εγγραφής στο πρόγραμμα γευμάτων",
"Create_New_Food": "Προσθήκη νέου φαγητού",
"Create_New_Keyword": "Προσθήκη νέας λέξης-κλειδί",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών",
"Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης",
"Created": "Δημιουργήθηκε",
"Credits": "",
"Current_Period": "Τρέχουσα περίοδος",
"Custom Filter": "Προσαρμοσμένο φίλτρο",
"CustomImageHelp": "Ανεβάστε μια εικόνα για να εμφανίζεται στην επισκόπηση χώρου",
@@ -143,10 +155,14 @@
"Food_Alias": "Ψευδώνυμο φαγητού",
"Food_Replace": "Αντικατάσταση Φαγητού",
"Foods": "Φαγητά",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
@@ -165,6 +181,9 @@
"Image": "Εικόνα",
"Import": "Εισαγωγή",
"Import Recipe": "Εισαγωγή συνταγής",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Συνέβη ένα σφάλμα κατά την εισαγωγή. Για να το δείτε, εμφανίστε τις λεπτομέρειες στο κάτω μέρος της σελίδας.",
"Import_Not_Yet_Supported": "Η εισαγωγή δεν υποστηρίζεται ακόμη",
"Import_Result_Info": "Έγινε εισαγωγή {imported} από τις {total} συνταγές",
@@ -195,8 +214,11 @@
"Language": "Γλώσσα",
"Last_name": "Επίθετο",
"Learn_More": "Μάθετε περισσότερα",
"LeaveSpace": "",
"Link": "Σύνδεσμος",
"Load_More": "Φόρτωση περισσότερων",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Καταγραφή μαγειρέματος",
"Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής",
"Logo": "Λογότυπο",
@@ -216,6 +238,8 @@
"Message": "Μήνυμα",
"MissingProperties": "",
"Month": "Μήνας",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Μετακίνηση",
"MoveCategory": "Μετακίνηση σε: ",
"Move_Down": "Μετακίνηση κάτω",
@@ -252,6 +276,8 @@
"Note": "Σημείωση",
"Number of Objects": "Αριθμός αντικειμένων",
"Nutrition": "Διατροφική αξία",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Είστε εκτός σύνδεσης, η λίστα αγορών μπορεί να μην συγχρονιστεί.",
"Ok": "ΟΚ",
"OnHand": "Τώρα διαθέσιμα",
@@ -328,6 +354,7 @@
"Selected": "Επιλεγμένο",
"Servings": "Μερίδες",
"Settings": "Ρυθμίσεις",
"SettingsOnlySuperuser": "",
"Share": "Κοινοποίηση",
"ShoppingBackgroundSyncWarning": "Κακό δίκτυο, αναμονή συγχρονισμού...",
"Shopping_Categories": "Κατηγορίες αγορών",
@@ -344,9 +371,14 @@
"Show_as_header": "Εμφάνιση ως κεφαλίδα",
"Single": "Ενικός",
"Size": "Μέγεθος",
"Skip": "",
"Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων",
"Sort_by_new": "Ταξινόμηση κατά νέο",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
"StartDate": "Ημερομηνία Έναρξης",
@@ -413,6 +445,8 @@
"Week": "Εβδομάδα",
"Week_Numbers": "Αριθμοί εδομάδων",
"Welcome": "Καλώς ήρθατε",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Έτος",
"Yes": "",
"add_keyword": "Προσθήκη λέξης-κλειδί",

View File

@@ -1,13 +1,16 @@
{
"AI": "AI",
"AIImportSubtitle": "Use AI to import images of recipes.",
"AISettingsHostedHelp": "You can enable AI features or change available credits by managing your subscription.",
"API": "API",
"APIKey": "API key",
"API_Browser": "API Browser",
"API_Documentation": "API Docs",
"AccessTokenHelp": "Access keys for the REST API.",
"Access_Token": "Access Token",
"Account": "Account",
"Actions": "Actions",
"Active": "Active",
"Activity": "Activity",
"Add": "Add",
"AddAll": "Add all",
@@ -26,6 +29,12 @@
"Added_on": "Added On",
"Admin": "Admin",
"Advanced": "Advanced",
"AiCreditsBalance": "Credit Balance",
"AiLog": "AI Log",
"AiLogHelp": "Overview of your spaces AI requests. ",
"AiModelHelp": "The list contains model that are offically tested and supported. You can add additional models if you want.",
"AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment",
"AllRecipes": "All Recipes",
"Amount": "Amount",
@@ -87,6 +96,7 @@
"Continue": "Continue",
"Conversion": "Conversion",
"ConversionsHelp": "With conversions you can calculate the amount of a food in different units. Currently this is only used for property calculation, later it might also be used in other parts of tandoor. ",
"ConvertUsingAI": "Convert using AI",
"CookLog": "Cook Log",
"CookLogHelp": "Entries in the cook log for recipes. ",
"Cooked": "Cooked",
@@ -100,6 +110,8 @@
"Create": "Create",
"Create Food": "Create Food",
"Create Recipe": "Create Recipe",
"CreateFirstRecipe": "Create your first recipe using the recipe editor.",
"CreateInvitation": "Create invitation",
"Create_Meal_Plan_Entry": "Create meal plan entry",
"Create_New_Food": "Add New Food",
"Create_New_Keyword": "Add New Keyword",
@@ -109,6 +121,7 @@
"Create_New_Unit": "Add New Unit",
"Created": "Created",
"CreatedBy": "Created by",
"Credits": "Credits",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Current Period",
"Custom Filter": "Custom Filter",
@@ -202,11 +215,15 @@
"Food_Replace": "Food Replace",
"Foods": "Foods",
"Friday": "Friday",
"FromBalance": "From Balance",
"Fulltext": "Fulltext",
"FulltextHelp": "Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use fuzzy search to find entries even when there are differences in how the word is written.",
"GettingStarted": "Getting Started",
"Global": "Global",
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
"Group": "Group",
"GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline",
@@ -232,7 +249,10 @@
"Import": "Import",
"Import Recipe": "Import Recipe",
"ImportAll": "Import all",
"ImportFirstRecipe": "Import your first recipe from one of thousands of websites or use one of the other importers to import your existing collection, documents or URL lists.",
"ImportIntoTandoor": "Import into Tandoor",
"ImportMealPlans": "Import mealplans",
"ImportShoppingList": "Import shoppinglists",
"Import_Error": "An Error occurred during your import. Please expand the Details at the bottom of the page to view it.",
"Import_Not_Yet_Supported": "Import not yet supported",
"Import_Result_Info": "{imported} of {total} recipes were imported",
@@ -271,9 +291,12 @@
"Last": "Last",
"Last_name": "Last Name",
"Learn_More": "Learn More",
"LeaveSpace": "Leave Space",
"Link": "Link",
"Load": "Load",
"Load_More": "Load More",
"LogCredits": "Log Credits.",
"LogCreditsHelp": "Log credit cost of AI requests. Without this users can perform as many AI requests as they want. ",
"Log_Cooking": "Log Cooking",
"Log_Recipe_Cooking": "Log Recipe Cooking",
"Logo": "Logo",
@@ -303,6 +326,8 @@
"ModelSelectResultsHelp": "Search for more results",
"Monday": "Monday",
"Month": "Month",
"MonthlyCredits": "Monthly Credits",
"MonthlyCreditsUsed": "Monthly credits used",
"More": "More",
"Move": "Move",
"MoveCategory": "Move To: ",
@@ -344,6 +369,8 @@
"Note": "Note",
"Number of Objects": "Number of Objects",
"Nutrition": "Nutrition",
"NutritionsPerServing": "Nutritions per Serving",
"NutritionsPerServingHelp": "Some applications do not specify if nutritions are per recipe or per serving. By default Tandoor treats them as per recipe. Check this box to treat them as per serving. ",
"OfflineAlert": "You are offline, shopping list may not syncronize.",
"Ok": "Ok",
"OnHand": "Currently On Hand",
@@ -456,6 +483,7 @@
"Servings": "Servings",
"ServingsText": "Servings Text",
"Settings": "Settings",
"SettingsOnlySuperuser": "Some Settings can only be changed by the Server Administrator.",
"Share": "Share",
"ShopLater": "Shop later",
"ShopNow": "Shop now",
@@ -479,17 +507,21 @@
"Show_as_header": "Show as header",
"Single": "Single",
"Size": "Size",
"Skip": "Skip",
"Social_Authentication": "Social Authentication",
"Sort_by_new": "Sort by new",
"Source": "Source",
"SourceImportHelp": "Import JSON in schema.org/recipe format or html pages with json+ld recipe or microdata.",
"SourceImportSubtitle": "Import JSON or HTML manually.",
"Space": "Space",
"SpaceHelp": "All your data is part of your space and can only be acccessed by space members. ",
"SpaceLimitExceeded": "Your space has surpassed one of its limits, some functions might be restricted.",
"SpaceLimitReached": "This Space has reached a limit. No more objects of this type can be created.",
"SpaceMemberHelp": "Add users to your space by creating an Invite Link and sending it to the person you want to add.",
"SpaceMembers": "Space Members",
"SpaceMembersHelp": "Users and their permissions in a space. ",
"SpaceMembersHelp": "Users and their permissions in a space. Add additional users using invite links.",
"SpaceName": "Space Name",
"SpacePrivateObjectsHelp": " Some things are private by default an can be shared with members of your space.",
"SpaceSettings": "Space Settings",
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
"Split": "Split",
@@ -601,6 +633,8 @@
"Week": "Week",
"Week_Numbers": "Week numbers",
"Welcome": "Welcome",
"WelcomeSettingsHelp": "Please choose the basic settings for your Tandoor space. You can change all of these later trough the settings.",
"WelcometoTandoor": "Welcome to Tandoor",
"WorkingTime": "Working time",
"Year": "Year",
"Yes": "Yes",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Usar IA para importar imágenes de recetas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Token de acceso",
"Account": "Cuenta",
"Actions": "Acciones",
"Active": "",
"Activity": "Actividad",
"Add": "Añadir",
"AddAll": "Agregar todo",
@@ -26,6 +28,12 @@
"Added_on": "Añadido el",
"Admin": "Administrador",
"Advanced": "Avanzado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineación",
"AllRecipes": "Todas las recetas",
"Amount": "Cantidad",
@@ -85,6 +93,7 @@
"Continue": "Continuar",
"Conversion": "Conversión",
"ConversionsHelp": "Con las conversiones puedes calcular la cantidad de un alimento en diferentes unidades. Actualmente esto solo se usa para el cálculo de propiedades, en un futuro podría ser usado en otras partes de Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Historial de cocina",
"CookLogHelp": "Entradas en el historial de cocina para recetas. ",
"Cooked": "Cocinado",
@@ -98,6 +107,8 @@
"Create": "Crear",
"Create Food": "Crear alimento",
"Create Recipe": "Crear receta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crear entrada de plan de comidas",
"Create_New_Food": "Añadir nuevo alimento",
"Create_New_Keyword": "Añadir nueva palabra clave",
@@ -107,6 +118,7 @@
"Create_New_Unit": "Añadir nueva unidad",
"Created": "Creada",
"CreatedBy": "Creado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo actual",
"Custom Filter": "Filtro personalizado",
@@ -200,7 +212,11 @@
"Food_Replace": "Sustituir Alimento",
"Foods": "Alimentos",
"Friday": "Viernes",
"FromBalance": "",
"GettingStarted": "Primeros pasos",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado",
@@ -224,7 +240,10 @@
"Import": "Importar",
"Import Recipe": "Importar Receta",
"ImportAll": "Importar todo",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importar a Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ocurrió un Error ocurrió durante la importación. Por favor, expanda los Detalles al final de la página para verlo.",
"Import_Not_Yet_Supported": "Importación no soportada todavía",
"Import_Result_Info": "{imported} de {total} recetas fueron importadas",
@@ -263,9 +282,12 @@
"Last": "Último",
"Last_name": "Apellidos",
"Learn_More": "Saber Más",
"LeaveSpace": "",
"Link": "Enlace",
"Load": "Cargar",
"Load_More": "Cargar más",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar cocinada",
"Log_Recipe_Cooking": "Registro de recetas",
"Logo": "Logotipo",
@@ -294,6 +316,8 @@
"ModelSelectResultsHelp": "Buscar más resultados",
"Monday": "Lunes",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Más",
"Move": "Mover",
"MoveCategory": "Mover a: ",
@@ -335,6 +359,8 @@
"Note": "Nota",
"Number of Objects": "Número de Objetos",
"Nutrition": "Nutrición",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Estas desconectado, la lista de la compra puede no sincronizarse.",
"Ok": "Ok",
"OnHand": "Actualmente en Posesión",
@@ -441,6 +467,7 @@
"Servings": "Raciones",
"ServingsText": "Texto de la porción",
"Settings": "Opciones",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShopLater": "Comprar después",
"ShopNow": "Comprar ahora",
@@ -463,16 +490,20 @@
"Show_as_header": "Mostrar como encabezado",
"Single": "Simple",
"Size": "Tamaño",
"Skip": "",
"Social_Authentication": "Autenticación Social",
"Sort_by_new": "Ordenar por novedades",
"SourceImportHelp": "Importar JSON en formato schema.org/recipe o páginas HTML con recetas en formato JSON+LD o microdatos.",
"SourceImportSubtitle": "Importar JSON o HTML manualmente.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Tu espacio ha sobrepasado uno de sus límites, algunas funciones podrían estar restringidas.",
"SpaceLimitReached": "Este espacio ha alcanzado un límite. No se pueden crear más objetos de este tipo.",
"SpaceMemberHelp": "Agrega usuarios a tu espacio creando un enlace de invitación y enviándolo a la persona que quieras agregar.",
"SpaceMembers": "Miembros del espacio",
"SpaceMembersHelp": "Usuarios y sus permisos en un espacio. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Ajustes del espacio",
"Space_Cosmetic_Settings": "Algunos ajustes de apariencia pueden ser cambiados por los administradores del espacio y anularán los ajustes del cliente para ese espacio.",
"Split": "Dividir",
@@ -578,6 +609,8 @@
"Week": "Semana",
"Week_Numbers": "numero de semana",
"Welcome": "Bienvenido/a",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Tiempo de trabajo",
"Year": "Año",
"Yes": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Tili",
"Active": "",
"Add": "Lisää",
"AddChild": "",
"AddFoodToShopping": "Lisää {food} ostoslistaan",
@@ -17,6 +19,12 @@
"Added_on": "Lisätty",
"Advanced": "Edistynyt",
"Advanced Search Settings": "Tarkennetun Haun Asetukset",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Tasaus",
"Amount": "Määrä",
"App": "Applikaatio",
@@ -55,6 +63,7 @@
"Comments_setting": "Näytä Kommentit",
"Completed": "Valmis",
"Conversion": "Muuntaminen",
"ConvertUsingAI": "",
"Copy": "Kopioi",
"Copy Link": "Kopioi Linkki",
"Copy Token": "Kopioi Token",
@@ -63,6 +72,8 @@
"CountMore": "...+{count} enemmän",
"Create": "Luo",
"Create Food": "Luo Ruoka",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Luo ateriasuunnitelma merkintä",
"Create_New_Food": "Lisää Uusi Ruoka",
"Create_New_Keyword": "Lisää Uusi Avainsana",
@@ -71,6 +82,7 @@
"Create_New_Shopping_Category": "Lisää uusi ostoskategoria",
"Create_New_Unit": "Lisää Uusi Yksikkö",
"Created": "Luotu",
"Credits": "",
"Current_Period": "Nykyinen Jakso",
"Custom Filter": "Mukautettu Suodatin",
"CustomImageHelp": "Lataa kuva näytettäväksi tilan yleiskatsauksessa.",
@@ -140,10 +152,14 @@
"Food_Alias": "Ruoan nimimerkki",
"Food_Replace": "Korvaa Ruoka",
"Foods": "Ruuat",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana",
@@ -162,6 +178,9 @@
"Image": "Kuva",
"Import": "Tuo",
"Import Recipe": "Tuo Resepti",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Not_Yet_Supported": "Tuontia ei vielä tueta",
"Import_Supported": "Tuonti tuettu",
"Import_finished": "Tuonti valmistui",
@@ -189,8 +208,11 @@
"Language": "Kieli",
"Last_name": "Sukunimi",
"Learn_More": "Lisätietoja",
"LeaveSpace": "",
"Link": "Linkki",
"Load_More": "Lataa Lisää",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Kirjaa kokkaus",
"Log_Recipe_Cooking": "Kirjaa Reseptin valmistus",
"Logo": "Logo",
@@ -210,6 +232,8 @@
"Message": "Viesti",
"MissingProperties": "",
"Month": "Kuukausi",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Siirry",
"MoveCategory": "Siirrä paikkaan: ",
"Move_Down": "Siirry alas",
@@ -241,6 +265,8 @@
"Note": "Lisätiedot",
"Number of Objects": "Objektien määrä",
"Nutrition": "Ravitsemus",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Olet offline-tilassa, ostoslista ei välttämättä synkronoidu.",
"Ok": "Ok",
"OnHand": "Tällä hetkellä saatavilla",
@@ -317,6 +343,7 @@
"Selected": "Valittu",
"Servings": "Annokset",
"Settings": "Asetukset",
"SettingsOnlySuperuser": "",
"Share": "Jaa",
"ShoppingBackgroundSyncWarning": "Huono verkkoyhteys, odotetaan synkronointia ...",
"Shopping_Categories": "Ostoskategoriat",
@@ -332,9 +359,14 @@
"Show_as_header": "Näytä otsikkona",
"Single": "Yksittäinen",
"Size": "Koko",
"Skip": "",
"Social_Authentication": "Sosiaalinen Todennus",
"Sort_by_new": "Lajittele uusien mukaan",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
"StartDate": "Aloituspäivä",
"Starting_Day": "Viikon aloituspäivä",
@@ -393,6 +425,8 @@
"Week": "Viikko",
"Week_Numbers": "Viikkonumerot",
"Welcome": "Tervetuloa",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Vuosi",
"Yes": "",
"add_keyword": "Lisää Avainsana",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utiliser l'IA pour importer des images de recettes.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Jeton d'accès",
"Account": "Compte",
"Actions": "Actions",
"Active": "",
"Activity": "Activité",
"Add": "Ajouter",
"AddAll": "Tout ajouter",
@@ -27,6 +29,12 @@
"Admin": "Admin",
"Advanced": "Avancé",
"Advanced Search Settings": "Paramètres de recherche avancée",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alignement",
"AllRecipes": "Toutes les recettes",
"Amount": "Quantité",
@@ -88,6 +96,7 @@
"Continue": "Continuer",
"Conversion": "Conversion",
"ConversionsHelp": "Avec les conversions, vous pouvez calculer une quantité dans différentes unités. Actuellement, c'est utilisé uniquement pour le calcul des propriétés, mais ça pourrait être utilisé dans d'autres parties de Tandoor dans le futur. ",
"ConvertUsingAI": "",
"CookLog": "Journal de cuisine",
"CookLogHelp": "Entrées dans le journal de cuisine pour les recettes. ",
"Cooked": "Cuit",
@@ -101,6 +110,8 @@
"Create": "Créer",
"Create Food": "Créer un aliment",
"Create Recipe": "Créer une recette",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Créer une entrée de menu",
"Create_New_Food": "Ajouter un nouvel aliment",
"Create_New_Keyword": "Ajouter un nouveau mot-clé",
@@ -110,6 +121,7 @@
"Create_New_Unit": "Ajouter une nouvelle unité",
"Created": "Créé",
"CreatedBy": "Créé par",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Période actuelle",
"Custom Filter": "Filtre personnalisé",
@@ -203,11 +215,15 @@
"Food_Replace": "Remplacer l'aliment",
"Foods": "Aliments",
"Friday": "Vendredi",
"FromBalance": "",
"Fulltext": "Texte intégral",
"FulltextHelp": "Champs de recherche en texte intégral. Remarque : les méthodes de recherche \"web\", \"phrase\" et \"raw\" ne fonctionnent qu'avec des champs en texte intégral.",
"Fuzzy": "Approximatif",
"FuzzySearchHelp": "Utilisez la recherche approximative pour trouver des entrées même lorsqu'il existe des différences dans la façon dont le mot est écrit.",
"GettingStarted": "Commencer",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête",
@@ -233,7 +249,10 @@
"Import": "Importer",
"Import Recipe": "Importer une recette",
"ImportAll": "Tout importer",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importer dans Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Une erreur est survenue pendant votre importation. Veuillez développer les détails au bas de la page pour la consulter.",
"Import_Not_Yet_Supported": "Importation pas encore prise en charge",
"Import_Result_Info": "{imported} sur {total} recettes ont été importées",
@@ -272,9 +291,12 @@
"Last": "Dernier",
"Last_name": "Nom",
"Learn_More": "Apprenez-en plus",
"LeaveSpace": "",
"Link": "Lien",
"Load": "Chargement",
"Load_More": "Charger plus",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Marquer comme cuisiné",
"Log_Recipe_Cooking": "Marquer la recette comme cuisinée",
"Logo": "Logo",
@@ -301,6 +323,8 @@
"ModelSelectResultsHelp": "Chercher plus de résultats",
"Monday": "Lundi",
"Month": "Mois",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Plus",
"Move": "Déplacer",
"MoveCategory": "Déplacer vers : ",
@@ -342,6 +366,8 @@
"Note": "Notes",
"Number of Objects": "Nombre d'objets",
"Nutrition": "Valeurs nutritionnelles",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Vous êtes déconnecté, votre liste de courses peut ne pas être synchronisée.",
"Ok": "D'accord",
"OnHand": "Disponible actuellement",
@@ -454,6 +480,7 @@
"Servings": "Portions",
"ServingsText": "Texte des portions",
"Settings": "Paramètres",
"SettingsOnlySuperuser": "",
"Share": "Partager",
"ShopLater": "Acheter plus tard",
"ShopNow": "Acheter maintenant",
@@ -477,17 +504,21 @@
"Show_as_header": "Montrer comme en-tête",
"Single": "Unique",
"Size": "Taille",
"Skip": "",
"Social_Authentication": "Authentification Sociale",
"Sort_by_new": "Trier par nouveautés",
"Source": "Source",
"SourceImportHelp": "Importez du JSON au format schema.org/recipe ou des pages HTML avec une recette json+ld ou des microdonnées.",
"SourceImportSubtitle": "Importez en JSON ou HTML manuellement.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Votre groupe a dépassé une de ses limites, certaines fonctions pourraient être restreintes.",
"SpaceLimitReached": "Ce groupe a atteint sa limite. Aucun nouvel objet de ce type ne peut être créé.",
"SpaceMemberHelp": "Ajoutez des utilisateurs à votre espace en créant un lien d'invitation et en l'envoyant à la personne que vous souhaitez ajouter.",
"SpaceMembers": "Membres du groupe",
"SpaceMembersHelp": "Utilisateurs et permissions dans un groupe. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Paramètres du groupe",
"Space_Cosmetic_Settings": "Certains paramètres cosmétiques peuvent être modifiés par un administrateur de l'espace et seront prioritaires sur les paramètres des utilisateurs pour cet espace.",
"Split": "Diviser",
@@ -597,6 +628,8 @@
"Week": "Semaine",
"Week_Numbers": "Numéro de semaine",
"Welcome": "Bienvenue",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Temps de préparation",
"Year": "Année",
"Yes": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "חשבון",
"Active": "",
"Add": "הוספה",
"AddChild": "",
"AddFoodToShopping": "הוסף {מזון} לרשימת הקניות",
@@ -16,6 +18,12 @@
"Added_by": "נוסף ע\"י",
"Added_on": "נוסף ב",
"Advanced": "מתקדם",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "יישור",
"Amount": "כמות",
"App": "אפליקציה",
@@ -57,6 +65,7 @@
"Comments_setting": "הצג תגובות",
"Completed": "הושלם",
"Conversion": "עברית",
"ConvertUsingAI": "",
"Copy": "העתקה",
"Copy Link": "העתק קישור",
"Copy Token": "העתק טוקן",
@@ -66,6 +75,8 @@
"Create": "יצירה",
"Create Food": "צור מאכל",
"Create Recipe": "צור מתכון",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "צור רשימת תכנון אוכל",
"Create_New_Food": "הוסף אוכל חדש",
"Create_New_Keyword": "הוסף מילת מפתח",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "הוסף קטגוריות קניות חדשה",
"Create_New_Unit": "הוסף יחידה",
"Created": "נוצר",
"Credits": "",
"Current_Period": "תקופה נוכחית",
"Custom Filter": "פילטר מותאם",
"CustomImageHelp": "העלאת תמונה שתראה באזור הסקירה.",
@@ -143,10 +155,14 @@
"Food_Alias": "שם כינוי לאוכל",
"Food_Replace": "החלף אוכל",
"Foods": "מאכלים",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח",
@@ -165,6 +181,9 @@
"Image": "תמונה",
"Import": "ייבוא",
"Import Recipe": "ייבא מתכון",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "שגיאה בעת ייבוא. הרחב את הפירוט בסוף עמוד זה לראות מידע נוסף.",
"Import_Not_Yet_Supported": "ייבוא לא נתמך עדיין",
"Import_Result_Info": "{imported} מתוך {total} מתכונים יובאו",
@@ -195,8 +214,11 @@
"Language": "שפה",
"Last_name": "שם משפחה",
"Learn_More": "למד עוד",
"LeaveSpace": "",
"Link": "קישור",
"Load_More": "טען עוד",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "רשום הכנת מתכון",
"Log_Recipe_Cooking": "רשום בישול מתכון",
"Logo": "לוגו",
@@ -216,6 +238,8 @@
"Message": "הודעה",
"MissingProperties": "",
"Month": "חודש",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "העברה",
"MoveCategory": "העבר אל: ",
"Move_Down": "העברה למטה",
@@ -252,6 +276,8 @@
"Note": "הערה",
"Number of Objects": "מספר אובייקטים",
"Nutrition": "תזונה",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "אתה במצב מנותק, רשימת הקניות לא בהכרח מסונכרנת.",
"Ok": "אישור",
"OnHand": "כרגע נגיש",
@@ -328,6 +354,7 @@
"Selected": "נבחר",
"Servings": "מנות",
"Settings": "הגדרות",
"SettingsOnlySuperuser": "",
"Share": "שיתוף",
"ShoppingBackgroundSyncWarning": "בעיית תקשורת, מחכה לסנכון...",
"Shopping_Categories": "קטגוריות קניות",
@@ -344,9 +371,14 @@
"Show_as_header": "הצג בתור כותרת",
"Single": "בודד",
"Size": "גודל",
"Skip": "",
"Social_Authentication": "אימות חברתי",
"Sort_by_new": "סדר ע\"י חדש",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
"StartDate": "תאריך התחלה",
@@ -413,6 +445,8 @@
"Week": "שבוע",
"Week_Numbers": "מספר השבוע",
"Welcome": "ברוכים הבאים",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "שנה",
"Yes": "",
"add_keyword": "הוסף מילת מפתח",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Korisnički račun",
"Active": "",
"Add": "Dodaj",
"AddChild": "",
"AddFoodToShopping": "Dodaj {food} na svoj popis za kupovinu",
@@ -16,6 +18,12 @@
"Added_by": "Dodao",
"Added_on": "Dodano",
"Advanced": "Napredno",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnanje",
"Amount": "Količina",
"App": "Aplikacija",
@@ -57,6 +65,7 @@
"Comments_setting": "Prikaži komentare",
"Completed": "Završeno",
"Conversion": "Konverzija",
"ConvertUsingAI": "",
"Copy": "Kopiraj",
"Copy Link": "Kopiraj vezu",
"Copy Token": "Kopiraj token",
@@ -66,6 +75,8 @@
"Create": "Stvori",
"Create Food": "Kreiraj namirnicu",
"Create Recipe": "Kreiraj recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Kreirajte unos plana obroka",
"Create_New_Food": "Dodaj novu namirnicu",
"Create_New_Keyword": "Dodaj novu ključnu riječ",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "Dodaj novu kategoriju za kupovinu",
"Create_New_Unit": "Dodaj novu jedinicu",
"Created": "Stvoreno",
"Credits": "",
"Current_Period": "Trenutno razdoblje",
"Custom Filter": "Prilagođeni filtar",
"CustomImageHelp": "Učitaj sliku za prikaz u pregledu prostora.",
@@ -143,10 +155,14 @@
"Food_Alias": "Nadimci namirnice",
"Food_Replace": "Zamjena namirnica",
"Foods": "Namirnice",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi",
@@ -165,6 +181,9 @@
"Image": "Slika",
"Import": "Uvoz",
"Import Recipe": "Uvezi recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Došlo je do pogreške tijekom uvoza. Molimo proširite pojedinosti na dnu stranice kako bi vidjeli grešku.",
"Import_Not_Yet_Supported": "Uvoz još nije podržan",
"Import_Result_Info": "Uvezeno je {imported} od {total} recepata",
@@ -195,8 +214,11 @@
"Language": "Jezik",
"Last_name": "Prezime",
"Learn_More": "Saznajte više",
"LeaveSpace": "",
"Link": "Poveznica",
"Load_More": "Učitaj više",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zapis kuhanja",
"Log_Recipe_Cooking": "Dnevnik recepata kuhanja",
"Logo": "Logotip",
@@ -216,6 +238,8 @@
"Message": "Poruka",
"MissingProperties": "",
"Month": "Mjesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Premjesti",
"MoveCategory": "Premjesti u: ",
"Move_Down": "Premjesti dolje",
@@ -252,6 +276,8 @@
"Note": "Bilješka",
"Number of Objects": "Broj objekata",
"Nutrition": "Nutritivna vrijednost",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Nisi na mreži, popis za kupnju se možda neće sinkronizirati.",
"Ok": "Ok",
"OnHand": "Trenutno pri ruci",
@@ -328,6 +354,7 @@
"Selected": "Odabrano",
"Servings": "Porcije",
"Settings": "Postavke",
"SettingsOnlySuperuser": "",
"Share": "Podijeli",
"ShoppingBackgroundSyncWarning": "Loša mreža, čeka se sinkronizacija...",
"Shopping_Categories": "Kategorije Kupovine",
@@ -344,9 +371,14 @@
"Show_as_header": "Prikaži kao zaglavlje",
"Single": "Jedna",
"Size": "Veličina",
"Skip": "",
"Social_Authentication": "Autentifikacija putem društvenih mreža",
"Sort_by_new": "Poredaj po novom",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.",
"Split_All_Steps": "Podijeli sve retke u zasebne korake.",
"StartDate": "Početni datum",
@@ -413,6 +445,8 @@
"Week": "Tjedan",
"Week_Numbers": "Brojevi tjedana",
"Welcome": "Dobrodošli",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Godina",
"Yes": "",
"add_keyword": "Dodaj ključnu riječ",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Fiók",
"Active": "",
"Add": "Hozzáadás",
"AddChild": "",
"AddFoodToShopping": "{food} hozzáadása bevásárlólistához",
@@ -16,6 +18,12 @@
"Added_by": "Hozzádta",
"Added_on": "Hozzáadva",
"Advanced": "Haladó",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Igazítás",
"Amount": "Összeg",
"App": "Applikáció",
@@ -56,6 +64,7 @@
"Comments_setting": "Hozzászólások megjelenítése",
"Completed": "Kész",
"Conversion": "Konverzió",
"ConvertUsingAI": "",
"Copy": "Másolás",
"Copy Link": "Link másolása",
"Copy Token": "Token másolása",
@@ -64,6 +73,8 @@
"Create": "Létrehozás",
"Create Food": "Alapanyag létrehozása",
"Create Recipe": "Recept létrehozása",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Menüterv bejegyzés létrehozása",
"Create_New_Food": "Új alapanyag hozzáadása",
"Create_New_Keyword": "Új kulcsszó hozzáadása",
@@ -71,6 +82,7 @@
"Create_New_Shopping Category": "Új vásárlási kategória létrehozása",
"Create_New_Shopping_Category": "Új vásárlási kategória hozzáadása",
"Create_New_Unit": "Új mértékegység hozzáadása",
"Credits": "",
"Current_Period": "Jelenlegi periódus",
"Custom Filter": "Egyéni szűrő",
"DELETE_ERROR": "",
@@ -126,10 +138,14 @@
"Food_Alias": "",
"Food_Replace": "Étel cseréje",
"Foods": "Alapanyagok",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése",
@@ -148,6 +164,9 @@
"Image": "Kép",
"Import": "Import",
"Import Recipe": "Recept importálása",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Hiba történt az importálás során. Kérjük, a megtekintéshez bontsa ki az oldal alján található Részletek menüpontot.",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "{total}-ból/ből {imported} recept importálva",
@@ -177,8 +196,11 @@
"Language": "Nyelv",
"Last_name": "Vezetéknév",
"Learn_More": "Tudjon meg többet",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Továbbiak betöltése",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Főzés naplózása",
"Log_Recipe_Cooking": "Főzés naplózása",
"Make_Header": "Átalakítás címsorra",
@@ -197,6 +219,8 @@
"Message": "Üzenet",
"MissingProperties": "",
"Month": "Hónap",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mozgatás",
"MoveCategory": "Áthelyezés ide: ",
"Move_Down": "Lefelé mozgatás",
@@ -229,6 +253,8 @@
"Note": "Megjegyzés",
"Number of Objects": "Objektumok száma",
"Nutrition": "Tápérték",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Ön éppen offline állapotban van, a bevásárlólista nem biztos, hogy szinkronizálódik.",
"Ok": "Ok",
"OnHand": "Jelenleg készleten",
@@ -301,6 +327,7 @@
"Selected": "Kiválasztott",
"Servings": "Adag",
"Settings": "Beállítások",
"SettingsOnlySuperuser": "",
"Share": "Megosztás",
"Shopping_Categories": "Vásárlási kategóriák",
"Shopping_Category": "Vásárlási kategória",
@@ -312,8 +339,13 @@
"Show_as_header": "Megjelenítés címként",
"Single": "Egyetlen",
"Size": "Méret",
"Skip": "",
"Sort_by_new": "Rendezés legújabbak szerint",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.",
"StartDate": "Kezdés dátuma",
"Starting_Day": "A hét kezdőnapja",
@@ -373,6 +405,8 @@
"Week": "Hét",
"Week_Numbers": "",
"Welcome": "Üdvözöljük",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Év",
"Yes": "",
"add_keyword": "Kulcsszó hozzáadása",

View File

@@ -1,6 +1,8 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "",
"AddChild": "",
"Add_nutrition_recipe": "Ավելացնել սննդայնություն բաղադրատոմսին",
@@ -8,6 +10,12 @@
"Add_to_Plan": "Ավելացնել պլանին",
"Add_to_Shopping": "Ավելացնել գնումներին",
"Advanced Search Settings": "Ընդլայնված փնտրման կարգավորումներ",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Apply": "",
"Automate": "Ավտոմատացնել",
"BatchDeleteConfirm": "",
@@ -22,11 +30,15 @@
"Categories": "",
"Category": "",
"Close": "",
"ConvertUsingAI": "",
"Copy": "",
"Create": "Ստեղծել",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
"Credits": "",
"DELETE_ERROR": "",
"Date": "",
"Delete": "",
@@ -51,10 +63,14 @@
"File": "",
"Files": "",
"Food": "Սննդամթերք",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
@@ -63,12 +79,18 @@
"IgnoreAccents": "",
"IgnoreAccentsHelp": "",
"Import": "Ներմուծել",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_finished": "Ներմուծումն ավարտված է",
"Information": "Տեղեկություն",
"Ingredients": "",
"Keywords": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Գրանցել եփելը",
"Log_Recipe_Cooking": "Գրանցել բաղադրատոմսի օգտագործում",
"ManageSubscription": "",
@@ -78,6 +100,8 @@
"MergeAutomateHelp": "",
"Merge_Keyword": "Միացնել բանալի բառը",
"MissingProperties": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Տեղափոխել",
"Move_Food": "Տեղափոխել սննդամթերքը",
"Move_Keyword": "Տեղափոխել բանալի բառը",
@@ -90,6 +114,8 @@
"NoUnit": "",
"No_Results": "Արդյունքներ չկան",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"Ok": "",
"Open": "",
"Parent": "Ծնող",
@@ -124,13 +150,19 @@
"Selected": "",
"Servings": "",
"Settings": "Կարգավորումներ",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Category": "Գնումների կատեգորիա",
"Shopping_list": "Գնումների ցուցակ",
"Show_as_header": "Ցույց տալ որպես խորագիր",
"Size": "",
"Skip": "",
"Sort_by_new": "Տեսակավորել ըստ նորերի",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"StartsWith": "",
"StartsWithHelp": "",
"Step": "",
@@ -150,6 +182,8 @@
"View_Recipes": "Դիտել բաղադրատոմսերը",
"Visibility": "",
"Waiting": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Yes": "",
"all_fields_optional": "Բոլոր տողերը կամավոր են և կարող են մնալ դատարկ։",
"and": "և",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "Tambahkan",
"AddChild": "",
"AddFoodToShopping": "",
@@ -16,6 +18,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -48,6 +56,7 @@
"Coming_Soon": "",
"Comments_setting": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "Salin",
"Copy Link": "Salin Tautan",
"Copy Token": "Salin Token",
@@ -56,6 +65,8 @@
"CountMore": "",
"Create": "Membuat",
"Create Food": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -63,6 +74,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -114,10 +126,14 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -135,6 +151,9 @@
"IgnoredFood": "",
"Image": "Gambar",
"Import": "Impor",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -162,8 +181,11 @@
"Keywords": "Kata Kunci",
"Language": "",
"Last_name": "",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Muat lebih banyak",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Log Memasak",
"Log_Recipe_Cooking": "Log Resep Memasak",
"Make_Header": "Buat Header",
@@ -182,6 +204,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Bergerak",
"MoveCategory": "",
"Move_Down": "Pindahkan kebawah",
@@ -212,6 +236,8 @@
"NotInShopping": "",
"Note": "Catatan",
"Nutrition": "Nutrisi",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "Membuka",
"OnHand": "",
@@ -277,6 +303,7 @@
"Selected": "Terpilih",
"Servings": "Porsi",
"Settings": "Pengaturan",
"SettingsOnlySuperuser": "",
"Share": "Bagikan",
"Shopping_Categories": "Kategori Belanja",
"Shopping_Category": "Kategori Belanja",
@@ -288,9 +315,14 @@
"Show_as_header": "Tampilkan sebagai tajuk",
"Single": "",
"Size": "Ukuran",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Urutkan berdasarkan baru",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",
@@ -340,6 +372,8 @@
"Website": "",
"Week": "",
"Week_Numbers": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -16,6 +18,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +65,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -66,6 +75,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -142,10 +154,14 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -164,6 +180,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -194,8 +213,11 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
@@ -215,6 +237,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -251,6 +275,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -327,6 +353,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",
@@ -342,9 +369,14 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"StartDate": "",
@@ -411,6 +443,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utilizza IA per importare le immagini delle ricette.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Token di accesso",
"Account": "Account",
"Actions": "Azioni",
"Active": "",
"Activity": "Attività",
"Add": "Aggiungi",
"AddAll": "Aggiungi tutto",
@@ -27,6 +29,12 @@
"Admin": "Amministratore",
"Advanced": "Avanzate",
"Advanced Search Settings": "Impostazioni avanzate di ricerca",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Allineamento",
"AllRecipes": "Tutte le ricette",
"Amount": "Quantità",
@@ -88,6 +96,7 @@
"Continue": "Continua",
"Conversion": "Conversione",
"ConversionsHelp": "Con le conversioni è possibile calcolare la quantità di un alimento in diverse unità. Attualmente, questo metodo viene utilizzato solo per il calcolo delle proprietà, ma in futuro potrebbe essere utilizzato anche in altre parti del tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro di cucina",
"CookLogHelp": "Le voci nel registro di cucina per le ricette. ",
"Cooked": "Cucinati",
@@ -101,6 +110,8 @@
"Create": "Crea",
"Create Food": "Crea alimento",
"Create Recipe": "Crea ricetta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crea voce nel piano alimentare",
"Create_New_Food": "Aggiungi nuovo alimento",
"Create_New_Keyword": "Aggiungi nuova parola chiave",
@@ -110,6 +121,7 @@
"Create_New_Unit": "Aggiungi nuova unità",
"Created": "Creata",
"CreatedBy": "Creata da",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo attuale",
"Custom Filter": "Filtro personalizzato",
@@ -203,11 +215,15 @@
"Food_Replace": "Sostituisci alimento",
"Foods": "Alimenti",
"Friday": "Venerdì",
"FromBalance": "",
"Fulltext": "Fulltext",
"FulltextHelp": "Campi per la ricerca full text. Nota: i metodi di ricerca 'web', 'phrase', e 'raw' funzionano solo con i campi fulltext.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Utilizza la ricerca fuzzy per trovare voci anche quando ci sono differenze nel modo in cui la parola è scritta.",
"GettingStarted": "Iniziamo",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione",
@@ -233,7 +249,10 @@
"Import": "Importa",
"Import Recipe": "Importa ricetta",
"ImportAll": "Importa tutto",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importa in Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Si è verificato un errore durante l'importazione. Per avere maggiori informazioni, espandi la sezione dettagli in fondo alla pagina.",
"Import_Not_Yet_Supported": "Importazione non ancora supportata",
"Import_Result_Info": "{imported} di {total} ricette sono state importate",
@@ -272,9 +291,12 @@
"Last": "Ultimo",
"Last_name": "Cognome",
"Learn_More": "Scopri altro",
"LeaveSpace": "",
"Link": "Collegamento",
"Load": "Carica",
"Load_More": "Carica altro",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro ricette cucinate",
"Log_Recipe_Cooking": "Aggiungi a ricette cucinate",
"Logo": "Logo",
@@ -303,6 +325,8 @@
"ModelSelectResultsHelp": "Cerca altri risultati",
"Monday": "Lunedì",
"Month": "Mese",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Altro",
"Move": "Sposta",
"MoveCategory": "Sposta in: ",
@@ -344,6 +368,8 @@
"Note": "Nota",
"Number of Objects": "Numero di oggetti",
"Nutrition": "Nutrienti",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Sei offline, le liste della spesa potrebbero non sincronizzarsi.",
"Ok": "Ok",
"OnHand": "Attualmente disponibili",
@@ -456,6 +482,7 @@
"Servings": "Porzioni",
"ServingsText": "Testo porzioni",
"Settings": "Impostazioni",
"SettingsOnlySuperuser": "",
"Share": "Condividi",
"ShopLater": "Compra dopo",
"ShopNow": "Compra subito",
@@ -479,17 +506,21 @@
"Show_as_header": "Mostra come intestazione",
"Single": "Singolo",
"Size": "Dimensione",
"Skip": "",
"Social_Authentication": "Autenticazione social",
"Sort_by_new": "Prima i nuovi",
"Source": "Fonte",
"SourceImportHelp": "Importa JSON nel formato schema.org/recipe o pagine HTML con ricetta json+ld o microdati.",
"SourceImportSubtitle": "Importa manualmente JSON o HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Il tuo spazio ha superato uno dei suoi limiti, alcune funzioni potrebbero essere limitate.",
"SpaceLimitReached": "Questo spazio ha raggiunto il limite. Non è possibile creare altri oggetti di questo tipo.",
"SpaceMemberHelp": "Aggiungi utenti al tuo spazio creando un collegamento di invito e inviandolo alla persona che desideri aggiungere.",
"SpaceMembers": "Membri dello spazio",
"SpaceMembersHelp": "Utenti e relativi permessi in uno spazio. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Impostazioni spazio",
"Space_Cosmetic_Settings": "Alcune impostazioni cosmetiche possono essere modificate dagli amministratori dell'istanza e sovrascriveranno le impostazioni client per quell'istanza.",
"Split": "Dividi",
@@ -599,6 +630,8 @@
"Week": "Settimana",
"Week_Numbers": "Numeri della settimana",
"Welcome": "Benvenuto",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Orario lavorativo",
"Year": "Anno",
"Yes": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -16,6 +18,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "Suma",
"App": "",
@@ -56,6 +64,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -65,6 +74,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -72,6 +83,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -128,10 +140,14 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -150,6 +166,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -179,8 +198,11 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "Įkelti daugiau",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Užregistruoti patiekalo gaminimą",
"Log_Recipe_Cooking": "Užregistruoti recepto pagaminimą",
"Make_Header": "Padaryti antraštę",
@@ -199,6 +221,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "Nuleisti žemyn",
@@ -232,6 +256,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -305,6 +331,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",
@@ -316,9 +343,14 @@
"Show_as_header": "Rodyti kaip antraštę",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Rūšiuoti pagal naujumą",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"StartDate": "",
"Starting_Day": "",
@@ -381,6 +413,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -16,6 +18,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +65,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -66,6 +75,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -74,6 +85,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -143,10 +155,14 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -165,6 +181,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -195,8 +214,11 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
@@ -216,6 +238,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -252,6 +276,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -328,6 +354,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",
@@ -344,9 +371,14 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"StartDate": "",
@@ -413,6 +445,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "Legg til",
"AddChild": "",
"AddFoodToShopping": "Legg til {food] i handlelisten din",
@@ -16,6 +18,12 @@
"Added_by": "Lagt til av",
"Added_on": "Lagt til",
"Advanced": "Avansert",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mengde",
"App": "App",
@@ -55,6 +63,7 @@
"Comments_setting": "",
"Completed": "Fullført",
"Conversion": "Omregn enhet",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier lenke",
"Copy Token": "Kopier Token",
@@ -64,6 +73,8 @@
"Create": "Opprett",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Opprett måltidsplanoppføring",
"Create_New_Food": "Opprett ny matrett",
"Create_New_Keyword": "Opprett nytt nøkkelord",
@@ -71,6 +82,7 @@
"Create_New_Shopping Category": "Opprett ny handle kategori",
"Create_New_Shopping_Category": "Opprett new handle kategori",
"Create_New_Unit": "Opprett ny enhet",
"Credits": "",
"Current_Period": "Gjeldende periode",
"Custom Filter": "Egendefinert Filter",
"CustomImageHelp": "Last opp et bilde for å vise \"space\"-oversikten.",
@@ -134,10 +146,14 @@
"FoodOnHand": "Du har {food} på lager.",
"Food_Alias": "Matrett Alias",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord",
@@ -156,6 +172,9 @@
"Image": "Bilde",
"Import": "Importer",
"Import Recipe": "Importer oppskrift",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -186,8 +205,11 @@
"Language": "Språk",
"Last_name": "Etternavn",
"Learn_More": "Lær mer",
"LeaveSpace": "",
"Link": "Lenke",
"Load_More": "Last inn flere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Loggfør tilbereding",
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
"Make_Header": "Bruk som overskrift",
@@ -206,6 +228,8 @@
"Message": "Melding",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytt",
"MoveCategory": "Flytt til: ",
"Move_Down": "Flytt ned",
@@ -238,6 +262,8 @@
"Note": "Merk",
"Number of Objects": "Antall objekter",
"Nutrition": "Næringsinnhold",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du er ikke koblet til internett. Det kan hende handlelisten ikke synkroniserer.",
"Ok": "Ok",
"OnHand": "På lager",
@@ -312,6 +338,7 @@
"Selected": "Valgte",
"Servings": "Porsjoner",
"Settings": "Innstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårlig nettverkstilkobling, venter på synkronisering...",
"Shopping_Categories": "Butikk Kategorier",
@@ -326,9 +353,14 @@
"Show_as_header": "Vis som overskrift",
"Single": "",
"Size": "Størrelse",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Sorter etter nyest",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"StartDate": "Startdato",
"Starting_Day": "Dag uken skal state på",
@@ -390,6 +422,8 @@
"Week": "Uke",
"Week_Numbers": "Ukenummer",
"Welcome": "Velkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Toegangstoken",
"Account": "Account",
"Actions": "Acties",
"Active": "",
"Activity": "Activiteit",
"Add": "Voeg toe",
"AddAll": "Voeg alles toe",
@@ -28,6 +30,12 @@
"Admin": "Beheer",
"Advanced": "Geavanceerd",
"Advanced Search Settings": "Geavanceerde zoekinstellingen",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Afstemming",
"AllRecipes": "Alle recepten",
"Amount": "Hoeveelheid",
@@ -89,6 +97,7 @@
"Continue": "Doorgaan",
"Conversion": "Omrekening",
"ConversionsHelp": "Met omrekeningen kun je de hoeveelheid van een ingrediënt in verschillende eenheden berekenen. Momenteel wordt dit alleen gebruikt voor het berekenen van eigenschappen, later kan het ook in andere onderdelen van Tandoor gebruikt worden. ",
"ConvertUsingAI": "",
"CookLog": "Kooklogboek",
"CookLogHelp": "Items in het kooklogboek voor recepten. ",
"Cooked": "Gekookt",
@@ -102,6 +111,8 @@
"Create": "Aanmaken",
"Create Food": "Maak voedingsmiddel",
"Create Recipe": "Maak recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Maak maaltijdplan",
"Create_New_Food": "Voeg nieuw voedingsmiddel toe",
"Create_New_Keyword": "Voeg nieuw trefwoord toe",
@@ -111,6 +122,7 @@
"Create_New_Unit": "Voeg nieuwe eenheid toe",
"Created": "Gemaakt",
"CreatedBy": "Gemaakt door",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Huidige periode",
"Custom Filter": "Aangepast filter",
@@ -204,11 +216,15 @@
"Food_Replace": "Voedingsmiddelen vervangen",
"Foods": "Voedingsmiddelen",
"Friday": "Vrijdag",
"FromBalance": "",
"Fulltext": "Volledige tekst",
"FulltextHelp": "Velden voor volledige tekstzoekopdrachten. Opmerking: de zoekmethoden web, zin en ruw werken alleen met volledige tekstvelden.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Gebruik fuzzy search om items te vinden, zelfs als het woord anders is gespeld.",
"GettingStarted": "Aan de slag",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst",
@@ -234,7 +250,10 @@
"Import": "Importeer",
"Import Recipe": "Recept importeren",
"ImportAll": "Alles importeren",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importeer in Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Er is een fout opgetreden tijdens je import. Breid de details aan de onderzijde van de pagina uit om ze te bekijken.",
"Import_Not_Yet_Supported": "Import nog niet ondersteund",
"Import_Result_Info": "{imported} van {total} recepten zijn geïmporteerd",
@@ -273,9 +292,12 @@
"Last": "Laatste",
"Last_name": "Achternaam",
"Learn_More": "Meer informatie",
"LeaveSpace": "",
"Link": "Link",
"Load": "Laden",
"Load_More": "Laad meer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreer bereiding",
"Log_Recipe_Cooking": "Bereiding registreren",
"Logo": "Logo",
@@ -304,6 +326,8 @@
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
"Monday": "Maandag",
"Month": "Maand",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Meer",
"Move": "Verplaats",
"MoveCategory": "Verplaats naar: ",
@@ -345,6 +369,8 @@
"Note": "Notitie",
"Number of Objects": "Aantal objecten",
"Nutrition": "Voedingswaarde",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Je bent offline, boodschappenlijst synchroniseert mogelijk niet.",
"Ok": "Ok",
"OnHand": "Momenteel op voorraad",
@@ -457,6 +483,7 @@
"Servings": "Porties",
"ServingsText": "Portie tekst",
"Settings": "Instellingen",
"SettingsOnlySuperuser": "",
"Share": "Deel",
"ShopLater": "Later boodschappen doen",
"ShopNow": "Nu boodschappen doen",
@@ -480,17 +507,21 @@
"Show_as_header": "Toon als koptekst",
"Single": "Enkele",
"Size": "Grootte",
"Skip": "",
"Social_Authentication": "Authenticeren met sociale media-account",
"Sort_by_new": "Sorteer op nieuw",
"Source": "Bron",
"SourceImportHelp": "Importeer JSON in schema.org/recipe-formaat of html-paginas met json+ld-recepten of microdata.",
"SourceImportSubtitle": "Importeer handmatig JSON of HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Je ruimte heeft een limiet overschreden, sommige functies zijn mogelijk beperkt.",
"SpaceLimitReached": "Deze ruimte heeft een limiet bereikt. Er kunnen geen objecten van dit type meer worden aangemaakt.",
"SpaceMemberHelp": "Voeg gebruikers toe aan je ruimte door een uitnodigingslink aan te maken en naar de persoon te sturen die je wilt toevoegen.",
"SpaceMembers": "Gebruikers van de ruimte",
"SpaceMembersHelp": "Gebruikers en hun rechten in een ruimte. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Ruimte-instellingen",
"Space_Cosmetic_Settings": "Sommige weergave instellingen kunnen worden geforceerd door de administrator van de 'Ruimte' en zullen de persoonlijke instellingen voor die 'Ruimte' overschrijven.",
"Split": "Splitsen",
@@ -600,6 +631,8 @@
"Week": "Week",
"Week_Numbers": "Weeknummers",
"Welcome": "Welkom",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Bereidingstijd",
"Year": "Jaar",
"Yes": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -6,6 +7,7 @@
"Access_Token": "Token Dostępu",
"Account": "Konto",
"Actions": "Akcje",
"Active": "",
"Activity": "Aktywność",
"Add": "Dodaj",
"AddAll": "Dodaj wszystkie",
@@ -25,6 +27,12 @@
"Admin": "Administator",
"Advanced": "Zaawansowany",
"Advanced Search Settings": "Ustawienia zaawansowanego wyszukiwania",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Wyrównanie",
"AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość",
@@ -83,6 +91,7 @@
"Confirm": "Potwierdź",
"Continue": "Kontynuuj",
"Conversion": "Konwersja",
"ConvertUsingAI": "",
"Copy": "Kopiuj",
"Copy Link": "Skopiuj link",
"Copy Token": "Kopiuj Token",
@@ -92,6 +101,8 @@
"Create": "Stwórz",
"Create Food": "Twórz jedzenie",
"Create Recipe": "Utwórz przepis",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Utwórz wpis planu posiłków",
"Create_New_Food": "Dodaj nową żywność",
"Create_New_Keyword": "Dodaj nowe słowo kluczowe",
@@ -100,6 +111,7 @@
"Create_New_Shopping_Category": "Dodaj nową kategorię zakupów",
"Create_New_Unit": "Dodaj nowa jednostkę",
"Created": "Utworzony",
"Credits": "",
"Current_Period": "Bieżący okres",
"Custom Filter": "Filtr niestandardowy",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
@@ -169,10 +181,14 @@
"Food_Alias": "Alias żywności",
"Food_Replace": "Zastąp produkt",
"Foods": "Żywność",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe",
@@ -191,6 +207,9 @@
"Image": "Obraz",
"Import": "Importuj",
"Import Recipe": "Importuj przepis",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Podczas importowania wystąpił błąd. Rozwiń Szczegóły na dole strony, aby go wyświetlić.",
"Import_Not_Yet_Supported": "Importowanie jeszcze nie wspierane",
"Import_Result_Info": "{imported} z {total} przepisów zostało zaimportowanych",
@@ -221,8 +240,11 @@
"Language": "Język",
"Last_name": "Nazwisko",
"Learn_More": "Dowiedz się więcej",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Załaduj więcej",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zanotuj ugotowanie",
"Log_Recipe_Cooking": "Zaloguj gotowanie przepisu",
"Logo": "Logo",
@@ -242,6 +264,8 @@
"Message": "Wiadomość",
"MissingProperties": "",
"Month": "Miesiąc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Przenieś",
"MoveCategory": "Przenieś do: ",
"Move_Down": "Przesunąć w dół",
@@ -278,6 +302,8 @@
"Note": "Notatka",
"Number of Objects": "Ilość obiektów",
"Nutrition": "Odżywianie",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Jesteś offline, lista zakupów może nie być zsynchronizowana.",
"Ok": "Ok",
"OnHand": "Obecnie posiadane",
@@ -354,6 +380,7 @@
"Selected": "Wybrane",
"Servings": "Porcje",
"Settings": "Ustawienia",
"SettingsOnlySuperuser": "",
"Share": "Udostępnij",
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
"Shopping_Categories": "Kategorie zakupów",
@@ -370,9 +397,14 @@
"Show_as_header": "Pokaż jako nagłówek",
"Single": "Pojedynczy",
"Size": "Rozmiar",
"Skip": "",
"Social_Authentication": "Uwierzytelnianie społecznościowe",
"Sort_by_new": "Sortuj według nowych",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.",
"StartDate": "Data początkowa",
@@ -439,6 +471,8 @@
"Week": "Tydzień",
"Week_Numbers": "Numery tygodni",
"Welcome": "Witamy",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Rok",
"Yes": "",
"add_keyword": "Dodaj słowo kluczowe",

View File

@@ -1,6 +1,8 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "Adicionar",
"AddChild": "",
"AddFoodToShopping": "Adicionar {food} à sua lista de compras",
@@ -14,6 +16,12 @@
"Added_by": "Adicionado por",
"Added_on": "Adicionado a",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"Amount": "Quantidade",
"Apply": "",
@@ -46,6 +54,7 @@
"Coming_Soon": "",
"Completed": "Completo",
"Conversion": "Conversão",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Ligação",
"Copy Token": "Copiar Chave",
@@ -53,6 +62,8 @@
"CountMore": "...+{count} mais",
"Create": "Criar",
"Create Food": "Criar Comida",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Criar entrada para plano de refeições",
"Create_New_Food": "Adicionar nova comida",
"Create_New_Keyword": "Adicionar nova palavra-chave",
@@ -60,6 +71,7 @@
"Create_New_Shopping Category": "Criar nova categoria de Compras",
"Create_New_Shopping_Category": "Adicionar nova categoria de compras",
"Create_New_Unit": "Adicionar nova unidade",
"Credits": "",
"Current_Period": "Período atual",
"Custom Filter": "",
"CustomImageHelp": "Fazer upload de uma image para mostrar na visão geral do espaço.",
@@ -114,10 +126,14 @@
"FoodOnHand": "Tem {food} disponível.",
"Food_Alias": "Alcunha da comida",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida",
"Hide_Keyword": "",
@@ -133,6 +149,9 @@
"IgnoredFood": "{food} está definida para ignorar compras.",
"Image": "Image",
"Import": "Importar",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_finished": "Importação terminada",
"Information": "Informação",
"Ingredient Editor": "Editor de Ingredientes",
@@ -153,8 +172,11 @@
"Keywords": "Palavras-chave",
"Language": "Linguagem",
"Learn_More": "Aprenda mais",
"LeaveSpace": "",
"Link": "Ligação",
"Load_More": "Carregar Mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar Culinária",
"Log_Recipe_Cooking": "Registrar Receitas de Culinária",
"Make_Header": "Tornar cabeçalho",
@@ -171,6 +193,8 @@
"Merge_Keyword": "Unir palavra-chave",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover para: ",
"Move_Down": "Mover para baixo",
@@ -198,6 +222,8 @@
"Note": "Nota",
"Number of Objects": "Número de objetos",
"Nutrition": "Nutrição",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Está offline, lista das compras poderá não sincronizar.",
"Ok": "Ok",
"OnHand": "Atualmente disponível",
@@ -267,6 +293,7 @@
"Selected": "Selecionado",
"Servings": "Doses",
"Settings": "Definições",
"SettingsOnlySuperuser": "",
"Share": "Partilhar",
"Shopping_Categories": "Categorias de Compras",
"Shopping_Category": "Categoria de Compras",
@@ -278,8 +305,13 @@
"Show_Week_Numbers": "Mostrar números das semanas?",
"Show_as_header": "Mostrar como cabeçalho",
"Size": "Tamanho",
"Skip": "",
"Sort_by_new": "Ordenar por mais recente",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"StartDate": "Data de início",
"Starting_Day": "Dia de início da semana",
"StartsWith": "",
@@ -329,6 +361,8 @@
"Week": "Semana",
"Week_Numbers": "Números das semanas",
"Welcome": "Bem-vindo",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Ano",
"Yes": "",
"add_keyword": "Adicionar Palavra Chave",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Use IA para importar imagens das receitas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Token de acesso",
"Account": "Conta",
"Actions": "Ações",
"Active": "",
"Activity": "Atividade",
"Add": "Adicionar",
"AddAll": "Adicionar todos",
@@ -26,6 +28,12 @@
"Added_on": "Incluído Em",
"Admin": "Administrador",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"AllRecipes": "Todas Receitas",
"Amount": "Quantidade",
@@ -87,6 +95,7 @@
"Continue": "Continuar",
"Conversion": "Conversão",
"ConversionsHelp": "Com conversões, você pode calcular a quantidade de um alimento em diferentes unidades. Atualmente, isso é usado apenas para cálculo de propriedades, posteriormente poderá ser usado em outras partes do Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro de cozimento",
"CookLogHelp": "Entradas no registro de cozimento para receitas. ",
"Cooked": "Cozido",
@@ -100,6 +109,8 @@
"Create": "Criar",
"Create Food": "Criar Alimento",
"Create Recipe": "Criar Receita",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Criar Plano de Refeição",
"Create_New_Food": "Incluir Novo Alimento",
"Create_New_Keyword": "Incluir Nova Palavra-Chave",
@@ -109,6 +120,7 @@
"Create_New_Unit": "Incluir Nova Unidade",
"Created": "Criado",
"CreatedBy": "Criado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Período Atual",
"Custom Filter": "Filtro Customizado",
@@ -202,11 +214,15 @@
"Food_Replace": "Substituir Alimento",
"Foods": "Alimentos",
"Friday": "Sexta-feira",
"FromBalance": "",
"Fulltext": "Texto completo",
"FulltextHelp": "Campos para pesquisa textual completa. Observação: os métodos de pesquisa 'web', 'phrase' e 'raw' só funcionam com campos de pesquisa textual completa.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use pesquisa fuzzy para encontrar registros mesmo quando existem diferenças na grafia das palavras utilizadas.",
"GettingStarted": "Começando",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título",
@@ -232,7 +248,10 @@
"Import": "Importar",
"Import Recipe": "Importar Receita",
"ImportAll": "Importar todos",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importar para Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ocorreu um erro durante a importação. Expanda os detalhes na parte inferior da página para visualizá-los.",
"Import_Not_Yet_Supported": "Importação ainda não suportada",
"Import_Result_Info": "{imported} de {total} receitas foram importadas",
@@ -271,9 +290,12 @@
"Last": "Último",
"Last_name": "Último Nome",
"Learn_More": "Aprender Mais",
"LeaveSpace": "",
"Link": "Link",
"Load": "Carregar",
"Load_More": "Carregar mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro de Cozinha",
"Log_Recipe_Cooking": "Registrar receitas feitas",
"Logo": "Logotipo",
@@ -296,6 +318,8 @@
"Message": "Mensagem",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover Para: ",
"Move_Down": "Mover para baixo",
@@ -332,6 +356,8 @@
"Note": "Nota",
"Number of Objects": "Número de Objetos",
"Nutrition": "Nutrição",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Você está offline, a lista de compras não pode ser sincronizada.",
"Ok": "Ok",
"OnHand": "Atualmente disponível",
@@ -402,6 +428,7 @@
"Selected": "Selecionado",
"Servings": "Porções",
"Settings": "Configurações",
"SettingsOnlySuperuser": "",
"Share": "Compartilhar",
"ShoppingBackgroundSyncWarning": "Rede ruim, aguardando sincronização...",
"Shopping_Categories": "Categorias de Mercado",
@@ -418,9 +445,14 @@
"Show_as_header": "Mostrar como título",
"Single": "Simples",
"Size": "Tamanho",
"Skip": "",
"Social_Authentication": "Autenticação social",
"Sort_by_new": "Ordenar por novos",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Algumas configurações cosméticas podem ser alteradas pelos administradores do espaço e substituirão as configurações do cliente para esse espaço.",
"Split_All_Steps": "Divida todas as linhas em etapas separadas.",
"StartDate": "Data Início",
@@ -483,6 +515,8 @@
"Week": "Semana",
"Week_Numbers": "Números da Semana",
"Welcome": "Bem vindo",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Ano",
"Yes": "",
"add_keyword": "Incluir Palavra-Chave",

View File

@@ -1,8 +1,10 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
"Account": "Cont",
"Active": "",
"Add": "Adaugă",
"AddChild": "",
"AddFoodToShopping": "Adăugă {food} în lista de cumpărături",
@@ -17,6 +19,12 @@
"Added_on": "Adăugat la",
"Advanced": "Avansat",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Amount": "Cantitate",
"App": "Aplicație",
"Apply": "",
@@ -53,6 +61,7 @@
"Coming_Soon": "În curând",
"Comments_setting": "Afișează comentarii",
"Completed": "Completat",
"ConvertUsingAI": "",
"Copy": "Copie",
"Copy Link": "Copiere link",
"Copy Token": "Copiere token",
@@ -62,6 +71,8 @@
"Create": "Creează",
"Create Food": "Creare mâncare",
"Create Recipe": "Crearea rețetei",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crearea înregistrării în planul de alimentare",
"Create_New_Food": "Adaugă mâncare nouă",
"Create_New_Keyword": "Adaugă cuvânt cheie nou",
@@ -69,6 +80,7 @@
"Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
"Create_New_Shopping_Category": "Adaugă categorie de cumpărături nouă",
"Create_New_Unit": "Adaugă unitate nouă",
"Credits": "",
"Current_Period": "Perioada curentă",
"Custom Filter": "Filtru personalizat",
"DELETE_ERROR": "",
@@ -121,10 +133,14 @@
"FoodOnHand": "Aveți {food} la îndemână.",
"Food_Alias": "Pseudonim mâncare",
"Foods": "Alimente",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie",
@@ -143,6 +159,9 @@
"Image": "Imagine",
"Import": "Importă",
"Import Recipe": "Importă rețeta",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "A apărut o eroare în timpul importului. Vă rugăm să extindeți detaliile din partea de jos a paginii pentru a le vizualiza.",
"Import_Not_Yet_Supported": "Importul încă nu este compatibil",
"Import_Result_Info": "{imported} din {total} rețete au fost importate",
@@ -171,8 +190,11 @@
"Keywords": "Cuvinte cheie",
"Language": "Limba",
"Last_name": "Nume de familie",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Încărcați mai mult",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Jurnal de pregătire",
"Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
"Make_Header": "Creare antet",
@@ -191,6 +213,8 @@
"Message": "Mesaj",
"MissingProperties": "",
"Month": "Lună",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mută",
"MoveCategory": "Mută la: ",
"Move_Down": "Deplasați-vă în jos",
@@ -221,6 +245,8 @@
"NotInShopping": "{food} nu se află în lista de cumpărături.",
"Note": "Notă",
"Nutrition": "Nutriție",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Sunteți offline, este posibil ca lista de cumpărături să nu se sincronizeze.",
"Ok": "Ok",
"OnHand": "În prezent, la îndemână",
@@ -289,6 +315,7 @@
"Selected": "Selectat",
"Servings": "Porții",
"Settings": "Setări",
"SettingsOnlySuperuser": "",
"Share": "Împărtășire",
"Shopping_Categories": "Categorii de cumpărături",
"Shopping_Category": "Categorie de cumpărături",
@@ -300,9 +327,14 @@
"Show_as_header": "Afișare ca antet",
"Single": "Singur",
"Size": "Marime",
"Skip": "",
"Social_Authentication": "Autentificare socială",
"Sort_by_new": "Sortare după nou",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
"Starting_Day": "Ziua de început a săptămânii",
"StartsWith": "",
@@ -359,6 +391,8 @@
"Website": "Site web",
"Week": "Săptămână",
"Week_Numbers": "Numerele săptămânii",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "An",
"Yes": "",
"add_keyword": "Adăugare cuvânt cheie",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Используй AI для импорта изображений рецептов.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Токен доступа",
"Account": "Аккаунт",
"Actions": "Действия",
"Active": "",
"Activity": "Активность",
"Add": "Добавить",
"AddAll": "Добавить все",
@@ -27,6 +29,12 @@
"Admin": "Админ",
"Advanced": "Расширенный",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Выравнивание",
"AllRecipes": "Все рецепты",
"Amount": "Количество",
@@ -88,6 +96,7 @@
"Continue": "Продолжить",
"Conversion": "Преобразование",
"ConversionsHelp": "С помощью преобразований вы можете рассчитывать количество продукта в разных единицах измерения. В настоящее время это используется только для расчёта свойств, но в будущем может применяться и в других частях Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Журнал приготовления",
"CookLogHelp": "История приготовлений по рецептам. ",
"Cooked": "Приготовлено",
@@ -101,6 +110,8 @@
"Create": "Создать",
"Create Food": "Создать продукт",
"Create Recipe": "Создать рецепт",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Создать плана питания",
"Create_New_Food": "Добавить новую еду",
"Create_New_Keyword": "Добавить ключевое слово",
@@ -110,6 +121,7 @@
"Create_New_Unit": "Добавить единицу измерения",
"Created": "Создано",
"CreatedBy": "Создано пользователем",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Текущий период",
"Custom Filter": "Пользовательский фильтр",
@@ -203,11 +215,15 @@
"Food_Replace": "Замена продукта",
"Foods": "Продукты",
"Friday": "Пятница",
"FromBalance": "",
"Fulltext": "Полнотекстовый",
"FulltextHelp": "Поля, используемые в полнотекстовом поиске. Важно: методы поиска web, phrase и raw применимы только к полнотекстовым полям.",
"Fuzzy": "Нечёткий",
"FuzzySearchHelp": "Нечёткий поиск позволяет находить записи, даже если в написании есть ошибки или отличия.",
"GettingStarted": "Начало работы",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок",
@@ -233,7 +249,10 @@
"Import": "Импорт",
"Import Recipe": "Импортировать рецепт",
"ImportAll": "Импортировать всё",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Импорт в Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Во время импорта произошла ошибка. Для просмотра разверните \"Подробности\" в нижней части страницы.",
"Import_Not_Yet_Supported": "Импорт пока не поддерживается",
"Import_Result_Info": "{imported} из {total} рецептов были импортированы",
@@ -272,9 +291,12 @@
"Last": "Последний",
"Last_name": "Фамилия",
"Learn_More": "Узнать больше",
"LeaveSpace": "",
"Link": "Гиперссылка",
"Load": "Загрузить",
"Load_More": "Загрузить еще",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Журнал приготовления",
"Log_Recipe_Cooking": "Журнал приготовления",
"Logo": "Логотип",
@@ -302,6 +324,8 @@
"ModelSelectResultsHelp": "Показать больше результатов",
"Monday": "Понедельник",
"Month": "Месяц",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Ещё",
"Move": "Переместить",
"MoveCategory": "Переместить в: ",
@@ -342,6 +366,8 @@
"Note": "Заметка",
"Number of Objects": "Количество (шт.)",
"Nutrition": "Питательность",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Вы находитесь вне сети, список покупок может не синхронизироваться.",
"Ok": "Открыть",
"OnHand": "В Наличии",
@@ -454,6 +480,7 @@
"Servings": "Порции",
"ServingsText": "Описание порций",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Поделиться",
"ShopLater": "Купить позже",
"ShopNow": "Купить сейчас",
@@ -477,17 +504,21 @@
"Show_as_header": "Показывать как заголовок",
"Single": "Одиночный",
"Size": "Размер",
"Skip": "",
"Social_Authentication": "Социальная аутентификация",
"Sort_by_new": "Сортировка по новизне",
"Source": "Источник",
"SourceImportHelp": "Импортируйте JSON в формате schema.org/recipe или HTML-страницы с рецептами в формате JSON-LD или микроданных.",
"SourceImportSubtitle": "Импортировать JSON или HTML вручную.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Ваше пространство превысило один из лимитов, некоторые функции могут быть ограничены.",
"SpaceLimitReached": "В этом пространстве достигнут лимит. Новые объекты данного типа создавать нельзя.",
"SpaceMemberHelp": "Для добавления пользователей создайте пригласительную ссылку и передайте её человеку, которого хотите пригласить.",
"SpaceMembers": "Участники пространства",
"SpaceMembersHelp": "Пользователи и их права доступа в пространстве. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Настройки пространства",
"Space_Cosmetic_Settings": "Администраторы пространства могут менять некоторые визуальные настройки, которые будут переопределять настройки клиента для данного пространства.",
"Split": "Разделить",
@@ -597,6 +628,8 @@
"Week": "Неделя",
"Week_Numbers": "Номер недели",
"Welcome": "Добро пожаловать",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Время работы",
"Year": "Год",
"Yes": "",

View File

@@ -1,6 +1,7 @@
{
"AI": "Umetna inteligenca",
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -8,6 +9,7 @@
"Access_Token": "Dostopni žeton",
"Account": "Račun",
"Actions": "Dejanja",
"Active": "",
"Activity": "Aktivnost",
"Add": "Dodaj",
"AddAll": "Dodaj vse",
@@ -27,6 +29,12 @@
"Admin": "Skrbnik",
"Advanced": "Napredno",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnava",
"AllRecipes": "Vsi recepti",
"Amount": "Količina",
@@ -88,6 +96,7 @@
"Continue": "Nadaljuj",
"Conversion": "Pogovor",
"ConversionsHelp": "S pretvorbami lahko izračunate količino živila v različnih enotah. Trenutno se to uporablja le za izračun lastnosti, kasneje pa se lahko uporabi tudi v drugih delih Tandoorja. ",
"ConvertUsingAI": "",
"CookLog": "Kuharski dnevnik",
"CookLogHelp": "Vnosi v dnevnik kuhanja za recepte. ",
"Cooked": "Kuhano",
@@ -101,6 +110,8 @@
"Create": "Ustvari",
"Create Food": "Ustvari živilo",
"Create Recipe": "Ustvari recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
"Create_New_Food": "Dodaj Novo Hrano",
"Create_New_Keyword": "Dodaj novo ključno besedo",
@@ -110,6 +121,7 @@
"Create_New_Unit": "Dodaj novo enoto",
"Created": "Ustvarjeno",
"CreatedBy": "Ustvaril/a",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Trenutno obdobje",
"Custom Filter": "Filter po meri",
@@ -203,11 +215,15 @@
"Food_Replace": "Zamenjava živila",
"Foods": "Živila",
"Friday": "Petek",
"FromBalance": "",
"Fulltext": "Celotno besedilo",
"FulltextHelp": "Polja za iskanje po celotnem besedilu. Opomba: metode iskanja »splet«, »fraza« in »surovo« delujejo samo s polji po celotnem besedilu.",
"Fuzzy": "Nejasno",
"FuzzySearchHelp": "Uporabite mehko iskanje za iskanje vnosov, tudi če obstajajo razlike v načinu pisanja besede.",
"GettingStarted": "Začetek",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov",
@@ -233,7 +249,10 @@
"Import": "Uvozi",
"Import Recipe": "Uvozi recept",
"ImportAll": "Uvozi vse",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Uvozi v Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.",
"Import_Not_Yet_Supported": "Uvoz še ni podprt",
"Import_Result_Info": "Uvoženih je bilo {imported} od {total} receptov",
@@ -272,9 +291,12 @@
"Last": "Zadnji",
"Last_name": "Priimek",
"Learn_More": "Preberite Več",
"LeaveSpace": "",
"Link": "Hiperpovezava",
"Load": "Naloži",
"Load_More": "Naloži več",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zgodovina kuhanja",
"Log_Recipe_Cooking": "Beleži kuharski recept",
"Logo": "Logotip",
@@ -303,6 +325,8 @@
"ModelSelectResultsHelp": "Išči več rezultatov",
"Monday": "Ponedeljek",
"Month": "Mesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Več",
"Move": "Premakni",
"MoveCategory": "Premakni v: ",
@@ -344,6 +368,8 @@
"Note": "Opomba",
"Number of Objects": "Število predmetov",
"Nutrition": "Prehrana",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Si v načinu brez povezave, nakupovalni listek se mogoče ne bo sinhroniziral.",
"Ok": "V redu",
"OnHand": "Trenutno imam v roki",
@@ -456,6 +482,7 @@
"Servings": "Porcije",
"ServingsText": "Besedilo o porcijah",
"Settings": "Nastavitve",
"SettingsOnlySuperuser": "",
"Share": "Deli",
"ShopLater": "Nakupujte pozneje",
"ShopNow": "Nakupujte zdaj",
@@ -479,17 +506,21 @@
"Show_as_header": "Prikaži kot glavo",
"Single": "Ena",
"Size": "Velikost",
"Skip": "",
"Social_Authentication": "Socialna avtentikacija",
"Sort_by_new": "Razvrsti po novih",
"Source": "Vir",
"SourceImportHelp": "Uvozite JSON v formatu schema.org/recipe ali na straneh html z receptom json+ld ali mikropodatki.",
"SourceImportSubtitle": "Ročno uvozite JSON ali HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Vaš prostor je presegel eno od svojih omejitev, nekatere funkcije so morda omejene.",
"SpaceLimitReached": "Ta prostor je dosegel omejitev. Te vrste predmetov ni mogoče ustvariti več.",
"SpaceMemberHelp": "Dodajte uporabnike v svoj prostor tako, da ustvarite povezavo za povabilo in jo pošljete osebi, ki jo želite dodati.",
"SpaceMembers": "Člani prostora",
"SpaceMembersHelp": "Uporabniki in njihova dovoljenja v prostoru. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Nastavitve prostora",
"Space_Cosmetic_Settings": "Nekatere kozmetične nastavitve lahko spremenijo skrbniki prostora in bodo preglasile nastavitve odjemalca za ta prostor.",
"Split": "Razdelitev",
@@ -599,6 +630,8 @@
"Week": "Teden",
"Week_Numbers": "Števila tednov",
"Welcome": "Dobrodošli",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Delovni čas",
"Year": "Leto",
"Yes": "",

View File

@@ -1,5 +1,6 @@
{
"AIImportSubtitle": "Använd AI för att importera bilder av recept.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -7,6 +8,7 @@
"Access_Token": "Åtkomstnyckel",
"Account": "Konto",
"Actions": "Åtgärder",
"Active": "",
"Activity": "Aktivitet",
"Add": "Lägg till",
"AddAll": "Lägg till alla",
@@ -26,6 +28,12 @@
"Added_on": "Tillagd på",
"Admin": "Administratör",
"Advanced": "Avancerat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Orientering",
"AllRecipes": "Alla recept",
"Amount": "Mängd",
@@ -87,6 +95,7 @@
"Continue": "Fortsätt",
"Conversion": "Omvandling",
"ConversionsHelp": "Med omvandlingar kan du beräkna mängden av ett livsmedel i olika enheter. För närvarande används detta endast för egenskapsberäkning, senare kan det även användas i andra delar av Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Tillagningslogg",
"CookLogHelp": "Poster i tillagningsloggen för recept. ",
"Cooked": "Tillagad",
@@ -100,6 +109,8 @@
"Create": "Skapa",
"Create Food": "Skapa livsmedel",
"Create Recipe": "Skapa recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Skapa en måltidsplan",
"Create_New_Food": "Lägg till nytt livsmedel",
"Create_New_Keyword": "Lägg till nytt nyckelord",
@@ -109,6 +120,7 @@
"Create_New_Unit": "Lägg till enhet",
"Created": "Skapad",
"CreatedBy": "Skapad av",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Nuvarande period",
"Custom Filter": "Anpassat filter",
@@ -180,10 +192,14 @@
"Food_Alias": "Alias för livsmedel",
"Food_Replace": "Ersätt ingrediens",
"Foods": "Livsmedel",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord",
@@ -202,6 +218,9 @@
"Image": "Bild",
"Import": "Importera",
"Import Recipe": "Importera recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ett fel uppstod under din import. Expandera informationen längst ner på sidan för att se den.",
"Import_Not_Yet_Supported": "Import stöds inte ännu",
"Import_Result_Info": "{imported} av totalt {total} recept blev importerat",
@@ -232,8 +251,11 @@
"Language": "Språk",
"Last_name": "Efternamn",
"Learn_More": "Läs mer",
"LeaveSpace": "",
"Link": "Länk",
"Load_More": "Ladda mer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Logga tillagning",
"Log_Recipe_Cooking": "Logga tillagningen av receptet",
"Logo": "Logga",
@@ -253,6 +275,8 @@
"Message": "Meddelande",
"MissingProperties": "",
"Month": "Månad",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytta",
"MoveCategory": "Flytta till: ",
"Move_Down": "Flytta ned",
@@ -289,6 +313,8 @@
"Note": "Anteckning",
"Number of Objects": "Antal objekt",
"Nutrition": "Näringsinnehåll",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.",
"Ok": "Öppna",
"OnHand": "För närvarande till hands",
@@ -365,6 +391,7 @@
"Selected": "Vald",
"Servings": "Portioner",
"Settings": "Inställningar",
"SettingsOnlySuperuser": "",
"Share": "Dela",
"ShoppingBackgroundSyncWarning": "Dålig uppkoppling, inväntar synkronisering...",
"Shopping_Categories": "Shopping kategorier",
@@ -381,9 +408,14 @@
"Show_as_header": "Visa som rubrik",
"Single": "Enstaka",
"Size": "Storlek",
"Skip": "",
"Social_Authentication": "Social autentisering",
"Sort_by_new": "Sortera efter ny",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Vissa kosmetiska inställningar kan ändras av hushålls-administratörer och skriver över klientinställningar för det hushållet.",
"Split_All_Steps": "Dela upp alla rader i separata steg.",
"StartDate": "Startdatum",
@@ -450,6 +482,8 @@
"Week": "Vecka",
"Week_Numbers": "Veckonummer",
"Welcome": "Välkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "Lägg till nyckelord",

Some files were not shown because too many files have changed in this diff Show More