mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed8f97e9e0 | ||
|
|
034f68fc28 | ||
|
|
0158087a0b | ||
|
|
cb6bfd741d | ||
|
|
afeee5f7cb | ||
|
|
b43d6e08d4 | ||
|
|
1188624376 | ||
|
|
9ac837c969 | ||
|
|
fc4b017d30 | ||
|
|
4636ac28f9 | ||
|
|
397912e87f | ||
|
|
d0b860e623 | ||
|
|
8a90ed1274 | ||
|
|
163c2a53b6 | ||
|
|
286d707347 | ||
|
|
98d308aee9 | ||
|
|
a7c5240227 | ||
|
|
75fcff8e70 | ||
|
|
2f27cf4deb | ||
|
|
686b595f45 | ||
|
|
0f9f9e8f7c | ||
|
|
aba45657c3 | ||
|
|
e6abdf8cd4 | ||
|
|
6cedde7b2d | ||
|
|
741e9eb370 | ||
|
|
7db523d8c4 | ||
|
|
41f0060c43 | ||
|
|
5572833f64 | ||
|
|
780e441a3b | ||
|
|
c4fd2d0b4e | ||
|
|
1c6618f452 | ||
|
|
8c96a75a1e | ||
|
|
44baa8322c | ||
|
|
0fbb95438a | ||
|
|
c56dd9563c | ||
|
|
0008b7c975 | ||
|
|
524f086cc5 | ||
|
|
8550387e0c | ||
|
|
1618f8df79 | ||
|
|
22dfb2a410 | ||
|
|
f099e2e5d3 | ||
|
|
6973c65142 | ||
|
|
a01f86a14e | ||
|
|
9704268fdc | ||
|
|
84cc4c1165 | ||
|
|
5cb70becb8 | ||
|
|
5f99abf459 | ||
|
|
4a8ddce391 | ||
|
|
9a14a87c27 | ||
|
|
c01634f9bd | ||
|
|
f055df3b4d | ||
|
|
a83f474d70 | ||
|
|
63d358df36 | ||
|
|
e70548fcc0 | ||
|
|
17b03905e6 | ||
|
|
90403e6a13 | ||
|
|
db400cae25 | ||
|
|
0f8eee4e0f | ||
|
|
1f532f6276 | ||
|
|
b32715e493 | ||
|
|
0d19e12118 | ||
|
|
96e5213fa6 | ||
|
|
44c567d20b | ||
|
|
3c920593cf | ||
|
|
1d90f8b6f1 | ||
|
|
6b1217ec35 | ||
|
|
a71564a424 | ||
|
|
76c2e144fc | ||
|
|
981353380c | ||
|
|
96a520b1af | ||
|
|
05f537dc6b | ||
|
|
948d8da3b1 | ||
|
|
f8e4b39d88 | ||
|
|
6c498f7dac | ||
|
|
d25702b717 | ||
|
|
aca18fcbe0 | ||
|
|
98b57d2854 | ||
|
|
5e1c804fd1 | ||
|
|
a30deb4bae | ||
|
|
45a567856a | ||
|
|
7065d96f90 | ||
|
|
f8cd42dec9 | ||
|
|
8d736c0f88 |
@@ -1,12 +1,7 @@
|
||||
FROM python:3.10-alpine3.18
|
||||
FROM python:3.13-alpine3.22
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn
|
||||
|
||||
# Fix libxml error from xmlsec https://github.com/xmlsec/python-xmlsec/issues/257#issuecomment-1738620862
|
||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/community/" | tee -a /etc/apk/repositories
|
||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/main" | tee -a /etc/apk/repositories
|
||||
RUN apk add --no-cache libxml2-dev=2.9.14-r2 xmlsec-dev=1.2.33-r0
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn libgcc libstdc++ nginx tini envsubst nodejs npm
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
@@ -24,8 +19,10 @@ RUN \
|
||||
if [ `apk --print-arch` = "armv7" ]; then \
|
||||
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
|
||||
fi
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
|
||||
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
|
||||
pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt && \
|
||||
rm -rf /tmp/pip-tmp && \
|
||||
RUN apk add --no-cache --virtual .build-deps 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 rust && \
|
||||
python -m pip install --upgrade pip && \
|
||||
pip debug -v && \
|
||||
pip install wheel==0.45.1 && \
|
||||
pip install setuptools_rust==1.10.2 && \
|
||||
pip install -r /tmp/pip-tmp/requirements.txt --no-cache-dir &&\
|
||||
apk --purge del .build-deps
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -91,3 +91,4 @@ cookbook/static/vue3
|
||||
vue3/node_modules
|
||||
cookbook/tests/other/docs/reports/tests/tests.html
|
||||
cookbook/tests/other/docs/reports/tests/pytest.xml
|
||||
vue3/src/plugins
|
||||
|
||||
62
.vscode/tasks.json
vendored
62
.vscode/tasks.json
vendored
@@ -14,28 +14,16 @@
|
||||
},
|
||||
{
|
||||
"label": "Setup Dev Server",
|
||||
"dependsOn": ["Run Migrations", "Yarn Build"]
|
||||
"dependsOn": ["Run Migrations"]
|
||||
},
|
||||
{
|
||||
"label": "Run Dev Server",
|
||||
"type": "shell",
|
||||
"type": "shell",
|
||||
"dependsOn": ["Setup Dev Server"],
|
||||
"command": "python3 manage.py runserver"
|
||||
"command": "DEBUG=1 python3 manage.py runserver"
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install",
|
||||
"dependsOn": ["Yarn Install - Vue", "Yarn Install - Vue3"]
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install - Vue",
|
||||
"type": "shell",
|
||||
"command": "yarn install --force",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install - Vue3",
|
||||
"type": "shell",
|
||||
"command": "yarn install --force",
|
||||
"options": {
|
||||
@@ -44,18 +32,6 @@
|
||||
},
|
||||
{
|
||||
"label": "Generate API",
|
||||
"dependsOn": ["Generate API - Vue", "Generate API - Vue3"]
|
||||
},
|
||||
{
|
||||
"label": "Generate API - Vue",
|
||||
"type": "shell",
|
||||
"command": "openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8000/openapi/",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue/src/utils/openapi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Generate API - Vue3",
|
||||
"type": "shell",
|
||||
"command": "openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/",
|
||||
"options": {
|
||||
@@ -63,43 +39,19 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Serve",
|
||||
"label": "Yarn Dev",
|
||||
"type": "shell",
|
||||
"command": "yarn serve",
|
||||
"dependsOn": ["Yarn Install - Vue"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Vite Serve",
|
||||
"type": "shell",
|
||||
"command": "vite",
|
||||
"dependsOn": ["Yarn Install - Vue3"],
|
||||
"command": "yarn dev",
|
||||
"dependsOn": ["Yarn Install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Build",
|
||||
"dependsOn": ["Yarn Build - Vue", "Vite Build - Vue3"],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Yarn Build - Vue",
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"dependsOn": ["Yarn Install - Vue"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Vite Build - Vue3",
|
||||
"type": "shell",
|
||||
"command": "vite build",
|
||||
"dependsOn": ["Yarn Install - Vue3"],
|
||||
"dependsOn": ["Yarn Install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM python:3.13-alpine3.22
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git libgcc libstdc++ nginx tini envsubst
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git libgcc libstdc++ nginx tini envsubst nodejs npm
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
|
||||
@@ -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>
|
||||
|
||||
8
boot.sh
8
boot.sh
@@ -10,6 +10,8 @@ GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
|
||||
GUNICORN_THREADS="${GUNICORN_THREADS:-2}"
|
||||
GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
|
||||
|
||||
PLUGINS_BUILD="${PLUGINS_BUILD:-0}"
|
||||
|
||||
if [ "${TANDOOR_PORT}" -eq 80 ]; then
|
||||
echo "TANDOOR_PORT set to 8080 because 80 is now taken by the integrated nginx"
|
||||
TANDOOR_PORT=8080
|
||||
@@ -82,9 +84,13 @@ echo "Database is ready"
|
||||
|
||||
echo "Migrating database"
|
||||
|
||||
|
||||
python manage.py migrate
|
||||
|
||||
if [ "${PLUGINS_BUILD}" -eq 1 ]; then
|
||||
echo "Running yarn build at startup because PLUGINS_BUILD is enabled"
|
||||
python plugin.py
|
||||
fi
|
||||
|
||||
echo "Collecting static files, this may take a while..."
|
||||
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
@@ -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',)
|
||||
|
||||
80
cookbook/helper/ai_helper.py
Normal file
80
cookbook/helper/ai_helper.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum
|
||||
from litellm import CustomLogger
|
||||
|
||||
from cookbook.models import AiLog
|
||||
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
22
cookbook/helper/batch_edit_helper.py
Normal file
22
cookbook/helper/batch_edit_helper.py
Normal 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)
|
||||
@@ -37,7 +37,7 @@ def get_filetype(name):
|
||||
|
||||
def is_file_type_allowed(filename, image_only=False):
|
||||
is_file_allowed = False
|
||||
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css']
|
||||
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css', '.mp4', '.mov']
|
||||
allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
|
||||
check_list = allowed_image_types
|
||||
if not image_only:
|
||||
@@ -77,6 +77,8 @@ def handle_image(request, image_object, filetype):
|
||||
file_format = 'JPEG'
|
||||
if filetype == '.png':
|
||||
file_format = 'PNG'
|
||||
if filetype == '.webp':
|
||||
file_format = 'WEBP'
|
||||
|
||||
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
|
||||
if filetype == '.jpeg' or filetype == '.jpg':
|
||||
|
||||
@@ -330,6 +330,24 @@ class CustomRecipePermission(permissions.BasePermission):
|
||||
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS)
|
||||
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):
|
||||
"""
|
||||
|
||||
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-31 09:11
|
||||
|
||||
from django.db import migrations
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def migrate_comments(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
Comment = apps.get_model('cookbook', 'Comment')
|
||||
CookLog = apps.get_model('cookbook', 'CookLog')
|
||||
|
||||
cook_logs = []
|
||||
|
||||
for c in Comment.objects.all():
|
||||
cook_logs.append(CookLog(
|
||||
recipe=c.recipe,
|
||||
created_by=c.created_by,
|
||||
created_at=c.created_at,
|
||||
comment=c.text,
|
||||
space=c.recipe.space,
|
||||
))
|
||||
|
||||
CookLog.objects.bulk_create(cook_logs, unique_fields=('recipe', 'comment', 'created_at', 'created_by'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0222_alter_shoppinglistrecipe_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_comments),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0225_space_ai_enabled.py
Normal file
18
cookbook/migrations/0225_space_ai_enabled.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -329,6 +329,11 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=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 +346,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 +401,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'),)
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.property_helper import FoodPropertyHelper
|
||||
@@ -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,11 +326,52 @@ 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')
|
||||
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used')
|
||||
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
|
||||
food_inherit = FoodInheritFieldSerializer(many=True)
|
||||
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||
nav_logo = 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:
|
||||
@@ -360,16 +406,29 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
def create(self, validated_data):
|
||||
raise ValidationError('Cannot create using this endpoint')
|
||||
|
||||
def update(self, instance, 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 super().update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Space
|
||||
fields = (
|
||||
'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')
|
||||
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):
|
||||
@@ -592,7 +651,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
fields = (
|
||||
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'updated_at', 'full_name')
|
||||
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
|
||||
read_only_fields = ('id', 'label', 'numchild', 'numrecipe', 'parent', 'image')
|
||||
|
||||
|
||||
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin):
|
||||
@@ -787,7 +846,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
if plural_name := validated_data.pop('plural_name', None):
|
||||
plural_name = plural_name.strip()
|
||||
|
||||
if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first():
|
||||
if food := Food.objects.filter(Q(name__iexact=name) | Q(plural_name__iexact=name)).first():
|
||||
return food
|
||||
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
@@ -1038,7 +1097,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', '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)
|
||||
@@ -1112,6 +1171,57 @@ class RecipeImportSerializer(SpacedModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RecipeBatchUpdateSerializer(serializers.Serializer):
|
||||
recipes = serializers.ListField(child=serializers.IntegerField())
|
||||
keywords_add = serializers.ListField(child=serializers.IntegerField())
|
||||
keywords_remove = serializers.ListField(child=serializers.IntegerField())
|
||||
keywords_set = serializers.ListField(child=serializers.IntegerField())
|
||||
keywords_remove_all = serializers.BooleanField(default=False)
|
||||
|
||||
working_time = serializers.IntegerField(required=False, allow_null=True)
|
||||
waiting_time = serializers.IntegerField(required=False, allow_null=True)
|
||||
servings = serializers.IntegerField(required=False, allow_null=True)
|
||||
servings_text = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
private = serializers.BooleanField(required=False, allow_null=True)
|
||||
shared_add = serializers.ListField(child=serializers.IntegerField())
|
||||
shared_remove = serializers.ListField(child=serializers.IntegerField())
|
||||
shared_set = serializers.ListField(child=serializers.IntegerField())
|
||||
shared_remove_all = serializers.BooleanField(default=False)
|
||||
|
||||
show_ingredient_overview = serializers.BooleanField(required=False, allow_null=True)
|
||||
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)
|
||||
|
||||
@@ -1223,8 +1333,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class AutoMealPlanSerializer(serializers.Serializer):
|
||||
start_date = serializers.DateField()
|
||||
end_date = serializers.DateField()
|
||||
start_date = serializers.DateTimeField()
|
||||
end_date = serializers.DateTimeField()
|
||||
meal_type_id = serializers.IntegerField()
|
||||
keyword_ids = serializers.ListField()
|
||||
servings = CustomDecimalField()
|
||||
@@ -1480,7 +1590,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
||||
fields = (
|
||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by',
|
||||
'created_at',)
|
||||
read_only_fields = ('id', 'uuid', 'used_by' ,'created_by', 'created_at',)
|
||||
read_only_fields = ('id', 'uuid', 'used_by', 'created_by', 'created_at',)
|
||||
|
||||
|
||||
# CORS, REST and Scopes aren't currently working
|
||||
@@ -1542,7 +1652,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()
|
||||
@@ -1766,10 +1875,12 @@ 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)
|
||||
|
||||
|
||||
class ExportRequestSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
all = serializers.BooleanField(default=False)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,6 +53,17 @@
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-5">{% trans 'Plugins' %}</h3>
|
||||
Clone the plugin using git into the <code>recipe/plugin/</code> folder (Docker mount <code>/opt/recipe/recipes/plugins</code> to the host system and clone into it).
|
||||
<table class="table table-bordered">
|
||||
{% for p in plugins %}
|
||||
<tr>
|
||||
<td><a href="{{ p.github }}">{{ p.name }}</a> <br/>{{ p.base_path }}</td>
|
||||
<td><a href="{% url 'view_plugin_update' %}?module={{ p.module }}" class="btn btn-primary">Git Pull</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h4 class="mt-3">{% trans 'Media Serving' %} <span class="badge text-bg-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
|
||||
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if gunicorn_media %}
|
||||
|
||||
162
cookbook/tests/api/test_api_ai_provider.py
Normal file
162
cookbook/tests/api/test_api_ai_provider.py
Normal file
@@ -0,0 +1,162 @@
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -80,8 +82,9 @@ urlpatterns = [
|
||||
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('system/', views.system, name='view_system'),
|
||||
|
||||
path('system/', views.system, name='view_system'),
|
||||
path('plugin/update/', views.plugin_update, name='view_plugin_update'),
|
||||
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
|
||||
@@ -125,10 +128,6 @@ urlpatterns = [
|
||||
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
urlpatterns.append(path('test/', views.test, name='view_test'))
|
||||
urlpatterns.append(path('test2/', views.test2, name='view_test2'))
|
||||
|
||||
# catchall view for new frontend
|
||||
urlpatterns += [
|
||||
path('', views.index, name='index'),
|
||||
|
||||
@@ -65,6 +65,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 +76,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
|
||||
)
|
||||
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 +87,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 +112,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
|
||||
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)
|
||||
@@ -411,6 +414,7 @@ class MergeMixin(ViewSetMixin):
|
||||
description='Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.',
|
||||
type=int),
|
||||
OpenApiParameter(name='tree', description='Return all self and children of {obj} with ID [int].', type=int),
|
||||
OpenApiParameter(name='root_tree', description='Return all items belonging to the tree of the given {obj} id', type=int),
|
||||
]),
|
||||
move=extend_schema(parameters=[
|
||||
OpenApiParameter(name="parent", description='The ID of the desired parent of the {obj}.', type=OpenApiTypes.INT,
|
||||
@@ -423,6 +427,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
def get_queryset(self):
|
||||
root = self.request.query_params.get('root', None)
|
||||
tree = self.request.query_params.get('tree', None)
|
||||
root_tree = self.request.query_params.get('root_tree', None)
|
||||
|
||||
if root:
|
||||
if root.isnumeric():
|
||||
@@ -441,10 +446,21 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
|
||||
except self.model.DoesNotExist:
|
||||
self.queryset = self.model.objects.none()
|
||||
elif root_tree:
|
||||
if root_tree.isnumeric():
|
||||
try:
|
||||
self.queryset = self.model.objects.get(id=int(root_tree)).get_root().get_descendants_and_self()
|
||||
except self.model.DoesNotExist:
|
||||
self.queryset = self.model.objects.none()
|
||||
|
||||
else:
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
|
||||
serializer=self.serializer_class, tree=True)
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
# only order if not root_tree or tree mde because in these modes the sorting is relevant for the client
|
||||
if not root_tree and not tree:
|
||||
self.queryset = self.queryset.order_by(Lower('name').asc())
|
||||
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
||||
tree=True)
|
||||
@@ -604,6 +620,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
|
||||
@@ -902,6 +941,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,
|
||||
@@ -1359,9 +1486,103 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
|
||||
return Response(self.serializer_class(qs, many=True).data)
|
||||
|
||||
@decorators.action(detail=False, methods=['PUT'], serializer_class=RecipeBatchUpdateSerializer)
|
||||
def batch_update(self, request):
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
recipes = Recipe.objects.filter(id__in=serializer.validated_data['recipes'], space=self.request.space)
|
||||
safe_recipe_ids = Recipe.objects.filter(id__in=serializer.validated_data['recipes'], space=self.request.space).values_list('id', flat=True)
|
||||
|
||||
if 'keywords_add' in serializer.validated_data:
|
||||
keyword_relations = []
|
||||
for r in recipes:
|
||||
for k in serializer.validated_data['keywords_add']:
|
||||
keyword_relations.append(Recipe.keywords.through(recipe_id=r.pk, keyword_id=k))
|
||||
Recipe.keywords.through.objects.bulk_create(keyword_relations, ignore_conflicts=True, unique_fields=('recipe_id', 'keyword_id',))
|
||||
|
||||
if 'keywords_remove' in serializer.validated_data:
|
||||
for k in serializer.validated_data['keywords_remove']:
|
||||
Recipe.keywords.through.objects.filter(recipe_id__in=safe_recipe_ids, keyword_id=k).delete()
|
||||
|
||||
if 'keywords_set' in serializer.validated_data and len(serializer.validated_data['keywords_set']) > 0:
|
||||
keyword_relations = []
|
||||
Recipe.keywords.through.objects.filter(recipe_id__in=safe_recipe_ids).delete()
|
||||
for r in recipes:
|
||||
for k in serializer.validated_data['keywords_set']:
|
||||
keyword_relations.append(Recipe.keywords.through(recipe_id=r.pk, keyword_id=k))
|
||||
Recipe.keywords.through.objects.bulk_create(keyword_relations, ignore_conflicts=True, unique_fields=('recipe_id', 'keyword_id',))
|
||||
|
||||
if 'keywords_remove_all' in serializer.validated_data and serializer.validated_data['keywords_remove_all']:
|
||||
Recipe.keywords.through.objects.filter(recipe_id__in=safe_recipe_ids).delete()
|
||||
|
||||
if 'working_time' in serializer.validated_data:
|
||||
recipes.update(working_time=serializer.validated_data['working_time'])
|
||||
|
||||
if 'waiting_time' in serializer.validated_data:
|
||||
recipes.update(waiting_time=serializer.validated_data['waiting_time'])
|
||||
|
||||
if 'servings' in serializer.validated_data:
|
||||
recipes.update(servings=serializer.validated_data['servings'])
|
||||
|
||||
if 'servings_text' in serializer.validated_data:
|
||||
recipes.update(servings_text=serializer.validated_data['servings_text'])
|
||||
|
||||
if 'private' in serializer.validated_data and serializer.validated_data['private'] is not None:
|
||||
recipes.update(private=serializer.validated_data['private'])
|
||||
|
||||
if 'shared_add' in serializer.validated_data:
|
||||
shared_relation = []
|
||||
for r in recipes:
|
||||
for u in serializer.validated_data['shared_add']:
|
||||
shared_relation.append(Recipe.shared.through(recipe_id=r.pk, user_id=u))
|
||||
Recipe.shared.through.objects.bulk_create(shared_relation, ignore_conflicts=True, unique_fields=('recipe_id', 'user_id',))
|
||||
|
||||
if 'shared_remove' in serializer.validated_data:
|
||||
for s in serializer.validated_data['shared_remove']:
|
||||
Recipe.shared.through.objects.filter(recipe_id__in=safe_recipe_ids, user_id=s).delete()
|
||||
|
||||
if 'shared_set' in serializer.validated_data and len(serializer.validated_data['shared_set']) > 0:
|
||||
shared_relation = []
|
||||
Recipe.shared.through.objects.filter(recipe_id__in=safe_recipe_ids).delete()
|
||||
for r in recipes:
|
||||
for u in serializer.validated_data['shared_set']:
|
||||
shared_relation.append(Recipe.shared.through(recipe_id=r.pk, user_id=u))
|
||||
Recipe.shared.through.objects.bulk_create(shared_relation, ignore_conflicts=True, unique_fields=('recipe_id', 'user_id',))
|
||||
|
||||
if 'shared_remove_all' in serializer.validated_data and serializer.validated_data['shared_remove_all']:
|
||||
Recipe.shared.through.objects.filter(recipe_id__in=safe_recipe_ids).delete()
|
||||
|
||||
if 'clear_description' in serializer.validated_data and serializer.validated_data['clear_description']:
|
||||
recipes.update(description='')
|
||||
|
||||
if 'show_ingredient_overview' in serializer.validated_data and serializer.validated_data['show_ingredient_overview'] is not None:
|
||||
recipes.update(show_ingredient_overview=serializer.validated_data['show_ingredient_overview'])
|
||||
|
||||
return Response({}, 200)
|
||||
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
@extend_schema(responses=RecipeSerializer(many=False))
|
||||
@decorators.action(detail=True, pagination_class=None, methods=['PATCH'], serializer_class=RecipeSerializer)
|
||||
def delete_external(self, request, pk):
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space and has_group_permission(request.user, ['user']):
|
||||
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
|
||||
|
||||
if obj.storage:
|
||||
get_recipe_provider(obj).delete_file(obj)
|
||||
obj.storage = None
|
||||
obj.file_path = ''
|
||||
obj.file_uid = ''
|
||||
obj.save()
|
||||
|
||||
return Response(self.serializer_class(obj, many=False, context={'request': request}).data)
|
||||
|
||||
|
||||
@extend_schema_view(list=extend_schema(
|
||||
parameters=[OpenApiParameter(name='food_id', description='ID of food to filter for', type=int), ]))
|
||||
parameters=[OpenApiParameter(name='food_id', description='ID of food to filter for', type=int),
|
||||
OpenApiParameter(name='query', description='query that looks into food, base unit or converted unit by name', type=str), ]))
|
||||
class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = UnitConversion.objects
|
||||
serializer_class = UnitConversionSerializer
|
||||
@@ -1373,6 +1594,10 @@ class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
if food_id is not None:
|
||||
self.queryset = self.queryset.filter(food_id=food_id)
|
||||
|
||||
query = self.request.query_params.get('query', None)
|
||||
if query is not None:
|
||||
self.queryset = self.queryset.filter(Q(food__name__icontains=query) | Q(base_unit__name__icontains=query) | Q(converted_unit__name__icontains=query))
|
||||
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
@@ -1889,6 +2114,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']
|
||||
|
||||
@@ -1957,7 +2200,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,
|
||||
@@ -2262,7 +2513,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
|
||||
@@ -2415,7 +2665,7 @@ def meal_plans_to_ical(queryset, filename):
|
||||
request=inline_serializer(name="IngredientStringSerializer", fields={'text': CharField()}),
|
||||
responses=inline_serializer(name="ParsedIngredientSerializer",
|
||||
fields={'amount': IntegerField(), 'unit': CharField(), 'food': CharField(),
|
||||
'note': CharField()})
|
||||
'note': CharField(), 'original_text': CharField()})
|
||||
)
|
||||
@api_view(['POST'])
|
||||
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
||||
@@ -2425,13 +2675,19 @@ def ingredient_from_string(request):
|
||||
ingredient_parser = IngredientParser(request, False)
|
||||
amount, unit, food, note = ingredient_parser.parse(text)
|
||||
|
||||
ingredient = {'amount': amount, 'unit': None, 'food': None, 'note': note}
|
||||
ingredient = {'amount': amount, 'unit': None, 'food': None, 'note': note, 'original_text': text}
|
||||
if food:
|
||||
food, created = Food.objects.get_or_create(space=request.space, name=food)
|
||||
ingredient['food'] = {'name': food.name, 'id': food.id}
|
||||
if food_obj := Food.objects.filter(space=request.space).filter(Q(name=food) | Q(plural_name=food)).first():
|
||||
ingredient['food'] = {'name': food_obj.name, 'id': food_obj.id}
|
||||
else:
|
||||
food_obj = Food.objects.create(space=request.space, name=food)
|
||||
ingredient['food'] = {'name': food_obj.name, 'id': food_obj.id}
|
||||
|
||||
if unit:
|
||||
unit, created = Unit.objects.get_or_create(space=request.space, name=unit)
|
||||
ingredient['unit'] = {'name': unit.name, 'id': unit.id}
|
||||
if unit_obj := Unit.objects.filter(space=request.space).filter(Q(name=unit) | Q(plural_name=unit)).first():
|
||||
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
else:
|
||||
unit_obj = Unit.objects.create(space=request.space, name=unit)
|
||||
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
|
||||
return JsonResponse(ingredient, status=200)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from io import StringIO
|
||||
from uuid import UUID
|
||||
@@ -13,7 +14,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.cache import caches
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, PermissionDenied, BadRequest
|
||||
from django.core.management import call_command
|
||||
from django.db import models
|
||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
@@ -96,7 +97,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())
|
||||
@@ -238,11 +240,12 @@ def system(request):
|
||||
|
||||
for x in r.zrange('api:space-request-count', 0, 20, withscores=True, desc=True):
|
||||
s = x[0].decode('utf-8')
|
||||
space_stats = [Space.objects.get(pk=s).name, x[1]]
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
|
||||
api_space_stats.append(space_stats)
|
||||
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()
|
||||
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
|
||||
api_space_stats.append(space_stats)
|
||||
|
||||
cache_response = caches['default'].get(f'system_view_test_cache_entry', None)
|
||||
if not cache_response:
|
||||
@@ -266,6 +269,22 @@ def system(request):
|
||||
})
|
||||
|
||||
|
||||
def plugin_update(request):
|
||||
if not request.user.is_superuser:
|
||||
raise PermissionDenied
|
||||
|
||||
if not 'module' in request.GET:
|
||||
raise BadRequest
|
||||
|
||||
for p in PLUGINS:
|
||||
if p['module'] == request.GET['module']:
|
||||
update_response = subprocess.check_output(['git', 'pull'], cwd=p['base_path'])
|
||||
print(update_response)
|
||||
return HttpResponseRedirect(reverse('view_system'))
|
||||
|
||||
return HttpResponseRedirect(reverse('view_system'))
|
||||
|
||||
|
||||
def setup(request):
|
||||
with scopes_disabled():
|
||||
if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS:
|
||||
|
||||
@@ -11,34 +11,14 @@ If you like this application and want it to give back, there are many ways to co
|
||||
If you know any foreign languages you can:
|
||||
Improve the translations for any of the existing languages.
|
||||
|
||||
Add a new language to the long list of existing translations.
|
||||
|
||||
- Armenian
|
||||
- Bulgarian
|
||||
- Catalan
|
||||
- Czech
|
||||
- Danish
|
||||
- Dutch
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Hungarian
|
||||
- Italian
|
||||
- Latvian
|
||||
- Norwegian
|
||||
- Polish
|
||||
- Russian
|
||||
- Spanish
|
||||
- Swedish
|
||||
|
||||
See [here](/docs/contribute/translations) for further information on how to contribute translation to Tandoor.
|
||||
See [here](/contribute/translations/) for further information on how to contribute translation to Tandoor.
|
||||
|
||||
## Issues and Feature Requests
|
||||
|
||||
The most basic but also very important way of contributing is reporting issues and commenting on ideas and feature requests
|
||||
The most basic but also crucial way of contributing is reporting issues and commenting on ideas and feature requests
|
||||
over at [GitHub issues](https://github.com/vabene1111/recipes/issues).
|
||||
|
||||
Without feedback improvement can't happen, so don't hesitate to say what you want to say.
|
||||
Without feedback, improvement can't happen, so don't hesitate to say what you want to say.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -46,12 +26,12 @@ Helping improve the documentation for Tandoor is one of the easiest ways to give
|
||||
You can write guides on how to install and configure Tandoor expanding our repository of non-standard configuations.
|
||||
Or you can write how-to guides using some of Tandoor's advanced features such as authentication or automation.
|
||||
|
||||
See [here](/docs/contribute/documentation) for more information on how to add documentation to Tandoor.
|
||||
See [here](/contribute/documentation/) for more information on how to add documentation to Tandoor.
|
||||
|
||||
## Contributing Code
|
||||
|
||||
For the truly ambitious, you can help write code to fix issues, add additional features, or write your own scripts using
|
||||
Tandoor's extensive API and share your work with the community.
|
||||
|
||||
Before writing any code, please make sure that you review [contribution guidelines](/docs/contribute/guidelines) and
|
||||
[VSCode](/docs/contribute/vscode) or [PyCharm](/docs/contribute/pycharm) specific configurations.
|
||||
Before writing any code, please make sure that you review [contribution guidelines](/contribute/guidelines/) and
|
||||
[VSCode](/contribute/vscode) or [PyCharm](/contribute/pycharm) specific configurations.
|
||||
|
||||
@@ -32,10 +32,10 @@ To contribute to the project you are required to use the following packages with
|
||||
|
||||
## Testing
|
||||
|
||||
Django uses pytest-django to implement a full suite of testing. If you make any functional changes, please implment the appropriate
|
||||
Django uses pytest-django to implement a full suite of testing. If you make any functional changes, please implement the appropriate
|
||||
tests.
|
||||
|
||||
Tandoor is also actively soliciting contribors willing to setup vue3 testing. If you have knowledge in this area it would be greatly appreciated.
|
||||
Tandoor is also actively soliciting contributors willing to setup vue3 testing. If you have knowledge in this area it would be greatly appreciated.
|
||||
|
||||
## API Client
|
||||
|
||||
@@ -44,9 +44,7 @@ Tandoor is also actively soliciting contribors willing to setup vue3 testing. If
|
||||
The OpenAPI Generator is a Java project. You must have the java binary executable available on your PATH for this to work.
|
||||
|
||||
Tandoor uses [django-rest-framework](https://www.django-rest-framework.org/) for API implementation. Making contributions that impact the API requires an understanding of
|
||||
Viewsets and Serializers.
|
||||
|
||||
Also double check that your changes are actively reflected in the schema so that client apis are generated accurately.
|
||||
ViewSets and Serializers.
|
||||
|
||||
The API Client is generated automatically from the OpenAPI interface provided by the Django REST framework.
|
||||
For this [openapi-generator](https://github.com/OpenAPITools/openapi-generator) is used.
|
||||
@@ -55,17 +53,9 @@ Install it using your desired setup method. (For example, using `npm install @op
|
||||
|
||||
### Vue
|
||||
|
||||
Navigate to `vue/src/utils/openapi`.
|
||||
|
||||
Generate the schema using `openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8000/openapi/`. (Replace your dev server url if required.)
|
||||
|
||||
### Vue3
|
||||
|
||||
Navigate to `vue3/src/openapi`.
|
||||
|
||||
Generate the schema using `openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/`. (Replace your dev server url if required.)
|
||||
Generate the schema using the `generate_api_client.py` script in the main directory.
|
||||
|
||||
## Install and Configuration
|
||||
|
||||
Instructions for [VSCode](/docs/contribute/vscode)
|
||||
Instructions for [PyCharm](/docs/contribute/pycharm)
|
||||
Instructions for [VSCode](/contribute/vscode)
|
||||
Instructions for [PyCharm](/contribute/pycharm)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
!!! info "Development Setup"
|
||||
The dev setup is a little messy as this application combines the best (at least in my opinion) of both Django and Vue.js.
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
### Devcontainer Setup
|
||||
|
||||
@@ -32,17 +30,15 @@ populated from default values.
|
||||
|
||||
### Vue.js
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
!!! warning "Feature Freeze"
|
||||
With the exception of bug fixes, no changes will be accepted on the legacy `vue` front-end.
|
||||
<!-- prettier-ignore-end -->
|
||||
!!! danger "Development Setup"
|
||||
The vite dev server **must** be started before the django runserver command is run or else django will **not** recognize it and try to fallback to the build files.
|
||||
|
||||
Most new frontend pages are build using [Vue.js](https://vuejs.org/).
|
||||
The frontend is build using [Vue.js](https://vuejs.org/).
|
||||
|
||||
In order to work on these pages, you will have to install a Javascript package manager of your choice. The following examples use yarn.
|
||||
|
||||
In the `vue` folder run `yarn install` followed by `yarn build` to install and build the legacy front-end.
|
||||
In the `vue3` folder run `yarn install` followed by `yarn build` to install and build the new front-end.
|
||||
1. go to the `vue3` and run `yarn install` to install the dependencies
|
||||
2. run `yarn serve` to start the dev server that allows hot reloading and easy and quick development
|
||||
|
||||
After that you can use `yarn serve` from the `vue3` folder to start the development server, and proceed to test your changes.
|
||||
If you do not wish to work on those pages, but instead want the application to work properly during development, run `yarn build` to build the frontend pages once.
|
||||
If you do not wish to work on those pages, but instead want the application to work properly during development, run `yarn build` to build the frontend pages once. After that you
|
||||
might need to run `python manage.py collectstatic` to setup the static files.
|
||||
|
||||
@@ -16,8 +16,6 @@ Maintained by [Aimo](https://github.com/aimok04/kitshn)
|
||||
- Website: [https://kitshn.app/](https://kitshn.app/)
|
||||
- Appstores: [Apple](https://apps.apple.com/us/app/kitshn-for-tandoor/id6740168361), [Android](https://play.google.com/store/apps/details?id=de.kitshn.android)
|
||||
|
||||
|
||||
|
||||
### Untare (discontinued)
|
||||
|
||||
Maintained by [phantomate](https://github.com/phantomate/Untare)
|
||||
|
||||
@@ -33,17 +33,26 @@ VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=esbenp.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
In order to debug vue yarn and vite servers must be started before starting the django server.
|
||||
In order to hot reload vue, the `yarn dev` server must be started before starting the django server.
|
||||
|
||||
There are a number of built in tasks that are available. Here are a few of the key ones:
|
||||
|
||||
- `Setup Dev Server` - Runs all the prerequisite steps so that the dev server can be run inside VSCode.
|
||||
- `Setup Tests` - Runs all prerequisites so tests can be run inside VSCode.
|
||||
|
||||
Once these are run, you should be able to run/debug a django server in VSCode as well as run/debug tests directly through VSCode.
|
||||
There are also a few other tasks specified in case you have specific development needs:
|
||||
Once these are run, there are 2 options. If you want to run a vue3 server in a hot reload mode for quick development of the frontend, you should run a development vue server:
|
||||
|
||||
- `Yarn Dev` - Runs development Vue.js vite server not connected to VSCode. Useful if you want to make Vue changes and see them in realtime.
|
||||
|
||||
If not, you need to build and copy the frontend to the django server. If you make changes to the frontend, you need to re-run this and restart the django server:
|
||||
|
||||
- `Collect Static Files` - Builds and collects the vue3 frontend so that it can be served via the django server.
|
||||
|
||||
Once either of those steps are done, you can start the django server:
|
||||
|
||||
- `Run Dev Server` - Runs a django development server not connected to VSCode.
|
||||
|
||||
There are also a few other tasks specified in case you have specific development needs:
|
||||
|
||||
- `Run all pytests` - Runs all the pytests outside of VSCode.
|
||||
- `Yarn Serve` - Runs development Vue.js server not connected to VSCode. Useful if you want to make Vue changes and see them in realtime.
|
||||
- `Serve Documentation` - Runs a documentation server. Useful if you want to see how changes to documentation show up.
|
||||
|
||||
18
docs/features/ai.md
Normal file
18
docs/features/ai.md
Normal 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.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
21
plugin.py
Normal file
21
plugin.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
#TODO clean existing links for when plugins are uninstalled or not necessary because it will just be empty links?
|
||||
|
||||
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
|
||||
if os.path.isdir(PLUGINS_DIRECTORY):
|
||||
for d in os.listdir(PLUGINS_DIRECTORY):
|
||||
if d != '__pycache__':
|
||||
try:
|
||||
subprocess.run(['python', 'setup_repo.py'], shell=(os.name == 'nt'), cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print(f'ERROR failed to link plugin {d}')
|
||||
|
||||
subprocess.run(['npm', 'install', '--global', 'yarn'], shell=(os.name == 'nt'), cwd=os.path.join(BASE_DIR, 'vue3'))
|
||||
subprocess.run(['yarn', 'install'], shell=(os.name == 'nt'), cwd=os.path.join(BASE_DIR, 'vue3'))
|
||||
subprocess.run(['yarn', 'build'], shell=(os.name == 'nt'), cwd=os.path.join(BASE_DIR, 'vue3'))
|
||||
@@ -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)
|
||||
@@ -221,10 +221,7 @@ try:
|
||||
'module': f'recipes.plugins.{d}',
|
||||
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
||||
'base_url': plugin_class.base_url,
|
||||
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
|
||||
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
||||
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
||||
}
|
||||
PLUGINS.append(plugin_config)
|
||||
print(f'PLUGIN {d} loaded')
|
||||
@@ -534,28 +531,6 @@ if REDIS_HOST:
|
||||
# Vue webpack settings
|
||||
VUE_DIR = os.path.join(BASE_DIR, 'vue')
|
||||
|
||||
WEBPACK_LOADER = {
|
||||
'DEFAULT': {
|
||||
'CACHE': not DEBUG,
|
||||
'BUNDLE_DIR_NAME': 'vue/', # must end with slash
|
||||
'STATS_FILE': os.path.join(VUE_DIR, 'webpack-stats.json'),
|
||||
'POLL_INTERVAL': 0.1,
|
||||
'TIMEOUT': None,
|
||||
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
|
||||
},
|
||||
}
|
||||
|
||||
for p in PLUGINS:
|
||||
if p['bundle_name'] != '':
|
||||
WEBPACK_LOADER[p['bundle_name']] = {
|
||||
'CACHE': not DEBUG,
|
||||
'BUNDLE_DIR_NAME': 'vue/', # must end with slash
|
||||
'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'),
|
||||
'POLL_INTERVAL': 0.1,
|
||||
'TIMEOUT': None,
|
||||
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
|
||||
}
|
||||
|
||||
DJANGO_VITE = {
|
||||
"default": {
|
||||
"dev_mode": False,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Django==4.2.22
|
||||
Django==4.2.24
|
||||
cryptography===45.0.5
|
||||
django-annoying==0.10.6
|
||||
django-cleanup==9.0.0
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@vueform/multiselect": "^2.6.11",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@vueuse/router": "^13.6.0",
|
||||
@@ -22,7 +23,6 @@
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-simple-calendar": "7.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"vuetify": "^3.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -32,20 +32,21 @@
|
||||
"@types/node": "^24.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"esbuild-register": "^3.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.2",
|
||||
"workbox-core": "^7.3.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8",
|
||||
"workbox-background-sync": "^7.3.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-core": "^7.3.0",
|
||||
"workbox-expiration": "^7.3.0",
|
||||
"workbox-navigation-preload": "^7.3.0",
|
||||
"workbox-precaching": "^7.3.0",
|
||||
"workbox-routing": "^7.3.0",
|
||||
"workbox-strategies": "^7.3.0",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8"
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 13 KiB |
114
vue3/src/components/dialogs/AutoPlanDialog.vue
Normal file
114
vue3/src/components/dialogs/AutoPlanDialog.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<v-dialog max-width="600px" :activator="props.activator" v-model="dialog">
|
||||
<v-card :loading="loading">
|
||||
<v-closable-card-title v-model="dialog" :title="$t('Auto_Planner')" icon="fa-solid fa-calendar-plus"></v-closable-card-title>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<v-form>
|
||||
<model-select model="MealType" v-model="autoMealPlan.mealTypeId" :object="false"></model-select>
|
||||
<model-select model="Keyword" v-model="autoMealPlan.keywordIds" mode="tags" :object="false"></model-select>
|
||||
|
||||
<v-number-input :label="$t('Servings')" v-model="autoMealPlan.servings"></v-number-input>
|
||||
|
||||
<v-date-input :label="$t('Date')"
|
||||
multiple="range"
|
||||
v-model="dateRangeValue"
|
||||
:first-day-of-week="useUserPreferenceStore().deviceSettings.mealplan_startingDayOfWeek"
|
||||
:show-week="useUserPreferenceStore().deviceSettings.mealplan_displayWeekNumbers"
|
||||
prepend-icon=""
|
||||
prepend-inner-icon="$calendar"
|
||||
></v-date-input>
|
||||
|
||||
<model-select model="User" v-model="autoMealPlan.shared" mode="tags"></model-select>
|
||||
<v-checkbox v-model="autoMealPlan.addshopping" :label="$t('AddToShopping')" hide-details></v-checkbox>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn @click="dialog = false">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn color="create" prepend-icon="fa-solid fa-person-running" @click="doAutoPlan()" :loading="loading">{{ $t('Create') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {ApiApi, AutoMealPlan} from "@/openapi";
|
||||
import {onMounted, ref} from "vue";
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput'
|
||||
import {DateTime} from "luxon";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore.ts";
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const props = defineProps({
|
||||
activator: {type: String, default: 'parent'},
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const dialog = defineModel<boolean>({default: false})
|
||||
const loading = ref(false)
|
||||
|
||||
const dateRangeValue = ref([] as Date[])
|
||||
const autoMealPlan = ref({} as AutoMealPlan)
|
||||
|
||||
onMounted(() => {
|
||||
initializeRequest()
|
||||
})
|
||||
|
||||
/**
|
||||
* load default values for auto plan creation
|
||||
*/
|
||||
function initializeRequest() {
|
||||
autoMealPlan.value = {
|
||||
servings: 1,
|
||||
startDate: DateTime.now().toJSDate(),
|
||||
endDate: DateTime.now().plus({day: 7}).toJSDate(),
|
||||
shared: useUserPreferenceStore().userSettings.planShare,
|
||||
addshopping: useUserPreferenceStore().userSettings.mealplanAutoaddShopping,
|
||||
} as AutoMealPlan
|
||||
|
||||
dateRangeValue.value = []
|
||||
let currentDate = DateTime.fromJSDate(autoMealPlan.value.startDate).plus({day: 1}).toJSDate()
|
||||
while (currentDate <= autoMealPlan.value.endDate) {
|
||||
dateRangeValue.value.push(currentDate)
|
||||
currentDate = DateTime.fromJSDate(currentDate).plus({day: 1}).toJSDate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* perform auto plan creation
|
||||
*/
|
||||
function doAutoPlan() {
|
||||
let api = new ApiApi()
|
||||
loading.value = true
|
||||
|
||||
autoMealPlan.value.startDate = dateRangeValue.value[0]
|
||||
autoMealPlan.value.endDate = dateRangeValue.value[dateRangeValue.value.length - 1]
|
||||
console.log('requesting auto plan from ', autoMealPlan.value.startDate, ' to ', autoMealPlan.value.endDate)
|
||||
|
||||
api.apiAutoPlanCreate({autoMealPlan: autoMealPlan.value}).then(r => {
|
||||
dialog.value = false
|
||||
useMealPlanStore().refreshLastUpdatedPeriod()
|
||||
initializeRequest()
|
||||
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS)
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
98
vue3/src/components/dialogs/BatchDeleteDialog.vue
Normal file
98
vue3/src/components/dialogs/BatchDeleteDialog.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<v-dialog max-width="600px" :activator="props.activator" v-model="dialog">
|
||||
<v-card :loading="loading">
|
||||
<v-closable-card-title
|
||||
:title="$t('delete_title', {type: $t(genericModel.model.localizationKey)})"
|
||||
:sub-title="genericModel.getLabel(props.source)"
|
||||
:icon="genericModel.model.icon"
|
||||
v-model="dialog"
|
||||
></v-closable-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
{{ $t('BatchDeleteConfirm') }}
|
||||
|
||||
<v-list>
|
||||
<v-list-item border v-for="item in itemsToDelete">
|
||||
{{ genericModel.getLabel(item) }}
|
||||
<template #append>
|
||||
<v-icon icon="fa-solid fa-xmark" color="error" variant="tonal" v-if="failedItems.includes(item)"></v-icon>
|
||||
<v-icon icon="fa-solid fa-check" color="success" variant="tonal" v-else-if="updatedItems.includes(item)"></v-icon>
|
||||
<v-icon icon="fa-solid fa-circle-notch fa-spin" variant="tonal" color="info" v-else-if="loading"></v-icon>
|
||||
|
||||
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {food_id: item.id}}"
|
||||
v-if="genericModel.model.name == 'Food' && failedItems.includes(item)" size="small"></v-btn>
|
||||
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {unit_id: item.id}}"
|
||||
v-if="genericModel.model.name == 'Unit' && failedItems.includes(item)" size="small"></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<p class="font-italic text-disabled">{{$t('BatchDeleteHelp')}}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn :disabled="loading" @click="dialog = false">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="deleteAll()" :loading="loading">{{ $t('Delete_All') }}</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} from "@/types/Models.ts";
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const props = defineProps({
|
||||
model: {type: String as PropType<EditorSupportedModels>, required: true},
|
||||
items: {type: Array as PropType<Array<EditorSupportedTypes>>, required: true},
|
||||
activator: {type: String, default: 'parent'},
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const dialog = defineModel<boolean>({default: false})
|
||||
const loading = ref(false)
|
||||
|
||||
const genericModel = getGenericModelFromString(props.model, t)
|
||||
|
||||
const itemsToDelete = ref<EditorSupportedTypes[]>([])
|
||||
const failedItems = ref<EditorSupportedTypes[]>([])
|
||||
const updatedItems = ref<EditorSupportedTypes[]>([])
|
||||
|
||||
watch(dialog, (newValue, oldValue) => {
|
||||
if(!oldValue && newValue){
|
||||
itemsToDelete.value = JSON.parse(JSON.stringify(props.items))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* loop through the items and delete them
|
||||
*/
|
||||
function deleteAll() {
|
||||
let promises: Promise<any>[] = []
|
||||
loading.value = true
|
||||
|
||||
itemsToDelete.value.forEach(item => {
|
||||
promises.push(genericModel.destroy(item.id!).then((r: any) => {
|
||||
updatedItems.value.push(item)
|
||||
}).catch((err: any) => {
|
||||
failedItems.value.push(item)
|
||||
}))
|
||||
})
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
loading.value = false
|
||||
emit('change')
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
186
vue3/src/components/dialogs/BatchEditFoodDialog.vue
Normal file
186
vue3/src/components/dialogs/BatchEditFoodDialog.vue
Normal 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>
|
||||
166
vue3/src/components/dialogs/BatchEditRecipeDialog.vue
Normal file
166
vue3/src/components/dialogs/BatchEditRecipeDialog.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<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('Recipes'), count: updateItems.length})"
|
||||
:icon="TRecipe.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('Keywords')" :prepend-icon="TKeyword.icon" variant="plain">
|
||||
<v-card-text>
|
||||
<model-select model="Keyword" v-model="batchUpdateRequest.recipeBatchUpdate.keywordsAdd" :object="false" allow-create mode="tags">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-add"></v-icon>
|
||||
</template>
|
||||
</model-select>
|
||||
<model-select model="Keyword" v-model="batchUpdateRequest.recipeBatchUpdate.keywordsRemove" :object="false" allow-create mode="tags">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-minus"></v-icon>
|
||||
</template>
|
||||
</model-select>
|
||||
<model-select model="Keyword" v-model="batchUpdateRequest.recipeBatchUpdate.keywordsSet" :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('Keywords')})" hide-details v-model="batchUpdateRequest.recipeBatchUpdate.keywordsRemoveAll"></v-checkbox>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card :title="$t('Private_Recipe')" :subtitle="$t('Private_Recipe_Help')" prepend-icon="fa-solid fa-eye-slash" variant="plain">
|
||||
<v-card-text>
|
||||
|
||||
<v-select :items="boolUpdateOptions" :label="$t('Private_Recipe')" clearable v-model="batchUpdateRequest.recipeBatchUpdate._private"></v-select>
|
||||
|
||||
<model-select model="User" v-model="batchUpdateRequest.recipeBatchUpdate.sharedAdd" :object="false" allow-create mode="tags">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-add"></v-icon>
|
||||
</template>
|
||||
</model-select>
|
||||
<model-select model="User" v-model="batchUpdateRequest.recipeBatchUpdate.sharedRemove" :object="false" allow-create mode="tags">
|
||||
<template #prepend>
|
||||
<v-icon icon="fa-solid fa-minus"></v-icon>
|
||||
</template>
|
||||
</model-select>
|
||||
<model-select model="User" v-model="batchUpdateRequest.recipeBatchUpdate.sharedSet" :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('Users')})" hide-details v-model="batchUpdateRequest.recipeBatchUpdate.sharedRemoveAll"></v-checkbox>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card :title="$t('Miscellaneous')" prepend-icon="fa-solid fa-list" variant="plain">
|
||||
<v-card-text>
|
||||
<v-number-input :label="$t('WorkingTime')" v-model="batchUpdateRequest.recipeBatchUpdate.workingTime" :step="5">
|
||||
|
||||
</v-number-input>
|
||||
<v-number-input :label="$t('WaitingTime')" v-model="batchUpdateRequest.recipeBatchUpdate.waitingTime" :step="5">
|
||||
|
||||
</v-number-input>
|
||||
<v-number-input :label="$t('Serving')" v-model="batchUpdateRequest.recipeBatchUpdate.servings">
|
||||
|
||||
</v-number-input>
|
||||
<v-text-field :label="$t('ServingsText')" v-model="batchUpdateRequest.recipeBatchUpdate.servingsText" @update:model-value="updateServings = true">
|
||||
<template #append>
|
||||
<v-checkbox v-model="updateServings" hide-details></v-checkbox>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-select :items="boolUpdateOptions" :label="$t('show_ingredient_overview')" clearable v-model="batchUpdateRequest.recipeBatchUpdate.showIngredientOverview"></v-select>
|
||||
<v-checkbox hide-details :label="$t('DeleteSomething', {item: $t('Description')})" v-model="batchUpdateRequest.recipeBatchUpdate.clearDescription"></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="batchUpdateRecipes()" :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, TKeyword, TRecipe} from "@/types/Models.ts";
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {ApiApi, ApiRecipeBatchUpdateUpdateRequest, 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<RecipeOverview>>, required: true},
|
||||
activator: {type: String, default: 'parent'},
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const dialog = defineModel<boolean>({default: false})
|
||||
const loading = ref(false)
|
||||
|
||||
const updateItems = ref([] as RecipeOverview[])
|
||||
const batchUpdateRequest = ref({recipeBatchUpdate: {servingsText: ''}} as ApiRecipeBatchUpdateUpdateRequest)
|
||||
|
||||
const updateServings = ref(false)
|
||||
|
||||
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.recipeBatchUpdate.recipes = props.items.flatMap(r => r.id!)
|
||||
updateItems.value = JSON.parse(JSON.stringify(props.items))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* perform batch request to update recipes
|
||||
*/
|
||||
function batchUpdateRecipes() {
|
||||
let api = new ApiApi()
|
||||
loading.value = true
|
||||
|
||||
// prevent accidentally clearing the field with extra checkbox
|
||||
if (!updateServings.value) {
|
||||
batchUpdateRequest.value.recipeBatchUpdate.servingsText = undefined
|
||||
}
|
||||
|
||||
api.apiRecipeBatchUpdateUpdate(batchUpdateRequest.value).then(r => {
|
||||
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
emit('change')
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,26 +3,38 @@
|
||||
<v-card :loading="loading">
|
||||
<v-closable-card-title
|
||||
:title="$t('merge_title', {type: $t(genericModel.model.localizationKey)})"
|
||||
:sub-title="genericModel.getLabel(props.source)"
|
||||
:sub-title="sourceNames"
|
||||
:icon="genericModel.model.icon"
|
||||
v-model="dialog"
|
||||
></v-closable-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
{{ $t('merge_selection', {source: genericModel.getLabel(props.source), type: $t(genericModel.model.localizationKey)}) }}
|
||||
{{ $t('merge_selection', {source: sourceNames, type: $t(genericModel.model.localizationKey)}) }}
|
||||
<model-select :model="props.model" v-model="target" allow-create></model-select>
|
||||
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-card color="warning" variant="tonal">
|
||||
<v-card-title>{{ genericModel.getLabel(props.source) }}</v-card-title>
|
||||
</v-card>
|
||||
<v-icon icon="fa-solid fa-arrow-down" class="mt-4 mb-4"></v-icon>
|
||||
<v-card color="success" variant="tonal">
|
||||
<v-card-title v-if="!target">?</v-card-title>
|
||||
<v-card-title v-else>{{ genericModel.getLabel(target) }}</v-card-title>
|
||||
</v-card>
|
||||
<v-col>
|
||||
<v-list>
|
||||
<v-list-item border v-for="item in sourceItems">
|
||||
{{ genericModel.getLabel(item) }}
|
||||
|
||||
<template #append>
|
||||
<v-icon icon="fa-solid fa-xmark" color="error" variant="tonal" v-if="failedItems.includes(item)"></v-icon>
|
||||
<v-icon icon="fa-solid fa-check" color="success" variant="tonal" v-else-if="updatedItems.includes(item)"></v-icon>
|
||||
<v-icon icon="fa-solid fa-circle-notch fa-spin" variant="tonal" color="info" v-else-if="loading"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="text-center">
|
||||
<v-icon icon="fa-solid fa-arrow-down" class="mt-4 mb-4"></v-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="text-center" border>
|
||||
<span v-if="!target">?</span>
|
||||
<span v-else>{{ genericModel.getLabel(target) }}</span>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -30,7 +42,7 @@
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn :disabled="loading">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn :disabled="loading" @click="dialog = false">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn color="warning" @click="mergeModel()" :loading="loading" :disabled="!target">{{ $t('Merge') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -40,7 +52,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {PropType, ref} from "vue";
|
||||
import {computed, PropType, ref, watch} from "vue";
|
||||
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString} from "@/types/Models";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {useI18n} from "vue-i18n";
|
||||
@@ -51,7 +63,7 @@ const emit = defineEmits(['change'])
|
||||
|
||||
const props = defineProps({
|
||||
model: {type: String as PropType<EditorSupportedModels>, required: true},
|
||||
source: {type: {} as PropType<EditorSupportedTypes>, required: true},
|
||||
source: {type: Array as PropType<Array<EditorSupportedTypes>>, required: true},
|
||||
activator: {type: String, default: 'parent'},
|
||||
})
|
||||
|
||||
@@ -64,41 +76,62 @@ const automate = ref(false)
|
||||
const genericModel = getGenericModelFromString(props.model, t)
|
||||
const target = ref<null | EditorSupportedTypes>(null)
|
||||
|
||||
const sourceItems = ref<EditorSupportedTypes[]>([])
|
||||
const failedItems = ref<EditorSupportedTypes[]>([])
|
||||
const updatedItems = ref<EditorSupportedTypes[]>([])
|
||||
|
||||
watch(dialog, (newValue, oldValue) => {
|
||||
if (!oldValue && newValue) {
|
||||
sourceItems.value = JSON.parse(JSON.stringify(props.source))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* generate comma seperated list of item names that act as the source
|
||||
*/
|
||||
const sourceNames = computed(() => {
|
||||
if (sourceItems.value) {
|
||||
return sourceItems.value.map(i => genericModel.getLabel(i)).join(', ')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
/**
|
||||
* merge source into selected target
|
||||
*/
|
||||
function mergeModel() {
|
||||
let api = new ApiApi()
|
||||
let promises: Promise<any>[] = []
|
||||
|
||||
if (target.value != null) {
|
||||
loading.value = true
|
||||
|
||||
genericModel.merge(props.source, target.value).then(r => {
|
||||
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
|
||||
emit('change', target.value)
|
||||
sourceItems.value.forEach(sourceItem => {
|
||||
promises.push(genericModel.merge(sourceItem, target.value).then(r => {
|
||||
|
||||
if (automate.value && target.value != null && Object.hasOwn(props.source, 'name') && Object.hasOwn(target.value, 'name')) {
|
||||
let automation = {
|
||||
name: `${t('Merge') } ${props.source.name} -> ${target.value.name}`.substring(0,128),
|
||||
param1: props.source.name,
|
||||
param2: target.value.name,
|
||||
type: genericModel.model.mergeAutomation
|
||||
} as Automation
|
||||
api.apiAutomationCreate({automation: automation}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
dialog.value = false
|
||||
})
|
||||
}
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
if (!automate.value) {
|
||||
loading.value = false
|
||||
dialog.value = false
|
||||
}
|
||||
updatedItems.value.push(sourceItem)
|
||||
|
||||
if (automate.value && target.value != null && Object.hasOwn(sourceItem, 'name') && Object.hasOwn(sourceItem, 'name')) {
|
||||
let automation = {
|
||||
name: `${t('Merge')} ${sourceItem.name} -> ${target.value.name}`.substring(0, 128),
|
||||
param1: sourceItem.name,
|
||||
param2: target.value.name,
|
||||
type: genericModel.model.mergeAutomation
|
||||
} as Automation
|
||||
promises.push(api.apiAutomationCreate({automation: automation}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}))
|
||||
}
|
||||
}).catch(err => {
|
||||
updatedItems.value.push(sourceItem)
|
||||
}))
|
||||
})
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
loading.value = false
|
||||
emit('change')
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea :model-value="importLog.msg"></v-textarea>
|
||||
<v-textarea :model-value="importLog.msg" max-rows="25" auto-grow></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<slot name="prepend"></slot>
|
||||
|
||||
<v-chip class="me-1 mb-1" :label="props.label" :color="props.color" :size="props.size" :variant="props.variant" v-for="k in keywords"
|
||||
:to="{name: 'SearchPage', query: {keywords: k.id}}"> {{ k.label }}
|
||||
:to="useUserPreferenceStore().isAuthenticated ? {name: 'SearchPage', query: {keywords: k.id}} : undefined"> {{ k.label }}
|
||||
</v-chip>
|
||||
|
||||
<slot name="append"></slot>
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import {Keyword, KeywordLabel} from "@/openapi";
|
||||
import {computed, PropType} from "vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
|
||||
const props = defineProps({
|
||||
keywords: Array as PropType<Array<Keyword> | Array<KeywordLabel> | undefined>,
|
||||
|
||||
@@ -131,9 +131,9 @@ function refreshVisiblePeriod(startDateUnknown: boolean) {
|
||||
|
||||
// load backwards to as on initial
|
||||
if (startDateUnknown) {
|
||||
useMealPlanStore().refreshFromAPI(DateTime.fromJSDate(calendarDate.value).minus({days: days}).toJSDate(), DateTime.now().plus({days: days}).toJSDate())
|
||||
useMealPlanStore().refreshFromAPI(DateTime.fromJSDate(calendarDate.value).minus({days: days}).toJSDate(), DateTime.fromJSDate(calendarDate.value).plus({days: days}).toJSDate())
|
||||
} else {
|
||||
useMealPlanStore().refreshFromAPI(calendarDate.value, DateTime.now().plus({days: days}).toJSDate())
|
||||
useMealPlanStore().refreshFromAPI(calendarDate.value, DateTime.fromJSDate(calendarDate.value).plus({days: days}).toJSDate())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,17 @@
|
||||
|
||||
<template v-if="route.name == 'MealPlanPage'">
|
||||
<v-divider></v-divider>
|
||||
<v-list-item prepend-icon="fa-solid fa-calendar-plus" link>
|
||||
{{$t('Auto_Planner')}}
|
||||
<auto-plan-dialog></auto-plan-dialog>
|
||||
</v-list-item>
|
||||
<v-list-subheader>{{$t('Settings')}}</v-list-subheader>
|
||||
<v-list-item>
|
||||
<meal-plan-device-settings></meal-plan-device-settings>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -27,6 +32,7 @@ import {useRoute} from "vue-router";
|
||||
import {getListModels} from "@/types/Models";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import MealPlanDeviceSettings from "@/components/settings/MealPlanDeviceSettings.vue";
|
||||
import AutoPlanDialog from "@/components/dialogs/AutoPlanDialog.vue";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
27
vue3/src/components/display/PrivateRecipeBadge.vue
Normal file
27
vue3/src/components/display/PrivateRecipeBadge.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
<span v-if="props.showText" class="ms-1 me-1">{{ $t('Private_Recipe') }}</span>
|
||||
<v-chip class="me-1 mb-1" :color="props.color" :size="props.size" :variant="props.variant" v-for="u in users" :key="u.id" prepend-icon="fa-solid fa-share-nodes"> {{ u.displayName }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {User} from "@/openapi";
|
||||
import {PropType} from "vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
|
||||
const props = defineProps({
|
||||
showText: {type: Boolean, default: true},
|
||||
users: {type: [] as PropType<Array<User>>, required: false},
|
||||
|
||||
size: {type: String, default: 'x-small'},
|
||||
color: {type: String, default: ''},
|
||||
variant: {type: String as PropType<NonNullable<"tonal" | "flat" | "text" | "elevated" | "outlined" | "plain"> | undefined>, default: 'tonal'},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,40 +1,11 @@
|
||||
<template>
|
||||
|
||||
<v-card class="mt-1" v-if="cookLogs.length > 0">
|
||||
<v-card-title>{{ $t('Activity') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item v-for="c in cookLogs.sort((a,b) => a.createdAt! > b.createdAt! ? 1 : -1)" :key="c.id">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary">V</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-bold">{{ c.createdBy.displayName }}
|
||||
<v-rating density="comfortable" size="x-small" color="tandoor" class="float-right" v-model="c.rating" readonly v-if="c.rating != undefined"></v-rating>
|
||||
</v-list-item-title>
|
||||
|
||||
{{ c.comment }}
|
||||
|
||||
<p v-if="c.servings != null && c.servings > 0">
|
||||
{{ c.servings }}
|
||||
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
|
||||
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
|
||||
<span v-else>{{ $t('Servings') }}</span>
|
||||
</p>
|
||||
|
||||
<p class="text-disabled">
|
||||
{{ DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_SHORT) }}
|
||||
</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-1 d-print-none">
|
||||
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated" :loading="loading">
|
||||
<v-card-text>
|
||||
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment"></v-textarea>
|
||||
<v-row de>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="4">
|
||||
<v-label>{{$t('Rating')}}</v-label><br/>
|
||||
<v-label>{{ $t('Rating') }}</v-label>
|
||||
<br/>
|
||||
<v-rating v-model="newCookLog.rating" clearable hover density="compact"></v-rating>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
@@ -52,6 +23,48 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-1" v-if="cookLogs.length > 0" :loading="loading">
|
||||
<v-card-title>{{ $t('Activity') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item class="border-t-sm" v-for="c in cookLogs" :key="c.id" :link="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary">{{ c.createdBy.displayName.charAt(0) }}</v-avatar>
|
||||
</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>
|
||||
|
||||
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
|
||||
|
||||
{{ c.servings }}
|
||||
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
|
||||
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
|
||||
<span v-else>{{ $t('Servings') }}</span>
|
||||
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<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-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 }">
|
||||
<span v-bind="props">{{ DateTime.fromJSDate(c.createdAt).toRelative({style: 'narrow'}) }}</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
<model-edit-dialog model="CookLog" :item="c" v-if="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id" @save="recLoadCookLog(props.recipe.id)" @delete="recLoadCookLog(props.recipe.id)"></model-edit-dialog>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
@@ -62,6 +75,8 @@ import {ApiApi, CookLog, Recipe} from "@/openapi";
|
||||
import {DateTime} from "luxon";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput'
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
|
||||
const props = defineProps({
|
||||
recipe: {
|
||||
@@ -73,21 +88,31 @@ const props = defineProps({
|
||||
const newCookLog = ref({} as CookLog);
|
||||
|
||||
const cookLogs = ref([] as CookLog[])
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
refreshActivity()
|
||||
recLoadCookLog(props.recipe.id)
|
||||
resetForm()
|
||||
})
|
||||
|
||||
/**
|
||||
* load cook logs from database for given recipe
|
||||
* recursively load cook logs from database for given recipe
|
||||
*/
|
||||
function refreshActivity() {
|
||||
function recLoadCookLog(recipeId: number, page: number = 1) {
|
||||
const api = new ApiApi()
|
||||
api.apiCookLogList({recipe: props.recipe.id}).then(r => {
|
||||
// TODO pagination
|
||||
loading.value = true
|
||||
if(page == 1){
|
||||
cookLogs.value = []
|
||||
}
|
||||
api.apiCookLogList({recipe: props.recipe.id, page: page}).then(r => {
|
||||
if (r.results) {
|
||||
cookLogs.value = r.results
|
||||
cookLogs.value = cookLogs.value.concat(r.results)
|
||||
if (r.next) {
|
||||
recLoadCookLog(recipeId, page + 1)
|
||||
} else {
|
||||
cookLogs.value = cookLogs.value.sort((a, b) => a.createdAt! > b.createdAt! ? 1 : -1)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,14 +13,18 @@
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<!-- <v-btn icon="fas fa-ellipsis-v" size="small" variant="plain"></v-btn>-->
|
||||
<recipe-context-menu :recipe="props.recipe" size="small"></recipe-context-menu>
|
||||
<recipe-context-menu :recipe="props.recipe" size="small" v-if="props.showMenu"></recipe-context-menu>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <p class="text-disabled">{{ props.recipe.createdBy.displayName}}</p>-->
|
||||
<keywords-component variant="outlined" :keywords="props.recipe.keywords" :max-keywords="3">
|
||||
<keywords-component variant="outlined" :keywords="props.recipe.keywords" :max-keywords="3" v-if="props.showKeywords">
|
||||
<template #prepend>
|
||||
|
||||
<v-chip class="mb-1 me-1" size="x-small" label variant="outlined" v-if="recipe._private">
|
||||
<private-recipe-badge :show-text="false"></private-recipe-badge>
|
||||
</v-chip>
|
||||
<v-chip class="mb-1 me-1" size="x-small" label variant="outlined" color="info"
|
||||
v-if="!props.recipe.internal">
|
||||
v-if="props.recipe.internal == false">
|
||||
{{ $t('External') }}
|
||||
</v-chip>
|
||||
<v-chip class="mb-1 me-1" size="x-small" prepend-icon="far fa-clock" label variant="outlined"
|
||||
@@ -32,7 +36,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<v-card :to="`/recipe/${props.recipe.id}`" :style="{'height': props.height}" v-if="false">
|
||||
<v-card :to="{name: 'RecipeViewPage', params: {id: props.recipe.id}}" :style="{'height': props.height}" v-if="false">
|
||||
<v-tooltip
|
||||
class="align-center justify-center"
|
||||
location="top center" origin="overlap"
|
||||
@@ -100,14 +104,16 @@ import {Recipe, RecipeOverview} from "@/openapi";
|
||||
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
|
||||
import RecipeImage from "@/components/display/RecipeImage.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
|
||||
|
||||
const props = defineProps({
|
||||
recipe: {type: {} as PropType<Recipe | RecipeOverview>, required: true,},
|
||||
loading: {type: Boolean, required: false},
|
||||
show_keywords: {type: Boolean, required: false},
|
||||
showKeywords: {type: Boolean, default: true, required: false},
|
||||
show_description: {type: Boolean, required: false},
|
||||
height: {type: String, required: false, default: '15vh'},
|
||||
linkTarget: {type: String, required: false, default: ''}
|
||||
linkTarget: {type: String, required: false, default: ''},
|
||||
showMenu: {type: Boolean, default: true, required: false}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated"></recipe-context-menu>
|
||||
</v-sheet>
|
||||
<keywords-component variant="flat" class="ms-1" :keywords="recipe.keywords"></keywords-component>
|
||||
<private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge>
|
||||
<v-rating v-model="recipe.rating" size="x-small" v-if="recipe.rating" half-increments readonly></v-rating>
|
||||
<v-sheet class="ps-2 text-disabled">
|
||||
{{ recipe.description }}
|
||||
@@ -35,8 +36,7 @@
|
||||
</v-card>
|
||||
</v-card>
|
||||
|
||||
<!-- only display values if not all are default (e.g. for external recipes) -->
|
||||
<v-card class="mt-1" v-if="recipe.workingTime != 0 || recipe.waitingTime != 0 || recipe.servings != 1">
|
||||
<v-card class="mt-1">
|
||||
<v-container>
|
||||
<v-row class="text-center text-body-2">
|
||||
<v-col class="pt-1 pb-1">
|
||||
@@ -85,6 +85,8 @@
|
||||
<i>{{ recipe.description }}</i>
|
||||
</p>
|
||||
|
||||
<private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge>
|
||||
|
||||
<v-rating v-model="recipe.rating" size="x-small" v-if="recipe.rating" readonly></v-rating>
|
||||
|
||||
<keywords-component variant="flat" class="mt-4" :keywords="recipe.keywords"></keywords-component>
|
||||
@@ -119,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>
|
||||
@@ -146,7 +153,8 @@
|
||||
variant="outlined"
|
||||
:title="$t('CreatedBy')"
|
||||
:subtitle="recipe.createdBy.displayName"
|
||||
prepend-icon="fa-solid fa-user">
|
||||
prepend-icon="fa-solid fa-user"
|
||||
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {createdby: recipe.createdBy.id!}}: undefined">
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
@@ -154,7 +162,8 @@
|
||||
variant="outlined"
|
||||
:title="$t('Created')"
|
||||
:subtitle="DateTime.fromJSDate(recipe.createdAt).toLocaleString(DateTime.DATETIME_MED)"
|
||||
prepend-icon="$create">
|
||||
prepend-icon="$create"
|
||||
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {createdon: DateTime.fromJSDate(recipe.createdAt).toISODate()}} : undefined">
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
@@ -162,7 +171,8 @@
|
||||
variant="outlined"
|
||||
:title="$t('Updated')"
|
||||
:subtitle="DateTime.fromJSDate(recipe.updatedAt).toLocaleString(DateTime.DATETIME_MED)"
|
||||
prepend-icon="$edit">
|
||||
prepend-icon="$edit"
|
||||
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {updatedon: DateTime.fromJSDate(recipe.updatedAt).toISODate()}}: undefined">
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" v-if="recipe.sourceUrl">
|
||||
@@ -186,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";
|
||||
@@ -201,6 +211,8 @@ import PropertyView from "@/components/display/PropertyView.vue";
|
||||
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()
|
||||
@@ -211,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
|
||||
*/
|
||||
@@ -243,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
|
||||
@@ -252,7 +266,7 @@ function aiConvertRecipe() {
|
||||
recipe.value.servingsText = r.recipe.servingsText
|
||||
recipe.value.workingTime = r.recipe.workingTime
|
||||
recipe.value.waitingTime = r.recipe.waitingTime
|
||||
|
||||
|
||||
servings.value = r.recipe.servings
|
||||
loading.value = true
|
||||
|
||||
|
||||
@@ -15,17 +15,16 @@
|
||||
<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">
|
||||
{{ $n(a.amount) }}
|
||||
<span v-if="a.unit">{{ a.unit.name }}</span>
|
||||
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1)">{{ $n(a.amount) }}</span>
|
||||
<span class="ms-1" v-if="a.unit">{{ pluralString(a.unit, a.amount) }}</span>
|
||||
</span>
|
||||
|
||||
</b>
|
||||
</span>
|
||||
<br/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
{{ shoppingListFood.food.name }} <br/>
|
||||
{{ pluralString(shoppingListFood.food, (amounts.length > 1 || (amounts.length == 1 && amounts[0].amount > 1) ? 2 : 1)) }} <br/>
|
||||
<span v-if="infoRow"><small class="text-disabled">{{ infoRow }}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,6 +58,7 @@ import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
|
||||
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
|
||||
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
|
||||
import {pluralString} from "@/utils/model_utils.ts";
|
||||
|
||||
const emit = defineEmits(['clicked'])
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
<!-- TODO remove once append to body for model select is working properly -->
|
||||
<v-spacer style="margin-top: 120px;"></v-spacer>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -180,7 +182,8 @@
|
||||
<template #append>
|
||||
<v-btn icon="$create" color="create" :disabled="manualAddRecipe == undefined">
|
||||
<v-icon icon="$create"></v-icon>
|
||||
<add-to-shopping-dialog :recipe="manualAddRecipe" v-if="manualAddRecipe != undefined" @created="useShoppingStore().refreshFromAPI(); manualAddRecipe = undefined"></add-to-shopping-dialog>
|
||||
<add-to-shopping-dialog :recipe="manualAddRecipe" v-if="manualAddRecipe != undefined"
|
||||
@created="useShoppingStore().refreshFromAPI(); manualAddRecipe = undefined"></add-to-shopping-dialog>
|
||||
</v-btn>
|
||||
</template>
|
||||
</ModelSelect>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<template>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title><i class="far fa-list-alt fa-fw me-2"></i> {{ $t('StepsOverview') }}</v-expansion-panel-title>
|
||||
<v-expansion-panel-title>
|
||||
<i class="far fa-list-alt fa-fw me-2"></i> {{ $t('StepsOverview') }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-container>
|
||||
<v-row v-for="(s, i) in props.steps">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn-toggle density="compact" v-model="useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview" border divided>
|
||||
<v-btn :value="false" prepend-icon="fa-solid fa-folder-tree">{{ $t('Structured') }}</v-btn>
|
||||
<v-btn :value="true" prepend-icon="fa-solid fa-arrows-to-circle">{{ $t('Summary') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(s, i) in props.steps" v-if="!useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview">
|
||||
<v-col class="pa-1" cols="12" md="6">
|
||||
<b v-if="s.showAsHeader">{{ i + 1 }}. {{ s.name }} </b>
|
||||
<ingredients-table v-model="s.ingredients" :ingredient-factor="props.ingredientFactor"></ingredients-table>
|
||||
@@ -21,7 +30,13 @@
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-row v-if="useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview">
|
||||
<v-col class="pa-1" cols="12" md="6">
|
||||
<ingredients-table v-model="mergedIngredients" :ingredient-factor="props.ingredientFactor" :show-checkbox="false"></ingredients-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
@@ -30,10 +45,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {PropType} from 'vue'
|
||||
import {Step} from "@/openapi";
|
||||
import {computed, PropType, ref} from 'vue'
|
||||
import {Ingredient, Step} from "@/openapi";
|
||||
import IngredientsTable from "@/components/display/IngredientsTable.vue";
|
||||
import StepView from "@/components/display/StepView.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
|
||||
const props = defineProps({
|
||||
steps: {
|
||||
@@ -46,6 +61,70 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const showMergedIngredients = ref(false)
|
||||
|
||||
const mergedIngredients = computed(() => {
|
||||
// Function to collect all ingredients from recipe steps
|
||||
const getAllIngredients = () => {
|
||||
const ingredients: Array<Ingredient> = [];
|
||||
|
||||
// Add ingredients from steps
|
||||
props.steps.forEach(step => {
|
||||
step.ingredients.forEach(ingredient => {
|
||||
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
|
||||
ingredients.push(ingredient);
|
||||
}
|
||||
});
|
||||
|
||||
// Add ingredients from step recipes if they exist
|
||||
if (step.stepRecipeData) {
|
||||
step.stepRecipeData.steps?.forEach((subStep: Step) => {
|
||||
subStep.ingredients.forEach((ingredient: Ingredient) => {
|
||||
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
|
||||
ingredients.push(ingredient);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ingredients;
|
||||
};
|
||||
|
||||
// Get all ingredients
|
||||
const allIngredients = getAllIngredients();
|
||||
|
||||
// Create a map to group and sum ingredients by food and unit
|
||||
const groupedIngredients = new Map<string, Ingredient>();
|
||||
|
||||
allIngredients.forEach(ingredient => {
|
||||
if (!ingredient.food || !ingredient.unit) return;
|
||||
|
||||
// Create a unique key for food-unit combination
|
||||
const key = `${ingredient.food.id}-${ingredient.unit.id}`;
|
||||
|
||||
if (groupedIngredients.has(key)) {
|
||||
// If this food-unit combination already exists, sum the amounts
|
||||
const existingIngredient = groupedIngredients.get(key)!;
|
||||
existingIngredient.amount += ingredient.amount;
|
||||
} else {
|
||||
// Create a new entry with the adjusted amount
|
||||
const clonedIngredient = {...ingredient};
|
||||
groupedIngredients.set(key, clonedIngredient);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert the map back to an array
|
||||
const result = Array.from(groupedIngredients.values());
|
||||
|
||||
// Sort alphabetically by food name
|
||||
return result.sort((a, b) => {
|
||||
const foodNameA = a.food?.name.toLowerCase() || '';
|
||||
const foodNameB = b.food?.name.toLowerCase() || '';
|
||||
return foodNameA.localeCompare(foodNameB);
|
||||
});
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
178
vue3/src/components/inputs/HierarchyEditor.vue
Normal file
178
vue3/src/components/inputs/HierarchyEditor.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<v-row justify="space-between" dense>
|
||||
<v-col cols="6">
|
||||
<v-card :loading="loading" variant="outlined">
|
||||
<v-card-text>
|
||||
<v-treeview
|
||||
v-model:activated="activeObjs"
|
||||
return-object
|
||||
activatable
|
||||
rounded
|
||||
indent-lines
|
||||
hide-actions
|
||||
density="compact"
|
||||
open-all
|
||||
item-title="name"
|
||||
:items="objTree"
|
||||
:disabled="loading">
|
||||
<template v-slot:append="{ item, depth, isFirst, isLast }">
|
||||
<v-icon icon="fa-solid fa-location-crosshairs" v-if="item.id == editingObj.id!"></v-icon>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-card v-if="activeObjs.length == 1" :title="activeObjs[0].name" :prepend-icon="genericModel.model.icon" variant="outlined">
|
||||
<v-card-text>
|
||||
<v-label>{{$t('AddChild')}}</v-label>
|
||||
<model-select :model="genericModel.model.name" v-model="addChildObj" allow-create>
|
||||
<template #append>
|
||||
<v-btn color="save" icon="$save" :disabled="addChildObj == undefined" @click="moveObject(addChildObj,activeObjs[0].id!); addChildObj = undefined"></v-btn>
|
||||
</template>
|
||||
</model-select>
|
||||
<v-label>{{$t('Parent')}}</v-label>
|
||||
<model-select :model="genericModel.model.name" v-model="setParentObj" allow-create>
|
||||
<template #append>
|
||||
<v-btn color="save" icon="$save" :disabled="setParentObj == undefined" @click="moveObject(activeObjs[0], setParentObj.id!); setParentObj = undefined"></v-btn>
|
||||
</template>
|
||||
</model-select>
|
||||
|
||||
<v-btn @click="moveObject(activeObjs[0],0)" class="mt-2" color="warning" prepend-icon="fa-solid fa-link-slash" block>
|
||||
{{$t('RemoveParent')}}
|
||||
</v-btn>
|
||||
<v-btn block prepend-icon="$edit" color="info" class="mt-4"
|
||||
:to="{name: 'ModelEditPage' , params: {model: genericModel.model.name, id: activeObjs[0].id }}"
|
||||
v-if="activeObjs[0].id != editingObj.id!">
|
||||
{{ $t('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn block prepend-icon="$search" color="success" class="mt-4 mb-10"
|
||||
:to="searchLink" target="_blank"
|
||||
v-if="searchLink">
|
||||
{{ $t('Recipes') }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, onMounted, PropType, ref} from "vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString} from "@/types/Models.ts";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
type Tree<T> = T & { children: Tree<T>[] };
|
||||
|
||||
const props = defineProps({
|
||||
model: {type: String as PropType<EditorSupportedModels>, required: true},
|
||||
})
|
||||
|
||||
const editingObj = defineModel<EditorSupportedTypes>({required: true})
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
/**
|
||||
* compute tree structure based on object list
|
||||
*/
|
||||
const objTree = computed(() => {
|
||||
return buildTreeDFS(objList.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* link to search for recipes using the selected object
|
||||
*/
|
||||
const searchLink = computed(() => {
|
||||
if (activeObjs.value.length == 1) {
|
||||
if (props.model == 'Keyword') {
|
||||
return {name: 'SearchPage', query: {keywords: activeObjs.value[0].id!}}
|
||||
} else if (props.model == 'Food') {
|
||||
return {name: 'SearchPage', query: {keywords: activeObjs.value[0].id!}}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
const objList = ref([] as EditorSupportedTypes[])
|
||||
|
||||
const activeObjs = ref([] as EditorSupportedTypes[])
|
||||
const addChildObj = ref<undefined | EditorSupportedTypes>(undefined)
|
||||
const setParentObj = ref<undefined | EditorSupportedTypes>(undefined)
|
||||
|
||||
const genericModel = ref(getGenericModelFromString(props.model, t))
|
||||
|
||||
onMounted(() => {
|
||||
recLoadObjectTree(editingObj.value.id!, 1)
|
||||
})
|
||||
|
||||
/**
|
||||
* recursively load all objects belonging to the selected objects tree
|
||||
* @param objId base object id to look for tree
|
||||
* @param page page to load
|
||||
*/
|
||||
function recLoadObjectTree(objId: number, page: number) {
|
||||
loading.value = true
|
||||
|
||||
genericModel.value.list({rootTree: objId, pageSize: 100}).then(response => {
|
||||
objList.value = objList.value.concat(response.results)
|
||||
if (response.next) {
|
||||
recLoadObjectTree(objId, page + 1)
|
||||
} else {
|
||||
if (activeObjs.value.length == 0) {
|
||||
activeObjs.value = [objTree.value.find(x => x.id! == editingObj.value.id!)]
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* move the given obj to the desired parent and update in local data cache
|
||||
* @param obj object to change parent for
|
||||
* @param parentId parent id to change the object to or 0 to remove parent
|
||||
*/
|
||||
function moveObject(obj: EditorSupportedTypes, parentId: number) {
|
||||
loading.value = true
|
||||
|
||||
genericModel.value.move(obj, parentId).then((r: any) => {
|
||||
objList.value = []
|
||||
recLoadObjectTree(editingObj.value.id!, 1)
|
||||
}).catch((err: any) => {
|
||||
loading.value = false
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build a tree datastructures from a DFS ordered list of objects
|
||||
*/
|
||||
function buildTreeDFS<T extends { id: number; parent: number | null }>(items: T[]): Tree<T>[] {
|
||||
let index = 0;
|
||||
|
||||
function buildSubtree(parentId: number | null): Tree<T>[] {
|
||||
const children: Tree<T>[] = [];
|
||||
while (index < items.length && items[index].parent === parentId) {
|
||||
const item = items[index++];
|
||||
const node: Tree<T> = {
|
||||
...item,
|
||||
children: buildSubtree(item.id) // recurse immediately, consumes children in DFS order
|
||||
};
|
||||
children.push(node);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
return buildSubtree(null); // start from roots (parent === null)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
<v-list-item :to="{ name: 'ModelEditPage', params: {model: 'recipe', id: recipe.id} }" prepend-icon="$edit">
|
||||
{{ $t('Edit') }}
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="$mealplan" link>
|
||||
<v-list-item prepend-icon="$mealplan" @click="mealPlanDialog = true">
|
||||
{{ $t('Add_to_Plan') }}
|
||||
<model-edit-dialog model="MealPlan" :itemDefaults="{recipe: recipe, servings: recipe.servings}"></model-edit-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="$shopping" link>
|
||||
{{ $t('Add_to_Shopping') }}
|
||||
@@ -30,11 +29,12 @@
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
|
||||
<model-edit-dialog model="MealPlan" :itemDefaults="{recipe: recipe, servings: recipe.servings}" :close-after-create="false" :close-after-save="false" v-model="mealPlanDialog"></model-edit-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {nextTick, PropType} from 'vue'
|
||||
import {nextTick, PropType, ref} from 'vue'
|
||||
import {Recipe, RecipeFlat, RecipeOverview} from "@/openapi";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
|
||||
@@ -46,6 +46,8 @@ const props = defineProps({
|
||||
size: {type: String, default: 'medium'},
|
||||
})
|
||||
|
||||
const mealPlanDialog = ref(false)
|
||||
|
||||
function openPrintView() {
|
||||
print()
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<div v-if="!mobile">
|
||||
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" :empty-insert-threshold="25" group="ingredients">
|
||||
<v-row v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" dense>
|
||||
<v-col cols="12" class="pa-0 ma-0 text-center text-disabled">
|
||||
<v-col cols="12" class="pa-0 ma-0 text-center text-disabled" v-if="ingredient.originalText">
|
||||
<v-icon icon="$import" size="x-small"></v-icon>
|
||||
{{ ingredient.originalText }}
|
||||
</v-col>
|
||||
@@ -306,6 +306,7 @@ function parseAndInsertIngredients() {
|
||||
r.forEach(i => {
|
||||
console.log(i)
|
||||
step.value.ingredients.push({
|
||||
originalText: i.value.originalText,
|
||||
amount: i.value.amount,
|
||||
food: i.value.food,
|
||||
unit: i.value.unit,
|
||||
|
||||
105
vue3/src/components/model_editors/AiProviderEditor.vue
Normal file
105
vue3/src/components/model_editors/AiProviderEditor.vue
Normal 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>
|
||||
@@ -12,11 +12,22 @@
|
||||
<v-card-text>
|
||||
<v-form :disabled="loading">
|
||||
|
||||
<v-textarea :label="$t('Comment')" rows="2" v-model="editingObj.comment"></v-textarea>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="4">
|
||||
<v-label>{{ $t('Rating') }}</v-label>
|
||||
<br/>
|
||||
<v-rating v-model="editingObj.rating" clearable hover density="compact"></v-rating>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
|
||||
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
|
||||
<v-text-field :label="$t('Code')" v-model="editingObj.code"></v-text-field>
|
||||
<v-number-input :label="$t('Servings')" v-model="editingObj.servings" :precision="2"></v-number-input>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-date-input :label="$t('Date')" v-model="editingObj.createdAt"></v-date-input>
|
||||
|
||||
<v-textarea :label="$t('Comment')" v-model="editingObj.comment"></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
@@ -27,19 +38,20 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {OpenDataFood, OpenDataVersion} from "@/openapi";
|
||||
import {CookLog} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<OpenDataVersion>, required: false, default: null},
|
||||
item: {type: {} as PropType<CookLog>, required: false, default: null},
|
||||
itemId: {type: [Number, String], required: false, default: undefined},
|
||||
itemDefaults: {type: {} as PropType<OpenDataVersion>, required: false, default: {} as OpenDataVersion},
|
||||
itemDefaults: {type: {} as PropType<CookLog>, required: false, default: {} as CookLog},
|
||||
dialog: {type: Boolean, default: false}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<OpenDataVersion>('OpenDataVersion', emit)
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<CookLog>('CookLog', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
@@ -58,7 +70,7 @@ onMounted(() => {
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<v-tab value="food">{{ $t('Food') }}</v-tab>
|
||||
<v-tab value="properties">{{ $t('Properties') }}</v-tab>
|
||||
<v-tab value="conversions">{{ $t('Conversion') }}</v-tab>
|
||||
<v-tab value="hierarchy">{{ $t('Hierarchy') }}</v-tab>
|
||||
<v-tab value="misc">{{ $t('Miscellaneous') }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-card-text>
|
||||
@@ -53,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>
|
||||
|
||||
@@ -83,7 +84,7 @@
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select v-model="uc.baseUnit" model="Unit" hide-details></model-select>
|
||||
<model-select v-model="uc.baseUnit" model="Unit" hide-details></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
@@ -97,7 +98,7 @@
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select v-model="uc.convertedUnit" model="Unit"></model-select>
|
||||
<model-select v-model="uc.convertedUnit" model="Unit"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@@ -105,7 +106,22 @@
|
||||
</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">
|
||||
<hierarchy-editor v-model="editingObj" :model="modelClass.model.name"></hierarchy-editor>
|
||||
|
||||
<v-checkbox :label="$t('substitute_siblings')" :hint="$t('substitute_siblings_help')" v-model="editingObj.substituteSiblings" persistent-hint></v-checkbox>
|
||||
<v-checkbox :label="$t('substitute_children')" :hint="$t('substitute_children_help')" v-model="editingObj.substituteChildren" persistent-hint></v-checkbox>
|
||||
|
||||
<ModelSelect model="FoodInheritField" v-model="editingObj.inheritFields" :label="$t('InheritFields')" :hint="$t('InheritFields_help')"
|
||||
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">
|
||||
@@ -117,14 +133,6 @@
|
||||
<v-divider class="mt-2 mb-2"></v-divider>
|
||||
<ModelSelect model="Food" v-model="editingObj.substitute" :label="$t('Substitutes')" :hint="$t('substitute_help')" mode="tags"></ModelSelect>
|
||||
|
||||
<v-checkbox :label="$t('substitute_siblings')" :hint="$t('substitute_siblings_help')" v-model="editingObj.substituteSiblings" persistent-hint></v-checkbox>
|
||||
<v-checkbox :label="$t('substitute_children')" :hint="$t('substitute_children_help')" v-model="editingObj.substituteChildren" persistent-hint></v-checkbox>
|
||||
|
||||
<ModelSelect model="FoodInheritField" v-model="editingObj.inheritFields" :label="$t('InheritFields')" :hint="$t('InheritFields_help')"
|
||||
mode="tags"></ModelSelect>
|
||||
<ModelSelect model="FoodInheritField" v-model="editingObj.childInheritFields" :label="$t('ChildInheritFields')" :hint="$t('ChildInheritFields_help')"
|
||||
mode="tags"></ModelSelect>
|
||||
|
||||
<!-- TODO re-add reset inheritance button/api call /function (previously annotated field on food -->
|
||||
<v-text-field :label="$t('Open_Data_Slug')" :hint="$t('open_data_help_text')" persistent-hint v-model="editingObj.openDataSlug" disabled></v-text-field>
|
||||
</v-form>
|
||||
@@ -154,6 +162,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import FdcSearchDialog from "@/components/dialogs/FdcSearchDialog.vue";
|
||||
import {openFdcPage} from "@/utils/fdc.ts";
|
||||
import {DateTime} from "luxon";
|
||||
import HierarchyEditor from "@/components/inputs/HierarchyEditor.vue";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@@ -212,7 +221,7 @@ onMounted(() => {
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.propertiesFoodAmount = 100
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,13 +9,29 @@
|
||||
: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="keyword">{{ $t('Keyword') }}</v-tab>
|
||||
<v-tab value="hierarchy">{{ $t('Hierarchy') }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
<v-card-text>
|
||||
<v-form :disabled="loading">
|
||||
<v-tabs-window v-model="tab">
|
||||
<v-tabs-window-item value="keyword">
|
||||
<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('Name')" v-model="editingObj.name"></v-text-field>
|
||||
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
|
||||
|
||||
</v-form>
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="hierarchy">
|
||||
<hierarchy-editor v-model="editingObj" :model="modelClass.model.name"></hierarchy-editor>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
</model-editor-base>
|
||||
|
||||
@@ -23,10 +39,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, watch} from "vue";
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {Keyword} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import HierarchyEditor from "@/components/inputs/HierarchyEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<Keyword>, required: false, default: null},
|
||||
@@ -38,6 +55,8 @@ const props = defineProps({
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Keyword>('Keyword', emit)
|
||||
|
||||
const tab = ref("keyword")
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
@@ -55,7 +74,7 @@ onMounted(() => {
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
|
||||
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{$t('Steps')}} </v-btn>
|
||||
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
|
||||
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{ $t('Steps') }}</v-btn>
|
||||
</v-form>
|
||||
|
||||
</v-tabs-window-item>
|
||||
@@ -77,12 +77,19 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-btn-group density="compact">
|
||||
<v-btn-group density="compact" divided border>
|
||||
<v-btn color="success" prepend-icon="fa-solid fa-plus" @click="addStep()">{{ $t('Add_Step') }}</v-btn>
|
||||
<v-btn color="warning" @click="dialogStepManager = true">
|
||||
<v-btn color="warning" @click="dialogStepManager = true" :disabled="editingObj.steps.length < 2">
|
||||
<v-icon icon="fa-solid fa-arrow-down-1-9"></v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn prepend-icon="fa-solid fa-maximize" @click="handleSplitAllSteps" :disabled="editingObj.steps.length < 1"><span
|
||||
v-if="!mobile">{{ $t('Split') }}</span></v-btn>
|
||||
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps" :disabled="editingObj.steps.length < 2"><span
|
||||
v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
|
||||
</v-btn-group>
|
||||
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -100,17 +107,26 @@
|
||||
v-model="editingObj.showIngredientOverview"></v-checkbox>
|
||||
|
||||
<v-text-field :label="$t('Imported_From')" v-model="editingObj.sourceUrl"></v-text-field>
|
||||
<v-checkbox :label="$t('Private_Recipe')" persistent-hint v-model="editingObj._private"></v-checkbox>
|
||||
<model-select mode="tags" model="User" :label="$t('Private_Recipe')" :hint="$t('Private_Recipe_Help')" persistent-hint v-model="editingObj.shared"
|
||||
<v-checkbox :label="$t('Private_Recipe')" persistent-hint :hint="$t('Private_Recipe_Help')" v-model="editingObj._private"></v-checkbox>
|
||||
<model-select mode="tags" model="User" :label="$t('Share')" persistent-hint v-model="editingObj.shared"
|
||||
append-to-body v-if="editingObj._private"></model-select>
|
||||
|
||||
<div class="mt-2" v-if="editingObj.filePath">
|
||||
{{ $t('ExternalRecipe') }}
|
||||
<v-text-field readonly v-model="editingObj.filePath"></v-text-field>
|
||||
|
||||
<v-btn prepend-icon="$delete" color="error" :loading="loading">{{ $t('delete_title', {type: $t('ExternalRecipe')}) }}
|
||||
<delete-confirm-dialog :object-name="editingObj.filePath" :model-name="$t('ExternalRecipe')" @delete="deleteExternalFile()"></delete-confirm-dialog>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
|
||||
<v-alert color="warning" icon="fa-solid fa-triangle-exclamation">
|
||||
{{$t('SpaceLimitReached')}}
|
||||
{{ $t('SpaceLimitReached') }}
|
||||
<v-btn color="success" variant="flat" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -138,7 +154,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref, shallowRef, watch} from "vue";
|
||||
import {Ingredient, Recipe, Step} from "@/openapi";
|
||||
import {ApiApi, Ingredient, Recipe, Step} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
@@ -151,7 +167,9 @@ import ClosableHelpAlert from "@/components/display/ClosableHelpAlert.vue";
|
||||
import {useDisplay} from "vuetify";
|
||||
import {isSpaceAtRecipeLimit} from "@/utils/logic_utils";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import SpaceSettings from "@/components/settings/SpaceSettings.vue";
|
||||
import {mergeAllSteps, splitAllSteps} from "@/utils/step_utils.ts";
|
||||
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@@ -188,7 +206,7 @@ onMounted(() => {
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor(){
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {
|
||||
newItemFunction: () => {
|
||||
editingObj.value.steps = [] as Step[]
|
||||
@@ -249,6 +267,33 @@ function deleteStepAtIndex(index: number) {
|
||||
editingObj.value.steps.splice(index, 1)
|
||||
}
|
||||
|
||||
function handleMergeAllSteps(): void {
|
||||
if (editingObj.value.steps) {
|
||||
mergeAllSteps(editingObj.value.steps)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSplitAllSteps(): void {
|
||||
if (editingObj.value.steps) {
|
||||
splitAllSteps(editingObj.value.steps, '\n')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes the external file for the recipe
|
||||
*/
|
||||
function deleteExternalFile() {
|
||||
let api = new ApiApi()
|
||||
loading.value = true
|
||||
api.apiRecipeDeleteExternalPartialUpdate({id: editingObj.value.id!, patchedRecipe: editingObj.value}).then(r => {
|
||||
editingObj.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import {useI18n} from "vue-i18n";
|
||||
import BtnCopy from "@/components/buttons/BtnCopy.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
@@ -111,7 +112,7 @@ function deleteInviteLink(inviteLink: InviteLink) {
|
||||
* @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}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -78,19 +78,38 @@
|
||||
|
||||
|
||||
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
|
||||
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
|
||||
|
||||
<!-- <model-select v-model="space.foodInherit" model="FoodInheritField" mode="tags"></model-select>-->
|
||||
<p class="text-h6 mt-2">{{ $t('AI') }}</p>
|
||||
<v-divider class="mb-2"></v-divider>
|
||||
<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="space.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
|
||||
|
||||
<template v-if="space.aiEnabled">
|
||||
<model-select model="AiProvider" :label="$t('Default')" v-model="space.aiDefaultProvider"></model-select>
|
||||
|
||||
<v-number-input v-model="space.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
|
||||
<v-number-input v-model="space.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
|
||||
|
||||
</template>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,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 +96,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/`), {
|
||||
|
||||
@@ -25,7 +25,9 @@ export function useNavigation() {
|
||||
TANDOOR_PLUGINS.forEach(plugin => {
|
||||
plugin.navigationDrawer.forEach(navEntry => {
|
||||
let navEntryCopy = Object.assign({}, navEntry)
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
if ('title' in navEntryCopy) {
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
}
|
||||
navigation.push(navEntryCopy)
|
||||
})
|
||||
})
|
||||
@@ -44,7 +46,9 @@ export function useNavigation() {
|
||||
TANDOOR_PLUGINS.forEach(plugin => {
|
||||
plugin.bottomNavigation.forEach(navEntry => {
|
||||
let navEntryCopy = Object.assign({}, navEntry)
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
if ('title' in navEntryCopy) {
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
}
|
||||
navigation.push(navEntryCopy)
|
||||
})
|
||||
})
|
||||
@@ -80,7 +84,9 @@ export function useNavigation() {
|
||||
TANDOOR_PLUGINS.forEach(plugin => {
|
||||
plugin.userNavigation.forEach(navEntry => {
|
||||
let navEntryCopy = Object.assign({}, navEntry)
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
if ('title' in navEntryCopy) {
|
||||
navEntryCopy.title = t(navEntryCopy.title)
|
||||
}
|
||||
navigation.push(navEntryCopy)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
"AddToShopping": "",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -13,12 +15,22 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "",
|
||||
"Auto_Planner": "",
|
||||
"Automate": "",
|
||||
"Automation": "",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -39,6 +51,7 @@
|
||||
"Color": "",
|
||||
"Coming_Soon": "",
|
||||
"Completed": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -53,6 +66,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -61,11 +75,13 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "",
|
||||
"DeleteShoppingConfirm": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "",
|
||||
"Delete_Keyword": "",
|
||||
"Description": "",
|
||||
"Disable_Amount": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Edit": "",
|
||||
@@ -95,16 +111,20 @@
|
||||
"FoodOnHand": "",
|
||||
"Food_Alias": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "",
|
||||
"Hide_Recipes": "",
|
||||
"Hide_as_header": "",
|
||||
"Hierarchy": "",
|
||||
"Icon": "",
|
||||
"IgnoreAccents": "",
|
||||
"IgnoreAccentsHelp": "",
|
||||
@@ -140,10 +160,13 @@
|
||||
"Keywords": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Make_Header": "",
|
||||
"Make_Ingredient": "",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "",
|
||||
"Meal_Plan": "",
|
||||
"Meal_Plan_Days": "",
|
||||
@@ -151,10 +174,13 @@
|
||||
"Meal_Type_Required": "",
|
||||
"Meal_Types": "",
|
||||
"Merge": "",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "",
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -175,6 +201,7 @@
|
||||
"New_Unit": "",
|
||||
"Next_Day": "",
|
||||
"Next_Period": "",
|
||||
"No": "",
|
||||
"NoCategory": "",
|
||||
"NoUnit": "",
|
||||
"No_ID": "",
|
||||
@@ -208,6 +235,8 @@
|
||||
"Previous_Day": "",
|
||||
"Previous_Period": "",
|
||||
"Print": "",
|
||||
"Private": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Protected": "",
|
||||
"Proteins": "",
|
||||
"Quick actions": "",
|
||||
@@ -222,7 +251,9 @@
|
||||
"Recipes": "",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "",
|
||||
"Reset": "",
|
||||
"Reset_Search": "",
|
||||
@@ -241,6 +272,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Categories": "",
|
||||
"Shopping_Category": "",
|
||||
@@ -253,6 +285,7 @@
|
||||
"Single": "",
|
||||
"Size": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"Starting_Day": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -292,6 +325,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "",
|
||||
"View_Recipes": "",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"Warning": "",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -299,6 +333,7 @@
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Add": "Добави",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Добавете {food} към списъка си за пазаруване",
|
||||
"AddToShopping": "Добавяне към списъка за пазаруване",
|
||||
"Add_Servings_to_Shopping": "Добавете {servings} порции към Пазаруване",
|
||||
@@ -13,12 +15,22 @@
|
||||
"Added_by": "Добавено от",
|
||||
"Added_on": "Добавено",
|
||||
"Advanced": "Разширено",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "Приложение",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "Сигурен ли си?",
|
||||
"Auto_Planner": "Автоматичен плановик",
|
||||
"Automate": "Автоматизация",
|
||||
"Automation": "Автоматизация",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Книжен пазар",
|
||||
"Books": "Книги",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -39,6 +51,7 @@
|
||||
"Color": "Цвят",
|
||||
"Coming_Soon": "Очаквайте скоро",
|
||||
"Completed": "Завършено",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Копиране",
|
||||
"Copy_template_reference": "Копирайте препратка към шаблона",
|
||||
"CountMore": "...+{count} още",
|
||||
@@ -50,6 +63,7 @@
|
||||
"Create_New_Meal_Type": "Добавете нов тип хранене",
|
||||
"Create_New_Shopping Category": "Създайте нова категория за пазаруване",
|
||||
"Create_New_Unit": "Добавяне на нова единица",
|
||||
"Credits": "",
|
||||
"Current_Period": "Текущ период",
|
||||
"Custom Filter": "Персонализиран филтър",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -58,11 +72,13 @@
|
||||
"DelayUntil": "Забавяне до",
|
||||
"Delete": "Изтрий",
|
||||
"DeleteShoppingConfirm": "Сигурни ли сте, че искате да премахнете цялата {food} от списъка за пазаруване?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "Изтриване на храна",
|
||||
"Delete_Keyword": "Изтриване на ключова дума",
|
||||
"Description": "Описание",
|
||||
"Disable_Amount": "Деактивиране на сумата",
|
||||
"Documentation": "Документация",
|
||||
"DontChange": "",
|
||||
"Download": "Изтегляне",
|
||||
"Drag_Here_To_Delete": "Плъзнете тук, за да изтриете",
|
||||
"Edit": "Редактиране",
|
||||
@@ -92,16 +108,20 @@
|
||||
"FoodOnHand": "Имате {храна} под ръка.",
|
||||
"Food_Alias": "Псевдоним на храната",
|
||||
"Foods": "Храни",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Групирай по",
|
||||
"Hide_Food": "Скриване на храна",
|
||||
"Hide_Keyword": "Скриване на ключови думи",
|
||||
"Hide_Keywords": "Скриване на ключова дума",
|
||||
"Hide_Recipes": "Скриване на рецепти",
|
||||
"Hide_as_header": "Скриване като заглавка",
|
||||
"Hierarchy": "",
|
||||
"Icon": "Икона",
|
||||
"IgnoreAccents": "",
|
||||
"IgnoreAccentsHelp": "",
|
||||
@@ -135,10 +155,13 @@
|
||||
"Keywords": "Ключови думи",
|
||||
"Link": "Връзка",
|
||||
"Load_More": "Зареди още",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Дневник на Готвене",
|
||||
"Log_Recipe_Cooking": "Дневник на Рецепта за готвене",
|
||||
"Make_Header": "Направете заглавие",
|
||||
"Make_Ingredient": "Направете съставка",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Управление на Книги",
|
||||
"Meal_Plan": "План на хранене",
|
||||
"Meal_Plan_Days": "Бъдещи планове за хранене",
|
||||
@@ -146,9 +169,12 @@
|
||||
"Meal_Type_Required": "Изисква се вид хранене",
|
||||
"Meal_Types": "Видове хранене",
|
||||
"Merge": "Обединяване",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Обединяване на ключова дума",
|
||||
"MissingProperties": "",
|
||||
"Month": "Месец",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Премести",
|
||||
"MoveCategory": "Премести към: ",
|
||||
"Move_Down": "Премести надолу",
|
||||
@@ -168,6 +194,7 @@
|
||||
"New_Unit": "Нова единица",
|
||||
"Next_Day": "Следващия ден",
|
||||
"Next_Period": "Следващ период",
|
||||
"No": "",
|
||||
"NoCategory": "Няма избрана категория.",
|
||||
"NoUnit": "",
|
||||
"No_ID": "Идентификатора не е намерен, не може да се изтрие.",
|
||||
@@ -201,6 +228,8 @@
|
||||
"Previous_Day": "Предишен ден",
|
||||
"Previous_Period": "Предишен период",
|
||||
"Print": "Печат",
|
||||
"Private": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Protected": "Защитен",
|
||||
"Proteins": "Протеини (белтъчини)",
|
||||
"Quick actions": "Бързи действия",
|
||||
@@ -215,7 +244,9 @@
|
||||
"Recipes": "Рецепти",
|
||||
"Recipes_In_Import": "Рецепти във вашия файл за импортиране",
|
||||
"Recipes_per_page": "Рецепти на страница",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Премахнете {food} от списъка си за пазаруване",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Изтрийте хранителните стойности от рецептата",
|
||||
"Reset": "Нулиране",
|
||||
"Reset_Search": "Нулиране на търсенето",
|
||||
@@ -234,6 +265,7 @@
|
||||
"Selected": "Избрано",
|
||||
"Servings": "Порции",
|
||||
"Settings": "Настройки",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Споделяне",
|
||||
"Shopping_Categories": "Категории за пазаруване",
|
||||
"Shopping_Category": "Категория за пазаруване",
|
||||
@@ -246,6 +278,7 @@
|
||||
"Single": "Единичен",
|
||||
"Size": "Размер",
|
||||
"Sort_by_new": "Сортиране по ново",
|
||||
"Space": "",
|
||||
"Starting_Day": "Начален ден от седмицата",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -283,6 +316,7 @@
|
||||
"User": "потребител",
|
||||
"View": "Изглед",
|
||||
"View_Recipes": "Вижте рецепти",
|
||||
"Visibility": "",
|
||||
"Waiting": "Очакване",
|
||||
"Warning": "Внимание",
|
||||
"Warning_Delete_Supermarket_Category": "Изтриването на категория супермаркет ще изтрие и всички връзки с храни. Сигурен ли си?",
|
||||
@@ -290,6 +324,7 @@
|
||||
"Week": "Седмица",
|
||||
"Week_Numbers": "Номера на седмиците",
|
||||
"Year": "Година",
|
||||
"Yes": "",
|
||||
"add_keyword": "Добавяне на ключова дума",
|
||||
"additional_options": "Допълнителни настройки",
|
||||
"advanced": "Разширено",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Compte",
|
||||
"Add": "Afegir",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Afegeix {food} a la llista de la compra",
|
||||
"AddToShopping": "Afegir a la llista de la compra",
|
||||
"Add_Servings_to_Shopping": "Afegir {servings} racions a la compra",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "Afegit per",
|
||||
"Added_on": "Afegit el",
|
||||
"Advanced": "Avançat",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alineació",
|
||||
"Amount": "Quantitat",
|
||||
"App": "Aplicació",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Automatitzar",
|
||||
"Automation": "Automatizació",
|
||||
"Back": "Enrere",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Marcadors",
|
||||
"Books": "Llibres",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "Mostrar comentaris",
|
||||
"Completed": "Completat",
|
||||
"Conversion": "Conversió",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copiar",
|
||||
"Copy Link": "Copiar Enllaç",
|
||||
"Copy Token": "Copiar Token",
|
||||
@@ -69,6 +82,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 l’espai.",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "Endarrerir fins",
|
||||
"Delete": "Eliminar",
|
||||
"DeleteShoppingConfirm": "Segur que vols eliminar tot el/la {food} de la llista de la compra?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Eliminar tot",
|
||||
"Delete_Food": "Eliminar Aliment",
|
||||
"Delete_Keyword": "Esborreu paraula clau",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "Deshabiliteu quantitat",
|
||||
"Disabled": "Desactivat",
|
||||
"Documentation": "Documentació",
|
||||
"DontChange": "",
|
||||
"Download": "Descarregar",
|
||||
"Drag_Here_To_Delete": "Arrossega aquí per a eliminar",
|
||||
"Edit": "Editar",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "Àlies per l'aliment",
|
||||
"Food_Replace": "Aliment equivalent",
|
||||
"Foods": "Aliments",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Agrupat per",
|
||||
"Hide_Food": "Amagar Aliment",
|
||||
"Hide_Keyword": "Amaga les paraules clau",
|
||||
"Hide_Keywords": "Amagueu paraula clau",
|
||||
"Hide_Recipes": "Amagueu receptes",
|
||||
"Hide_as_header": "Amagueu com a títol",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Hora",
|
||||
"Hours": "Hores",
|
||||
"Icon": "Icona",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "Saber-me més",
|
||||
"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",
|
||||
"Make_Header": "Establiu capçalera",
|
||||
"Make_Ingredient": "Establiu ingredient",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Gestioneu els llibres",
|
||||
"Manage_Emails": "Administrar correus",
|
||||
"Meal_Plan": "Pla d'àpats",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "El tipus és obligatori",
|
||||
"Meal_Types": "Tipus de menjars",
|
||||
"Merge": "Unificar",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Fusioneu paraula clau",
|
||||
"Message": "Missatge",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mes",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Moure",
|
||||
"MoveCategory": "Moure a: ",
|
||||
"Move_Down": "Moveu avall",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "Nova unitat",
|
||||
"Next_Day": "Següent dia",
|
||||
"Next_Period": "Període següent",
|
||||
"No": "",
|
||||
"NoCategory": "No s'ha seleccionat categoria.",
|
||||
"NoMoreUndo": "No hi ha canvis per desar.",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "Dia Anterior",
|
||||
"Previous_Period": "Període anterior",
|
||||
"Print": "Imprimir",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Recepta privada",
|
||||
"Private_Recipe_Help": "Només tu i la gent amb qui l'has compartit podran veure aquesta recepta.",
|
||||
"Properties": "Propietats",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "Receptes",
|
||||
"Recipes_In_Import": "Receptes al fitxer d'importació",
|
||||
"Recipes_per_page": "Receptes per pàgina",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Elimina {food} de la llista de la compra",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Esborreu nutrició de la recepta",
|
||||
"Reset": "Restablir",
|
||||
"Reset_Search": "Reinicieu la cerca",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "Seleccionat",
|
||||
"Servings": "Racions",
|
||||
"Settings": "Opcions",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartir",
|
||||
"ShoppingBackgroundSyncWarning": "Error de la connexió, esperant per sincronitzar ...",
|
||||
"Shopping_Categories": "Categoria de compres",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "Mida",
|
||||
"Social_Authentication": "Identificació amb Xarxes Socials",
|
||||
"Sort_by_new": "Ordenar a partir del més nou",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "Vàlid fins",
|
||||
"View": "Mostrar",
|
||||
"View_Recipes": "Mostreu les receptes",
|
||||
"Visibility": "",
|
||||
"Waiting": "Esperant",
|
||||
"Warning": "Advertència",
|
||||
"Warning_Delete_Supermarket_Category": "Si suprimiu una categoria de supermercat, també se suprimiran totes les relacions alimentàries. N'estàs segur?",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "Números de la setmana",
|
||||
"Welcome": "Benvingut/da",
|
||||
"Year": "Any",
|
||||
"Yes": "",
|
||||
"add_keyword": "Afegir Paraula Clau",
|
||||
"additional_options": "Opcions addicionals",
|
||||
"advanced": "Avançat",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Účet",
|
||||
"Add": "Přidat",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Přidat {food} na váš nákupní seznam",
|
||||
"AddToShopping": "Přidat do nákupního seznamu",
|
||||
"Add_Servings_to_Shopping": "Přidat {servings} porce na nákupní seznam",
|
||||
@@ -15,6 +17,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",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Automatizovat",
|
||||
"Automation": "Automatizace",
|
||||
"Back": "Zpět",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Skript v záložce",
|
||||
"Books": "Kuchařky",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,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",
|
||||
@@ -68,6 +81,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.",
|
||||
@@ -89,6 +103,7 @@
|
||||
"DelayUntil": "Odložit do",
|
||||
"Delete": "Smazat",
|
||||
"DeleteShoppingConfirm": "Jste si jistí, že chcete odstranit všechno {food} z nákupního seznamu?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Smazat vše",
|
||||
"Delete_Food": "Smazat potravinu",
|
||||
"Delete_Keyword": "Smazat štítek",
|
||||
@@ -98,6 +113,7 @@
|
||||
"Disable_Amount": "Skrýt množství",
|
||||
"Disabled": "Deaktivované",
|
||||
"Documentation": "Dokumentace",
|
||||
"DontChange": "",
|
||||
"Download": "Stáhnout",
|
||||
"Drag_Here_To_Delete": "Přesunutím sem smazat",
|
||||
"Edit": "Upravit",
|
||||
@@ -135,16 +151,20 @@
|
||||
"Food_Alias": "Přezdívka potraviny",
|
||||
"Food_Replace": "Nahrazení v potravině",
|
||||
"Foods": "Potraviny",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Seskupit podle",
|
||||
"Hide_Food": "Skrýt potravinu",
|
||||
"Hide_Keyword": "Skrýt štítky",
|
||||
"Hide_Keywords": "Skrýt štítek",
|
||||
"Hide_Recipes": "Skrýt recept",
|
||||
"Hide_as_header": "Skryj jako nadpis",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Hodina",
|
||||
"Hours": "Hodiny",
|
||||
"Icon": "Ikona",
|
||||
@@ -187,11 +207,14 @@
|
||||
"Learn_More": "Zjistit víc",
|
||||
"Link": "Odkaz",
|
||||
"Load_More": "Načíst další",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zaznamenat vaření",
|
||||
"Log_Recipe_Cooking": "Záznam vaření receptu",
|
||||
"Logo": "Logo",
|
||||
"Make_Header": "Použij jako nadpis",
|
||||
"Make_Ingredient": "Použij jako ingredienci",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Spravovat kuchařky",
|
||||
"Manage_Emails": "Spravovat emaily",
|
||||
"Meal_Plan": "Jídelníček",
|
||||
@@ -200,10 +223,13 @@
|
||||
"Meal_Type_Required": "Druh jídla je povinný",
|
||||
"Meal_Types": "Druhy jídel",
|
||||
"Merge": "Spojit",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Sloučit štítek",
|
||||
"Message": "Zpráva",
|
||||
"MissingProperties": "",
|
||||
"Month": "Měsíc",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Přesunout",
|
||||
"MoveCategory": "Přesunout do: ",
|
||||
"Move_Down": "Dolů",
|
||||
@@ -230,6 +256,7 @@
|
||||
"New_Unit": "Nová jednotka",
|
||||
"Next_Day": "Následující den",
|
||||
"Next_Period": "Další období",
|
||||
"No": "",
|
||||
"NoCategory": "Není vybrána žádná kategorie.",
|
||||
"NoUnit": "",
|
||||
"No_ID": "ID nenalezeno, odstranění není možné.",
|
||||
@@ -269,6 +296,7 @@
|
||||
"Previous_Day": "Předchozí den",
|
||||
"Previous_Period": "Předchozí období",
|
||||
"Print": "Tisk",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Soukromý recept",
|
||||
"Private_Recipe_Help": "Recept můžete zobrazit pouze vy a lidé, se kterými jej sdílíte.",
|
||||
"Properties": "Nutriční vlastnosti",
|
||||
@@ -290,7 +318,9 @@
|
||||
"Recipes": "Recepty",
|
||||
"Recipes_In_Import": "Receptů v importním souboru",
|
||||
"Recipes_per_page": "Receptů na stránku",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Odstranit {food} z nákupního seznamu",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Smazat nutriční hodnoty",
|
||||
"Reset": "Resetovat",
|
||||
"Reset_Search": "Zrušit filtry vyhledávání",
|
||||
@@ -311,6 +341,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",
|
||||
@@ -327,6 +358,7 @@
|
||||
"Size": "Velikost",
|
||||
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
|
||||
"Sort_by_new": "Seřadit od nejnovějšího",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -382,6 +414,7 @@
|
||||
"Valid Until": "Platné do",
|
||||
"View": "Zobrazit",
|
||||
"View_Recipes": "Zobrazit recepty",
|
||||
"Visibility": "",
|
||||
"Waiting": "Čekající",
|
||||
"Warning": "Varování",
|
||||
"Warning_Delete_Supermarket_Category": "Vymazáním kategorie obchodu dojde k odstranění všech vazeb na potraviny. Jste si jistí?",
|
||||
@@ -390,6 +423,7 @@
|
||||
"Week_Numbers": "Číslo týdne",
|
||||
"Welcome": "Vítejte",
|
||||
"Year": "Rok",
|
||||
"Yes": "",
|
||||
"add_keyword": "Přidat štítek",
|
||||
"additional_options": "Rozšířené možnosti",
|
||||
"advanced": "Pokročilé",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Bruger",
|
||||
"Add": "Tilføj",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Tilføj {food} til indkøbsliste",
|
||||
"AddToShopping": "Tilføj til indkøbsliste",
|
||||
"Add_Servings_to_Shopping": "Tilføj {servings} serveringer til indkøb",
|
||||
@@ -15,6 +17,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",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Automatiser",
|
||||
"Automation": "Automatisering",
|
||||
"Back": "Tilbage",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Bogmærke",
|
||||
"Books": "Bøger",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "Vis kommentarer",
|
||||
"Completed": "Afsluttet",
|
||||
"Conversion": "Konversion",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopier",
|
||||
"Copy Link": "Kopier link",
|
||||
"Copy Token": "Kopier token",
|
||||
@@ -69,6 +82,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.",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "Udskyd indtil",
|
||||
"Delete": "Slet",
|
||||
"DeleteShoppingConfirm": "Er du sikker på at du vil fjerne {food} fra indkøbsliste?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Slet alle",
|
||||
"Delete_Food": "Slet mad",
|
||||
"Delete_Keyword": "Slet nøgleord",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "Deaktiver antal",
|
||||
"Disabled": "Slået fra",
|
||||
"Documentation": "Dokumentation",
|
||||
"DontChange": "",
|
||||
"Download": "Download",
|
||||
"Drag_Here_To_Delete": "Træk herhen for at slette",
|
||||
"Edit": "Rediger",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "Alternativt navn til mad",
|
||||
"Food_Replace": "Erstat ingrediens",
|
||||
"Foods": "Mad",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Grupper efter",
|
||||
"Hide_Food": "Skjul mad",
|
||||
"Hide_Keyword": "Skjul nøgleord",
|
||||
"Hide_Keywords": "Skjul nøgleord",
|
||||
"Hide_Recipes": "Skjul opskrifter",
|
||||
"Hide_as_header": "Skjul som rubrik",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Time",
|
||||
"Hours": "Timer",
|
||||
"Icon": "Ikon",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "Lær mere",
|
||||
"Link": "Link",
|
||||
"Load_More": "Indlæs mere",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Noter tilberedning",
|
||||
"Log_Recipe_Cooking": "Noter tilberedning af opskrift",
|
||||
"Logo": "Logo",
|
||||
"Make_Header": "Opret rubrik",
|
||||
"Make_Ingredient": "Opret ingredient",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Administrer bøger",
|
||||
"Manage_Emails": "Håndter Emails",
|
||||
"Meal_Plan": "Madplan",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "Måltidstype påkrævet",
|
||||
"Meal_Types": "Måltidstyper",
|
||||
"Merge": "Sammenflet",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Sammenflet nøgleord",
|
||||
"Message": "Besked",
|
||||
"MissingProperties": "",
|
||||
"Month": "Måned",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Flyt",
|
||||
"MoveCategory": "Flyt til: ",
|
||||
"Move_Down": "Flyt ned",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "Ny enhed",
|
||||
"Next_Day": "Næste dag",
|
||||
"Next_Period": "Næste periode",
|
||||
"No": "",
|
||||
"NoCategory": "Ingen kategori valgt.",
|
||||
"NoMoreUndo": "Ingen ændringer at fortryde.",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "Forgående dag",
|
||||
"Previous_Period": "Forgående periode",
|
||||
"Print": "Udskriv",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Privat opskrift",
|
||||
"Private_Recipe_Help": "Opskriften er kun synlig for dig, og dem som den er delt med.",
|
||||
"Properties": "Egenskaber",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "Opskrifter",
|
||||
"Recipes_In_Import": "Opskrifter i din importerede fil",
|
||||
"Recipes_per_page": "Opskrifter pr. side",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Fjern {food} fra indkøbsliste",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Fjern næringsindhold fra opskrift",
|
||||
"Reset": "Nulstil",
|
||||
"Reset_Search": "Nulstil søgning",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "Valgt",
|
||||
"Servings": "Serveringer",
|
||||
"Settings": "Indstillinger",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Del",
|
||||
"ShoppingBackgroundSyncWarning": "Dårligt netværk, afventer synkronisering ...",
|
||||
"Shopping_Categories": "Indkøbskategorier",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "Størrelse",
|
||||
"Social_Authentication": "Social authenticering",
|
||||
"Sort_by_new": "Sorter efter nylige",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "Gyldig indtil",
|
||||
"View": "Vis",
|
||||
"View_Recipes": "Vis opskrifter",
|
||||
"Visibility": "",
|
||||
"Waiting": "Vente",
|
||||
"Warning": "Advarsel",
|
||||
"Warning_Delete_Supermarket_Category": "At slette en supermarkedskategori vil også slette alle relationer til mad. Er du sikker?",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "Ugenumre",
|
||||
"Welcome": "Velkommen",
|
||||
"Year": "År",
|
||||
"Yes": "",
|
||||
"add_keyword": "Tilføj nøgleord",
|
||||
"additional_options": "Yderligere indstillinger",
|
||||
"advanced": "Avanceret",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"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",
|
||||
"API_Browser": "API Browser",
|
||||
"API_Documentation": "API Dokumentation",
|
||||
"AccessTokenHelp": "Zugriffsschlüssel für die REST Schnittstelle.",
|
||||
@@ -11,6 +13,7 @@
|
||||
"Activity": "Aktivität",
|
||||
"Add": "Hinzufügen",
|
||||
"AddAll": "Alle Hinzufügen",
|
||||
"AddChild": "Kind hinzufügen",
|
||||
"AddFilter": "Filter Hinzufügen",
|
||||
"AddFoodToShopping": "Fügen Sie {food} zur Einkaufsliste hinzu",
|
||||
"AddMany": "Mehrere Hinzufügen",
|
||||
@@ -27,6 +30,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",
|
||||
@@ -46,6 +55,10 @@
|
||||
"BaseUnit": "Basiseinheit",
|
||||
"BaseUnitHelp": "Optionale Standardeinheit zur automatischen Umrechnung",
|
||||
"Basics": "Grundlagen",
|
||||
"BatchDeleteConfirm": "Möchtest du alle angezeigten Objekte löschen? Dies kann nicht rückgängig gemacht werden!",
|
||||
"BatchDeleteHelp": "Wenn ein Objekt nicht gelöscht werden kann, wird es noch irgendwo verwendet. ",
|
||||
"BatchEdit": "Massenbearbeitung",
|
||||
"BatchEditUpdatingItemsCount": "Bearbeite {count} {type}",
|
||||
"Book": "Buch",
|
||||
"Bookmarklet": "Lesezeichen",
|
||||
"BookmarkletHelp1": "Schiebe den Knopf in deine Lesezeichenleiste",
|
||||
@@ -84,6 +97,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",
|
||||
@@ -106,6 +120,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",
|
||||
@@ -132,6 +147,7 @@
|
||||
"Delete": "Löschen",
|
||||
"DeleteConfirmQuestion": "Sind Sie sicher das Sie dieses Objekt löschen wollen?",
|
||||
"DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste entfernen?",
|
||||
"DeleteSomething": "{item} löschen",
|
||||
"Delete_All": "Alles löschen",
|
||||
"Delete_Food": "Lebensmittel löschen",
|
||||
"Delete_Keyword": "Schlagwort löschen",
|
||||
@@ -144,6 +160,7 @@
|
||||
"Disable_Amount": "Menge deaktivieren",
|
||||
"Disabled": "Deaktiviert",
|
||||
"Documentation": "Dokumentation",
|
||||
"DontChange": "Nicht ändern",
|
||||
"Down": "Runter",
|
||||
"Download": "Herunterladen",
|
||||
"DragToUpload": "Drag & Drop oder Klicken zum Auswählen",
|
||||
@@ -197,11 +214,14 @@
|
||||
"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.",
|
||||
"GroupBy": "Gruppieren nach",
|
||||
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
|
||||
"Headline": "Überschrift",
|
||||
@@ -212,6 +232,7 @@
|
||||
"Hide_Keywords": "Schlagwort verstecken",
|
||||
"Hide_Recipes": "Rezepte verstecken",
|
||||
"Hide_as_header": "Keine Überschrift",
|
||||
"Hierarchy": "Hierarchie",
|
||||
"History": "Verlauf",
|
||||
"HostedFreeVersion": "Sie verwenden die kostenlose Testversion von Tandoor",
|
||||
"Hour": "Stunde",
|
||||
@@ -268,13 +289,15 @@
|
||||
"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",
|
||||
"Logout": "Ausloggen",
|
||||
"Make_Header": "In Überschrift wandeln",
|
||||
"Make_Ingredient": "In Zutat umwandeln",
|
||||
"ManageSubscription": "Tarfi verwalten",
|
||||
"ManageSubscription": "Tarif verwalten",
|
||||
"Manage_Books": "Bücher verwalten",
|
||||
"Manage_Emails": "E-Mails verwalten",
|
||||
"MealPlanHelp": "Ein Speiseplan ist ein Eintrag im Kalender zur Planung von Mahlzeiten. Er muss entweder ein Rezept oder einen Titel erhalten und kann mit der Einkaufsliste verknüpft werden. ",
|
||||
@@ -286,16 +309,19 @@
|
||||
"Meal_Type_Required": "Mahlzeitentyp ist erforderlich",
|
||||
"Meal_Types": "Mahlzeiten",
|
||||
"Merge": "Zusammenführen",
|
||||
"MergeAutomateHelp": "Erstelle eine Automatisierung ,die auch zukünftig erstellte Objekte mit diesem Namen durch das gewählte Objekt ersetzt.",
|
||||
"MergeAutomateHelp": "Erstelle eine Automatisierung, die auch zukünftig erstellte Objekte mit diesem Namen durch das gewählte Objekt ersetzt.",
|
||||
"Merge_Keyword": "Schlagworte zusammenführen",
|
||||
"Message": "Nachricht",
|
||||
"Messages": "Nachrichten",
|
||||
"Miscellaneous": "Sonstige",
|
||||
"MissingConversion": "Fehlende Umrechnung",
|
||||
"MissingProperties": "Fehlende Eigenschaften",
|
||||
"Model": "Modell",
|
||||
"ModelSelectResultsHelp": "Für mehr Ergebnisse suchen",
|
||||
"Monday": "Montag",
|
||||
"Month": "Monat",
|
||||
"MonthlyCredits": "Monatliche Credits",
|
||||
"MonthlyCreditsUsed": "Monatliche Credits verwendet",
|
||||
"More": "Mehr",
|
||||
"Move": "Verschieben",
|
||||
"MoveCategory": "Verschieben nach: ",
|
||||
@@ -325,6 +351,7 @@
|
||||
"Next": "Weiter",
|
||||
"Next_Day": "Tag vor",
|
||||
"Next_Period": "nächster Zeitraum",
|
||||
"No": "Nein",
|
||||
"NoCategory": "Ohne Kategorie",
|
||||
"NoMoreUndo": "Rückgängig: Keine Änderungen",
|
||||
"NoUnit": "Keine Einheit",
|
||||
@@ -377,8 +404,9 @@
|
||||
"Previous_Day": "Tag zurück",
|
||||
"Previous_Period": "voriger Zeitraum",
|
||||
"Print": "Drucken",
|
||||
"Private": "Privat",
|
||||
"Private_Recipe": "Privates Rezept",
|
||||
"Private_Recipe_Help": "Dieses Rezept ist nur für dich und Personen mit denen du es geteilt hast sichtbar.",
|
||||
"Private_Recipe_Help": "Private Rezepte sind nur für dich und Personen mit denen du Sie geteilt hast sichtbar.",
|
||||
"Profile": "Profil",
|
||||
"Properties": "Eigenschaften",
|
||||
"PropertiesFoodHelp": "Eigenschaften können für Rezepte und Lebensmittel erfasst werden. Eigenschaften von Lebensmitteln werden automatisch entsprechend der im Rezept enthaltenen Menge berechnet.",
|
||||
@@ -411,7 +439,9 @@
|
||||
"Recipes_In_Import": "Rezepte in deiner importierten Datei",
|
||||
"Recipes_per_page": "Rezepte pro Seite",
|
||||
"Remove": "Entfernen",
|
||||
"RemoveAllType": "Alle {type} entfernen",
|
||||
"RemoveFoodFromShopping": "{food} von der Einkaufsliste löschen",
|
||||
"RemoveParent": "Eltern entfernen",
|
||||
"Remove_nutrition_recipe": "Nährwerte aus Rezept löschen",
|
||||
"Reset": "Zurücksetzen",
|
||||
"ResetHelp": "Hilfe Zurücksetzen",
|
||||
@@ -445,6 +475,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",
|
||||
@@ -473,6 +504,7 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -498,10 +530,12 @@
|
||||
"Storage": "Externer Speicher",
|
||||
"StorageHelp": "Externe Speicherorte an denen Rezepte als Dateien (Foto/PDF) abgelegt und mit Tandor syncronisiert werden können.",
|
||||
"StoragePasswordTokenHelp": "Das hinterlegte Passwort/Token kann nicht angezeigt werden. Es wird nur aktualisiert wenn etwas neues in das Feld eingegeben wird. ",
|
||||
"Structured": "Strukturiert",
|
||||
"SubstituteOnHand": "Du hast eine Alternative vorrätig.",
|
||||
"Substitutes": "Alternativen",
|
||||
"Success": "Erfolgreich",
|
||||
"SuccessClipboard": "Einkaufsliste wurde in die Zwischenablage kopiert",
|
||||
"Summary": "Zusammenfassung",
|
||||
"Sunday": "Sonntag",
|
||||
"Supermarket": "Supermarkt",
|
||||
"SupermarketCategoriesOnly": "Nur Supermarktkategorien",
|
||||
@@ -575,6 +609,7 @@
|
||||
"ViewLogHelp": "Verlauf angesehener Rezepte. ",
|
||||
"View_Recipes": "Rezepte Ansehen",
|
||||
"Viewed": "Angesehen",
|
||||
"Visibility": "Sichtbarkeit",
|
||||
"Waiting": "Wartezeit",
|
||||
"WaitingTime": "Wartezeit",
|
||||
"WarnPageLeave": "Deine Änderungen wurden noch nicht gespeichert und gehen verloren. Seite wirklich verlassen?",
|
||||
@@ -588,6 +623,7 @@
|
||||
"Welcome": "Willkommen",
|
||||
"WorkingTime": "Arbeitszeit",
|
||||
"Year": "Jahr",
|
||||
"Yes": "Ja",
|
||||
"YourSpaces": "Deine Spaces",
|
||||
"active": "aktiv",
|
||||
"add_keyword": "Stichwort hinzufügen",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Λογαριασμός",
|
||||
"Add": "Προσθήκη",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Προσθήκη του φαγητού {food} στη λίστα αγορών σας",
|
||||
"AddToShopping": "Προσθήκη στη λίστα αγορών",
|
||||
"Add_Servings_to_Shopping": "Προσθήκη {servings} μερίδων στις αγορές",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "Προστέθηκε από",
|
||||
"Added_on": "Προστέθηκε στις",
|
||||
"Advanced": "Για προχωρημένους",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Ευθυγράμμιση",
|
||||
"Amount": "Ποσότητα",
|
||||
"App": "Εφαρμογή",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Αυτοματοποίηση",
|
||||
"Automation": "Αυτοματισμός",
|
||||
"Back": "Πίσω",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Bookmarklet",
|
||||
"Books": "Βιβλία",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "Εμφάνιση σχολίων",
|
||||
"Completed": "Ολοκληρωμένο",
|
||||
"Conversion": "Μετατροπή",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Αντιγραφή",
|
||||
"Copy Link": "Αντιγραφή συνδέσμου",
|
||||
"Copy Token": "Αντιγραφή token",
|
||||
@@ -69,6 +82,7 @@
|
||||
"Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών",
|
||||
"Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης",
|
||||
"Created": "Δημιουργήθηκε",
|
||||
"Credits": "",
|
||||
"Current_Period": "Τρέχουσα περίοδος",
|
||||
"Custom Filter": "Προσαρμοσμένο φίλτρο",
|
||||
"CustomImageHelp": "Ανεβάστε μια εικόνα για να εμφανίζεται στην επισκόπηση χώρου",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "Καθυστέρηση μέχρι",
|
||||
"Delete": "Διαγραφή",
|
||||
"DeleteShoppingConfirm": "Θέλετε σίγουρα να αφαιρέσετε τα {food} από τη λίστα αγορών;",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Διαγραφή όλων",
|
||||
"Delete_Food": "Διαγραφή φαγητού",
|
||||
"Delete_Keyword": "Διαγραφή λέξης-κλειδί",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "Απενεργοποίηση ποσότητας",
|
||||
"Disabled": "Απενεροποιημένο",
|
||||
"Documentation": "Τεκμηρίωση",
|
||||
"DontChange": "",
|
||||
"Download": "Λήψη",
|
||||
"Drag_Here_To_Delete": "Σύρετε εδώ για διαγραφή",
|
||||
"Edit": "Τροποποίηση",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "Ψευδώνυμο φαγητού",
|
||||
"Food_Replace": "Αντικατάσταση Φαγητού",
|
||||
"Foods": "Φαγητά",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Ομαδοποίηση κατά",
|
||||
"Hide_Food": "Απόκρυψη φαγητού",
|
||||
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
|
||||
"Hide_Keywords": "Απόκρυψη λέξης-κλειδί",
|
||||
"Hide_Recipes": "Απόκρυψη συνταγών",
|
||||
"Hide_as_header": "Απόκρυψη ως κεφαλίδα",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Ώρα",
|
||||
"Hours": "Ώρες",
|
||||
"Icon": "Εικονίδιο",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "Μάθετε περισσότερα",
|
||||
"Link": "Σύνδεσμος",
|
||||
"Load_More": "Φόρτωση περισσότερων",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Καταγραφή μαγειρέματος",
|
||||
"Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής",
|
||||
"Logo": "Λογότυπο",
|
||||
"Make_Header": "Δημιουργία κεφαλίδας",
|
||||
"Make_Ingredient": "Δημιουργία υλικού",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Διαχείριση βιβλίων",
|
||||
"Manage_Emails": "Διαχείριση email",
|
||||
"Meal_Plan": "Πρόγραμμα γευμάτων",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "Το είδος του γεύματος είναι απαραίτητο",
|
||||
"Meal_Types": "Είδη γευμάτων",
|
||||
"Merge": "Συγχώνευση",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Συγχώνευση λέξης-κλειδί",
|
||||
"Message": "Μήνυμα",
|
||||
"MissingProperties": "",
|
||||
"Month": "Μήνας",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Μετακίνηση",
|
||||
"MoveCategory": "Μετακίνηση σε: ",
|
||||
"Move_Down": "Μετακίνηση κάτω",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "Νέα μονάδα μέτρησης",
|
||||
"Next_Day": "Επόμενη μέρα",
|
||||
"Next_Period": "Επόμενη περίοδος",
|
||||
"No": "",
|
||||
"NoCategory": "Δεν έχει επιλεγεί κατηγορία.",
|
||||
"NoMoreUndo": "Δεν υπάρχουν αλλαγές για ανέρεση.",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "Προηγούμενη μέρα",
|
||||
"Previous_Period": "Προηγούμενη περίοδος",
|
||||
"Print": "Εκτύπωση",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Ιδιωτική συνταγή",
|
||||
"Private_Recipe_Help": "Η συνταγή είναι ορατή μόνο σε εσάς και στα άτομα με τα οποία την μοιράζεστε.",
|
||||
"Properties": "Ιδιότητες",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "Συνταγές",
|
||||
"Recipes_In_Import": "Συνταγές στο αρχείο εισαγωγής",
|
||||
"Recipes_per_page": "Συνταγές ανά σελίδα",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Αφαίρεση του φαγητού {food} από τη λίστα αγορών σας",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Αφαίρεση διατροφικής αξίας από τη συνταγή",
|
||||
"Reset": "Επαναφορά",
|
||||
"Reset_Search": "Επαναφορά αναζήτησης",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "Επιλεγμένο",
|
||||
"Servings": "Μερίδες",
|
||||
"Settings": "Ρυθμίσεις",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Κοινοποίηση",
|
||||
"ShoppingBackgroundSyncWarning": "Κακό δίκτυο, αναμονή συγχρονισμού...",
|
||||
"Shopping_Categories": "Κατηγορίες αγορών",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "Μέγεθος",
|
||||
"Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων",
|
||||
"Sort_by_new": "Ταξινόμηση κατά νέο",
|
||||
"Space": "",
|
||||
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
|
||||
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
|
||||
"StartDate": "Ημερομηνία Έναρξης",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "Ισχύει έως",
|
||||
"View": "Προβολή",
|
||||
"View_Recipes": "Προβολή συνταγών",
|
||||
"Visibility": "",
|
||||
"Waiting": "Αναμονή",
|
||||
"Warning": "Προειδοποίηση",
|
||||
"Warning_Delete_Supermarket_Category": "Η διαγραφή μιας κατηγορίας supermarket θα διαγράψει και όλες τις σχέσεις της με φαγητά. Είστε σίγουροι;",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "Αριθμοί εδομάδων",
|
||||
"Welcome": "Καλώς ήρθατε",
|
||||
"Year": "Έτος",
|
||||
"Yes": "",
|
||||
"add_keyword": "Προσθήκη λέξης-κλειδί",
|
||||
"additional_options": "Επιπλέον επιλογές",
|
||||
"advanced": "Για προχωρημένους",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"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.",
|
||||
@@ -11,6 +13,7 @@
|
||||
"Activity": "Activity",
|
||||
"Add": "Add",
|
||||
"AddAll": "Add all",
|
||||
"AddChild": "Add child",
|
||||
"AddFilter": "Add Filter",
|
||||
"AddFoodToShopping": "Add {food} to your shopping list",
|
||||
"AddMany": "Add Many",
|
||||
@@ -25,6 +28,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",
|
||||
@@ -44,6 +53,10 @@
|
||||
"BaseUnit": "Base Unit",
|
||||
"BaseUnitHelp": "Standard unit for automatic unit conversion",
|
||||
"Basics": "Basics",
|
||||
"BatchDeleteConfirm": "Do you want to delete all shown items? This cannot be undone!",
|
||||
"BatchDeleteHelp": "If an item cannot be deleted it is used somewhere. ",
|
||||
"BatchEdit": "Batch Edit",
|
||||
"BatchEditUpdatingItemsCount": "Editing {count} {type}",
|
||||
"Book": "Book",
|
||||
"Bookmarklet": "Bookmarklet",
|
||||
"BookmarkletHelp1": "Drag the following button to your bookmarks bar",
|
||||
@@ -82,6 +95,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",
|
||||
@@ -104,6 +118,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",
|
||||
@@ -130,6 +145,7 @@
|
||||
"Delete": "Delete",
|
||||
"DeleteConfirmQuestion": "Are you sure you want to delete this object?",
|
||||
"DeleteShoppingConfirm": "Are you sure that you want to remove all {food} from the shopping list?",
|
||||
"DeleteSomething": "Delete {item}",
|
||||
"Delete_All": "Delete all",
|
||||
"Delete_Food": "Delete Food",
|
||||
"Delete_Keyword": "Delete Keyword",
|
||||
@@ -142,6 +158,7 @@
|
||||
"Disable_Amount": "Disable Amount",
|
||||
"Disabled": "Disabled",
|
||||
"Documentation": "Documentation",
|
||||
"DontChange": "Don't change",
|
||||
"Down": "Down",
|
||||
"Download": "Download",
|
||||
"DragToUpload": "Drag and Drop or click to select",
|
||||
@@ -195,11 +212,14 @@
|
||||
"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. ",
|
||||
"GroupBy": "Group By",
|
||||
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
|
||||
"Headline": "Headline",
|
||||
@@ -210,6 +230,7 @@
|
||||
"Hide_Keywords": "Hide Keyword",
|
||||
"Hide_Recipes": "Hide Recipes",
|
||||
"Hide_as_header": "Hide as header",
|
||||
"Hierarchy": "Hierarchy",
|
||||
"History": "History",
|
||||
"HostedFreeVersion": "You are using the free version of Tandoor",
|
||||
"Hour": "Hour",
|
||||
@@ -266,6 +287,8 @@
|
||||
"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",
|
||||
@@ -291,9 +314,12 @@
|
||||
"Miscellaneous": "Miscellaneous",
|
||||
"MissingConversion": "Missing Conversion",
|
||||
"MissingProperties": "Missing Properties",
|
||||
"Model": "Model",
|
||||
"ModelSelectResultsHelp": "Search for more results",
|
||||
"Monday": "Monday",
|
||||
"Month": "Month",
|
||||
"MonthlyCredits": "Monthly Credits",
|
||||
"MonthlyCreditsUsed": "Monthly credits used",
|
||||
"More": "More",
|
||||
"Move": "Move",
|
||||
"MoveCategory": "Move To: ",
|
||||
@@ -323,6 +349,7 @@
|
||||
"Next": "Next",
|
||||
"Next_Day": "Next Day",
|
||||
"Next_Period": "Next Period",
|
||||
"No": "No",
|
||||
"NoCategory": "No Category",
|
||||
"NoMoreUndo": "No changes to be undone.",
|
||||
"NoUnit": "No Unit",
|
||||
@@ -375,8 +402,9 @@
|
||||
"Previous_Day": "Previous Day",
|
||||
"Previous_Period": "Previous Period",
|
||||
"Print": "Print",
|
||||
"Private": "Private",
|
||||
"Private_Recipe": "Private Recipe",
|
||||
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
|
||||
"Private_Recipe_Help": "Private recipes are only shown to you and people they are shared with.",
|
||||
"Profile": "Profile",
|
||||
"Properties": "Properties",
|
||||
"PropertiesFoodHelp": "Properties can be added to Recipes and Foods. Properties on Foods are automatically calculated based on their amount in the recipe.",
|
||||
@@ -409,7 +437,9 @@
|
||||
"Recipes_In_Import": "Recipes in your import file",
|
||||
"Recipes_per_page": "Recipes per Page",
|
||||
"Remove": "Remove",
|
||||
"RemoveAllType": "Remove all {type}",
|
||||
"RemoveFoodFromShopping": "Remove {food} from your shopping list",
|
||||
"RemoveParent": "Remove parent",
|
||||
"Remove_nutrition_recipe": "Delete nutrition from recipe",
|
||||
"Reset": "Reset",
|
||||
"ResetHelp": "Reset Help",
|
||||
@@ -443,6 +473,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",
|
||||
@@ -471,6 +502,7 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -496,10 +528,12 @@
|
||||
"Storage": "External Storage",
|
||||
"StorageHelp": "External storage locations where recipe files (image/pdf) can be stored and synced with Tandoor.",
|
||||
"StoragePasswordTokenHelp": "The stored password/token will never be displayed. It is only changed if something new is entered into the field. ",
|
||||
"Structured": "Structured",
|
||||
"SubstituteOnHand": "You have a substitute on hand.",
|
||||
"Substitutes": "Substitutes",
|
||||
"Success": "Success",
|
||||
"SuccessClipboard": "Shopping list copied to clipboard",
|
||||
"Summary": "Summary",
|
||||
"Sunday": "Sunday",
|
||||
"Supermarket": "Supermarket",
|
||||
"SupermarketCategoriesOnly": "Supermarket Categories Only",
|
||||
@@ -573,6 +607,7 @@
|
||||
"ViewLogHelp": "History of viewed recipes. ",
|
||||
"View_Recipes": "View Recipes",
|
||||
"Viewed": "Viewed",
|
||||
"Visibility": "Visibility",
|
||||
"Waiting": "Waiting",
|
||||
"WaitingTime": "Waiting Time",
|
||||
"WarnPageLeave": "There are unsaved changes that will get lost. Leave page anyway?",
|
||||
@@ -586,6 +621,7 @@
|
||||
"Welcome": "Welcome",
|
||||
"WorkingTime": "Working time",
|
||||
"Year": "Year",
|
||||
"Yes": "Yes",
|
||||
"YourSpaces": "Your Spaces",
|
||||
"active": "active",
|
||||
"add_keyword": "Add Keyword",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Usar IA para importar imágenes de recetas.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Actividad",
|
||||
"Add": "Añadir",
|
||||
"AddAll": "Agregar todo",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Agregar filtro",
|
||||
"AddFoodToShopping": "Añadir {food} a la lista de compras",
|
||||
"AddMany": "Agregar muchos",
|
||||
@@ -25,6 +27,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",
|
||||
@@ -43,6 +51,10 @@
|
||||
"BaseUnit": "Unidad base",
|
||||
"BaseUnitHelp": "Unidad estándar para la conversión automática de unidades",
|
||||
"Basics": "Básicos",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Libro",
|
||||
"Bookmarklet": "Marcador ejecutable",
|
||||
"BookmarkletHelp1": "Arrastra el siguiente botón a tu barra de marcadores",
|
||||
@@ -80,6 +92,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",
|
||||
@@ -102,6 +115,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",
|
||||
@@ -128,6 +142,7 @@
|
||||
"Delete": "Eliminar",
|
||||
"DeleteConfirmQuestion": "¿Confirmas querer eliminar este objeto?",
|
||||
"DeleteShoppingConfirm": "¿Confirmas que quieres eliminar todo el {food} de la lista de compras?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Eliminar todo",
|
||||
"Delete_Food": "Eliminar alimento",
|
||||
"Delete_Keyword": "Eliminar palabra clave",
|
||||
@@ -140,6 +155,7 @@
|
||||
"Disable_Amount": "Deshabilitar cantidad",
|
||||
"Disabled": "Deshabilitado",
|
||||
"Documentation": "Documentación",
|
||||
"DontChange": "",
|
||||
"Down": "Abajo",
|
||||
"Download": "Descargar",
|
||||
"DragToUpload": "Arrastra y suelta o haz clic para seleccionar",
|
||||
@@ -193,7 +209,10 @@
|
||||
"Food_Replace": "Sustituir Alimento",
|
||||
"Foods": "Alimentos",
|
||||
"Friday": "Viernes",
|
||||
"FromBalance": "",
|
||||
"GettingStarted": "Primeros pasos",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Agrupar por",
|
||||
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
|
||||
"Headline": "Encabezado",
|
||||
@@ -204,6 +223,7 @@
|
||||
"Hide_Keywords": "Esconder palabra clave",
|
||||
"Hide_Recipes": "Esconder recetas",
|
||||
"Hide_as_header": "Esconder como titulo",
|
||||
"Hierarchy": "",
|
||||
"History": "Historial",
|
||||
"HostedFreeVersion": "Estás usando la versión gratuita de Tandoor",
|
||||
"Hour": "Hora",
|
||||
@@ -258,6 +278,8 @@
|
||||
"Link": "Enlace",
|
||||
"Load": "Cargar",
|
||||
"Load_More": "Cargar más",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registrar cocinada",
|
||||
"Log_Recipe_Cooking": "Registro de recetas",
|
||||
"Logo": "Logotipo",
|
||||
@@ -286,6 +308,8 @@
|
||||
"ModelSelectResultsHelp": "Buscar más resultados",
|
||||
"Monday": "Lunes",
|
||||
"Month": "Mes",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Más",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover a: ",
|
||||
@@ -315,6 +339,7 @@
|
||||
"Next": "Siguiente",
|
||||
"Next_Day": "Siguiente Día",
|
||||
"Next_Period": "Siguiente Período",
|
||||
"No": "",
|
||||
"NoCategory": "No se ha seleccionado categoría.",
|
||||
"NoMoreUndo": "No hay cambios que deshacer.",
|
||||
"NoUnit": "",
|
||||
@@ -364,6 +389,7 @@
|
||||
"Previous_Day": "Día Anterior",
|
||||
"Previous_Period": "Período Anterior",
|
||||
"Print": "Imprimir",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Receta Privada",
|
||||
"Private_Recipe_Help": "La receta solo podrás verla tu y la gente con la que esta compartida.",
|
||||
"Profile": "Perfil",
|
||||
@@ -398,7 +424,9 @@
|
||||
"Recipes_In_Import": "Recetas en tu fichero de importación",
|
||||
"Recipes_per_page": "Recetas por página",
|
||||
"Remove": "Remover",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Eliminar {food} de la lista de la compra",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Borrar nutrición de la canasta",
|
||||
"Reset": "Restablecer",
|
||||
"ResetHelp": "Reiniciar ayuda",
|
||||
@@ -429,6 +457,7 @@
|
||||
"Servings": "Raciones",
|
||||
"ServingsText": "Texto de la porción",
|
||||
"Settings": "Opciones",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartir",
|
||||
"ShopLater": "Comprar después",
|
||||
"ShopNow": "Comprar ahora",
|
||||
@@ -455,6 +484,7 @@
|
||||
"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": "",
|
||||
"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.",
|
||||
@@ -553,6 +583,7 @@
|
||||
"ViewLogHelp": "Historial de recetas visualizadas. ",
|
||||
"View_Recipes": "Mostrar recetas",
|
||||
"Viewed": "Visualizada",
|
||||
"Visibility": "",
|
||||
"Waiting": "Esperando",
|
||||
"WaitingTime": "Tiempo de espera",
|
||||
"WarnPageLeave": "Hay cambios sin guardar que se perderán. ¿Salir de la página de todos modos?",
|
||||
@@ -566,6 +597,7 @@
|
||||
"Welcome": "Bienvenido/a",
|
||||
"WorkingTime": "Tiempo de trabajo",
|
||||
"Year": "Año",
|
||||
"Yes": "",
|
||||
"YourSpaces": "Tus espacios",
|
||||
"active": "activo",
|
||||
"add_keyword": "Añadir Palabra Clave",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Tili",
|
||||
"Add": "Lisää",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Lisää {food} ostoslistaan",
|
||||
"AddToShopping": "Lisää ostoslistalle",
|
||||
"Add_Servings_to_Shopping": "Lisää {servings} Annoksia Ostoksiin",
|
||||
@@ -16,6 +18,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",
|
||||
@@ -27,6 +35,10 @@
|
||||
"Automate": "Automatisoi",
|
||||
"Automation": "Automaatio",
|
||||
"Back": "Takaisin",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Kirjamerkki",
|
||||
"Books": "Kirjat",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -50,6 +62,7 @@
|
||||
"Comments_setting": "Näytä Kommentit",
|
||||
"Completed": "Valmis",
|
||||
"Conversion": "Muuntaminen",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopioi",
|
||||
"Copy Link": "Kopioi Linkki",
|
||||
"Copy Token": "Kopioi Token",
|
||||
@@ -66,6 +79,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.",
|
||||
@@ -87,6 +101,7 @@
|
||||
"DelayUntil": "Viive asti",
|
||||
"Delete": "Poista",
|
||||
"DeleteShoppingConfirm": "Oletko varma, että haluat poistaa kaikki {food} ostoslistalta?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Poista kaikki",
|
||||
"Delete_Food": "Poista ruoka",
|
||||
"Delete_Keyword": "Poista avainsana",
|
||||
@@ -96,6 +111,7 @@
|
||||
"Disable_Amount": "Poista Määrä käytöstä",
|
||||
"Disabled": "Ei käytössä",
|
||||
"Documentation": "Dokumentaatio",
|
||||
"DontChange": "",
|
||||
"Download": "Lataa",
|
||||
"Drag_Here_To_Delete": "Vedä tänne poistaaksesi",
|
||||
"Edit": "Muokkaa",
|
||||
@@ -133,16 +149,20 @@
|
||||
"Food_Alias": "Ruoan nimimerkki",
|
||||
"Food_Replace": "Korvaa Ruoka",
|
||||
"Foods": "Ruuat",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Ryhmittely peruste",
|
||||
"Hide_Food": "Piilota Ruoka",
|
||||
"Hide_Keyword": "Piilota avainsana",
|
||||
"Hide_Keywords": "Piilota Avainsana",
|
||||
"Hide_Recipes": "Piilota Reseptit",
|
||||
"Hide_as_header": "Piilota otsikko",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Tunti",
|
||||
"Hours": "Tuntia",
|
||||
"Icon": "Kuvake",
|
||||
@@ -183,11 +203,14 @@
|
||||
"Learn_More": "Lisätietoja",
|
||||
"Link": "Linkki",
|
||||
"Load_More": "Lataa Lisää",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Kirjaa kokkaus",
|
||||
"Log_Recipe_Cooking": "Kirjaa Reseptin valmistus",
|
||||
"Logo": "Logo",
|
||||
"Make_Header": "Valmista Otsikko",
|
||||
"Make_Ingredient": "Valmista Ainesosa",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Hallinnoi kirjoja",
|
||||
"Manage_Emails": "Hallinnoi sähköposteja",
|
||||
"Meal_Plan": "Ateriasuunnitelma",
|
||||
@@ -196,10 +219,13 @@
|
||||
"Meal_Type_Required": "Ateriatyyppi pakollinen",
|
||||
"Meal_Types": "Ateriatyypit",
|
||||
"Merge": "Yhdistä",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Yhdistä Avainsana",
|
||||
"Message": "Viesti",
|
||||
"MissingProperties": "",
|
||||
"Month": "Kuukausi",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Siirry",
|
||||
"MoveCategory": "Siirrä paikkaan: ",
|
||||
"Move_Down": "Siirry alas",
|
||||
@@ -221,6 +247,7 @@
|
||||
"New_Unit": "Uusi Yksikkö",
|
||||
"Next_Day": "Seuraava Päivä",
|
||||
"Next_Period": "Seuraava Jakso",
|
||||
"No": "",
|
||||
"NoCategory": "Luokkaa ei ole valittu.",
|
||||
"NoMoreUndo": "Ei peruttavia muutoksia.",
|
||||
"NoUnit": "",
|
||||
@@ -261,6 +288,7 @@
|
||||
"Previous_Day": "Edellinen Päivä",
|
||||
"Previous_Period": "Edellinen Jakso",
|
||||
"Print": "Tulosta",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Yksityinen Resepti",
|
||||
"Private_Recipe_Help": "Resepti näytetään vain sinulle ja ihmisille, joiden kanssa se jaetaan.",
|
||||
"Properties": "Ominaisuudet",
|
||||
@@ -282,7 +310,9 @@
|
||||
"Recipes": "Reseptit",
|
||||
"Recipes_In_Import": "Reseptit tuonti tiedostossasi",
|
||||
"Recipes_per_page": "Reseptejä sivulla",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Poista {food} ostoslistalta",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Poista ravintoaine reseptistä",
|
||||
"Reset": "Nollaa",
|
||||
"Reset_Search": "Nollaa haku",
|
||||
@@ -303,6 +333,7 @@
|
||||
"Selected": "Valittu",
|
||||
"Servings": "Annokset",
|
||||
"Settings": "Asetukset",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Jaa",
|
||||
"ShoppingBackgroundSyncWarning": "Huono verkkoyhteys, odotetaan synkronointia ...",
|
||||
"Shopping_Categories": "Ostoskategoriat",
|
||||
@@ -320,6 +351,7 @@
|
||||
"Size": "Koko",
|
||||
"Social_Authentication": "Sosiaalinen Todennus",
|
||||
"Sort_by_new": "Lajittele uusien mukaan",
|
||||
"Space": "",
|
||||
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
|
||||
"StartDate": "Aloituspäivä",
|
||||
"Starting_Day": "Viikon aloituspäivä",
|
||||
@@ -371,6 +403,7 @@
|
||||
"Valid Until": "Voimassa Asti",
|
||||
"View": "Katso",
|
||||
"View_Recipes": "Näytä Reseptit",
|
||||
"Visibility": "",
|
||||
"Waiting": "Odottaa",
|
||||
"Warning": "Varoitus",
|
||||
"Website": "Verkkosivusto",
|
||||
@@ -378,6 +411,7 @@
|
||||
"Week_Numbers": "Viikkonumerot",
|
||||
"Welcome": "Tervetuloa",
|
||||
"Year": "Vuosi",
|
||||
"Yes": "",
|
||||
"add_keyword": "Lisää Avainsana",
|
||||
"additional_options": "Lisäasetukset",
|
||||
"advanced": "Edistynyt",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Utiliser l'IA pour importer des images de recettes.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Activité",
|
||||
"Add": "Ajouter",
|
||||
"AddAll": "Tout ajouter",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Ajouter un filtre",
|
||||
"AddFoodToShopping": "Ajouter l’aliment {food} à votre liste de courses",
|
||||
"AddMany": "Ajouter plusieurs",
|
||||
@@ -26,6 +28,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é",
|
||||
@@ -45,6 +53,10 @@
|
||||
"BaseUnit": "Unité de base",
|
||||
"BaseUnitHelp": "Unité standard pour la conversion automatique des unités",
|
||||
"Basics": "Les bases",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Livre",
|
||||
"Bookmarklet": "Signet",
|
||||
"BookmarkletHelp1": "Faites glisser le bouton suivant dans votre barre de signets",
|
||||
@@ -83,6 +95,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",
|
||||
@@ -105,6 +118,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é",
|
||||
@@ -131,6 +145,7 @@
|
||||
"Delete": "Supprimer",
|
||||
"DeleteConfirmQuestion": "Voulez-vous vraiment supprimer cet objet ?",
|
||||
"DeleteShoppingConfirm": "Êtes-vous sûr(e) de vouloir supprimer tous les aliments {food} de votre liste de courses ?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Supprimer tout",
|
||||
"Delete_Food": "Supprimer l’aliment",
|
||||
"Delete_Keyword": "Supprimer le mot-clé",
|
||||
@@ -143,6 +158,7 @@
|
||||
"Disable_Amount": "Désactiver la quantité",
|
||||
"Disabled": "Désactivé",
|
||||
"Documentation": "Documentation",
|
||||
"DontChange": "",
|
||||
"Down": "Bas",
|
||||
"Download": "Télécharger",
|
||||
"DragToUpload": "Déplacer ou cliquer pour sélectionner",
|
||||
@@ -196,11 +212,14 @@
|
||||
"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": "",
|
||||
"GroupBy": "Grouper par",
|
||||
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
|
||||
"Headline": "En-tête",
|
||||
@@ -211,6 +230,7 @@
|
||||
"Hide_Keywords": "Cacher le mot-clé",
|
||||
"Hide_Recipes": "Cacher les recettes",
|
||||
"Hide_as_header": "Cacher comme en-tête",
|
||||
"Hierarchy": "",
|
||||
"History": "Historique",
|
||||
"HostedFreeVersion": "Vous utilisez la version gratuite de Tandoor",
|
||||
"Hour": "Heure",
|
||||
@@ -267,6 +287,8 @@
|
||||
"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",
|
||||
@@ -293,6 +315,8 @@
|
||||
"ModelSelectResultsHelp": "Chercher plus de résultats",
|
||||
"Monday": "Lundi",
|
||||
"Month": "Mois",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Plus",
|
||||
"Move": "Déplacer",
|
||||
"MoveCategory": "Déplacer vers : ",
|
||||
@@ -322,6 +346,7 @@
|
||||
"Next": "Suivant",
|
||||
"Next_Day": "Prochain jour",
|
||||
"Next_Period": "Prochaine période",
|
||||
"No": "",
|
||||
"NoCategory": "Pas de catégorie sélectionnée.",
|
||||
"NoMoreUndo": "Aucun changement à annuler.",
|
||||
"NoUnit": "Pas d'unité",
|
||||
@@ -374,6 +399,7 @@
|
||||
"Previous_Day": "Jour précédent",
|
||||
"Previous_Period": "Période précédente",
|
||||
"Print": "Imprimer",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Recette privée",
|
||||
"Private_Recipe_Help": "La recette est uniquement visible par vous et les gens avec qui elle est partagée.",
|
||||
"Profile": "Profile",
|
||||
@@ -408,7 +434,9 @@
|
||||
"Recipes_In_Import": "Recettes dans votre fichier d’importation",
|
||||
"Recipes_per_page": "Nombre de recettes par page",
|
||||
"Remove": "Enlever",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Supprimer l’aliment {food} de votre liste de courses",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Supprimer les valeurs nutritionelles de la recette",
|
||||
"Reset": "Réinitialiser",
|
||||
"ResetHelp": "Aide à la réinitialisation",
|
||||
@@ -442,6 +470,7 @@
|
||||
"Servings": "Portions",
|
||||
"ServingsText": "Texte des portions",
|
||||
"Settings": "Paramètres",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Partager",
|
||||
"ShopLater": "Acheter plus tard",
|
||||
"ShopNow": "Acheter maintenant",
|
||||
@@ -470,6 +499,7 @@
|
||||
"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": "",
|
||||
"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.",
|
||||
@@ -572,6 +602,7 @@
|
||||
"ViewLogHelp": "Historique des recettes consultées. ",
|
||||
"View_Recipes": "Voir les recettes",
|
||||
"Viewed": "Vue",
|
||||
"Visibility": "",
|
||||
"Waiting": "Attente",
|
||||
"WaitingTime": "Temps d'attente",
|
||||
"WarnPageLeave": "Certaines modifications non enregistrées seront perdues. Voulez-vous quand même quitter la page ?",
|
||||
@@ -585,6 +616,7 @@
|
||||
"Welcome": "Bienvenue",
|
||||
"WorkingTime": "Temps de préparation",
|
||||
"Year": "Année",
|
||||
"Yes": "",
|
||||
"YourSpaces": "Vos groupes",
|
||||
"active": "actif",
|
||||
"add_keyword": "Ajouter un Mot Clé",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "חשבון",
|
||||
"Add": "הוספה",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "הוסף {מזון} לרשימת הקניות",
|
||||
"AddToShopping": "הוסף לרשימת קניות",
|
||||
"Add_Servings_to_Shopping": "הוסף{מנה}מנות לקנייה",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "נוסף ע\"י",
|
||||
"Added_on": "נוסף ב",
|
||||
"Advanced": "מתקדם",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "יישור",
|
||||
"Amount": "כמות",
|
||||
"App": "אפליקציה",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "אוטומט",
|
||||
"Automation": "אוטומטציה",
|
||||
"Back": "חזור",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "סימניה",
|
||||
"Books": "ספרים",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "הצג תגובות",
|
||||
"Completed": "הושלם",
|
||||
"Conversion": "עברית",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "העתקה",
|
||||
"Copy Link": "העתק קישור",
|
||||
"Copy Token": "העתק טוקן",
|
||||
@@ -69,6 +82,7 @@
|
||||
"Create_New_Shopping_Category": "הוסף קטגוריות קניות חדשה",
|
||||
"Create_New_Unit": "הוסף יחידה",
|
||||
"Created": "נוצר",
|
||||
"Credits": "",
|
||||
"Current_Period": "תקופה נוכחית",
|
||||
"Custom Filter": "פילטר מותאם",
|
||||
"CustomImageHelp": "העלאת תמונה שתראה באזור הסקירה.",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "השהה עד",
|
||||
"Delete": "מחק",
|
||||
"DeleteShoppingConfirm": "האם אתה בטוח שברצונך להסיר את כל ה{מזון} מרשימת הקניות ?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "מחק הכל",
|
||||
"Delete_Food": "מחק אוכל",
|
||||
"Delete_Keyword": "מחר מילת מפתח",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "אל תאפשר כמות",
|
||||
"Disabled": "מושבת",
|
||||
"Documentation": "תיעוד",
|
||||
"DontChange": "",
|
||||
"Download": "הורדה",
|
||||
"Drag_Here_To_Delete": "משוך לכאן למחיקה",
|
||||
"Edit": "ערוך",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "שם כינוי לאוכל",
|
||||
"Food_Replace": "החלף אוכל",
|
||||
"Foods": "מאכלים",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "אסוף לפי",
|
||||
"Hide_Food": "הסתר אוכל",
|
||||
"Hide_Keyword": "הסתר מילות מפתח",
|
||||
"Hide_Keywords": "הסתרת מילת מפתח",
|
||||
"Hide_Recipes": "הסתרת מתכונים",
|
||||
"Hide_as_header": "הסתר בתור כותרת",
|
||||
"Hierarchy": "",
|
||||
"Hour": "שעה",
|
||||
"Hours": "שעות",
|
||||
"Icon": "צלמית",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "למד עוד",
|
||||
"Link": "קישור",
|
||||
"Load_More": "טען עוד",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "רשום הכנת מתכון",
|
||||
"Log_Recipe_Cooking": "רשום בישול מתכון",
|
||||
"Logo": "לוגו",
|
||||
"Make_Header": "הפוך לכותרת",
|
||||
"Make_Ingredient": "הפוך למרכיב",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "נהל ספרים",
|
||||
"Manage_Emails": "נהל כתובות דואר אלקטרוני",
|
||||
"Meal_Plan": "תוכנית ארוחה",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "סוג אוכל נדרש",
|
||||
"Meal_Types": "סוגי אוכל",
|
||||
"Merge": "איחוד",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "איחוד מילת מפתח",
|
||||
"Message": "הודעה",
|
||||
"MissingProperties": "",
|
||||
"Month": "חודש",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "העברה",
|
||||
"MoveCategory": "העבר אל: ",
|
||||
"Move_Down": "העברה למטה",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "יחידה חדשה",
|
||||
"Next_Day": "היום הבא",
|
||||
"Next_Period": "התקופה הבאה",
|
||||
"No": "",
|
||||
"NoCategory": "לא נבחרה קטגוריה.",
|
||||
"NoMoreUndo": "אין עוד שינויים לשחזור.",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "יום קודם",
|
||||
"Previous_Period": "תקופה קודמת",
|
||||
"Print": "הדפסה",
|
||||
"Private": "",
|
||||
"Private_Recipe": "מתכון פרטי",
|
||||
"Private_Recipe_Help": "המתכון מוצג רק לך ולאנשים ששותפו.",
|
||||
"Properties": "ערכים",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "מתכונים",
|
||||
"Recipes_In_Import": "מתכון בקובץ הייבוא",
|
||||
"Recipes_per_page": "מתכונים בכל דף",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "הסר {מזון} מרשימת הקניות",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "מחר ערכים תזונתיים מהמתכון",
|
||||
"Reset": "אפס",
|
||||
"Reset_Search": "אפס חיפוש",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "נבחר",
|
||||
"Servings": "מנות",
|
||||
"Settings": "הגדרות",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "שיתוף",
|
||||
"ShoppingBackgroundSyncWarning": "בעיית תקשורת, מחכה לסנכון...",
|
||||
"Shopping_Categories": "קטגוריות קניות",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "גודל",
|
||||
"Social_Authentication": "אימות חברתי",
|
||||
"Sort_by_new": "סדר ע\"י חדש",
|
||||
"Space": "",
|
||||
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
|
||||
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
|
||||
"StartDate": "תאריך התחלה",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "פעיל עד",
|
||||
"View": "תצוגה",
|
||||
"View_Recipes": "הצג מתכונים",
|
||||
"Visibility": "",
|
||||
"Waiting": "המתנה",
|
||||
"Warning": "אזהרה",
|
||||
"Warning_Delete_Supermarket_Category": "מחיקת קטגורית סופרמרקט תמחוק גם את המאכלים הקשורים. האם אתה בטוח ?",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "מספר השבוע",
|
||||
"Welcome": "ברוכים הבאים",
|
||||
"Year": "שנה",
|
||||
"Yes": "",
|
||||
"add_keyword": "הוסף מילת מפתח",
|
||||
"additional_options": "אפשרויות נוספות",
|
||||
"advanced": "מתקדם",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Korisnički račun",
|
||||
"Add": "Dodaj",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Dodaj {food} na svoj popis za kupovinu",
|
||||
"AddToShopping": "Dodaj na popis za kupovinu",
|
||||
"Add_Servings_to_Shopping": "Dodaj {servings} obroka u Kupovinu",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "Dodao",
|
||||
"Added_on": "Dodano",
|
||||
"Advanced": "Napredno",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Poravnanje",
|
||||
"Amount": "Količina",
|
||||
"App": "Aplikacija",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Automatiziraj",
|
||||
"Automation": "Automatizacija",
|
||||
"Back": "Nazad",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Knjižna oznaka",
|
||||
"Books": "Knjige",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "Prikaži komentare",
|
||||
"Completed": "Završeno",
|
||||
"Conversion": "Konverzija",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopiraj",
|
||||
"Copy Link": "Kopiraj vezu",
|
||||
"Copy Token": "Kopiraj token",
|
||||
@@ -69,6 +82,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.",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "Odgodi do",
|
||||
"Delete": "Obriši",
|
||||
"DeleteShoppingConfirm": "Jesi li siguran da želiš ukloniti svu {food} s popisa za kupnju?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Obriši sve",
|
||||
"Delete_Food": "Obriši namirnicu",
|
||||
"Delete_Keyword": "Obriši ključnu riječ",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "Onemogući količinu",
|
||||
"Disabled": "Onemogućeno",
|
||||
"Documentation": "Dokumentacija",
|
||||
"DontChange": "",
|
||||
"Download": "Preuzimanje",
|
||||
"Drag_Here_To_Delete": "Povuci ovdje za brisanje",
|
||||
"Edit": "Uredi",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "Nadimci namirnice",
|
||||
"Food_Replace": "Zamjena namirnica",
|
||||
"Foods": "Namirnice",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Grupiraj po",
|
||||
"Hide_Food": "Sakrij namirnicu",
|
||||
"Hide_Keyword": "Sakrij ključne riječi",
|
||||
"Hide_Keywords": "Sakrij ključnu riječ",
|
||||
"Hide_Recipes": "Sakrij Recepte",
|
||||
"Hide_as_header": "Sakrij kao zaglavlje",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Sat",
|
||||
"Hours": "Sati",
|
||||
"Icon": "Ikona",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "Saznajte više",
|
||||
"Link": "Poveznica",
|
||||
"Load_More": "Učitaj više",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zapis kuhanja",
|
||||
"Log_Recipe_Cooking": "Dnevnik recepata kuhanja",
|
||||
"Logo": "Logotip",
|
||||
"Make_Header": "Napravi zaglavlje",
|
||||
"Make_Ingredient": "Napravi sastojak",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Upravljaj knjigama",
|
||||
"Manage_Emails": "Upravljanje e-poštom",
|
||||
"Meal_Plan": "Plan obroka",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "Potreban je tip obroka",
|
||||
"Meal_Types": "Tipovi obroka",
|
||||
"Merge": "Spoji",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Spoji ključnu riječ",
|
||||
"Message": "Poruka",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mjesec",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Premjesti",
|
||||
"MoveCategory": "Premjesti u: ",
|
||||
"Move_Down": "Premjesti dolje",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "Nova jedinica",
|
||||
"Next_Day": "Sljedeći dan",
|
||||
"Next_Period": "Slijedeće razdoblje",
|
||||
"No": "",
|
||||
"NoCategory": "Nije odabrana kategorija.",
|
||||
"NoMoreUndo": "Nema promjena koje se mogu poništiti.",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "Prethodni dan",
|
||||
"Previous_Period": "Prethodno razdoblje",
|
||||
"Print": "Ispis",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Privatni Recept",
|
||||
"Private_Recipe_Help": "Recept se prikazuje samo vama i osobama s kojima se dijeli.",
|
||||
"Properties": "Svojstva",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "Recepti",
|
||||
"Recipes_In_Import": "Recepti u vašoj datoteci za uvoz",
|
||||
"Recipes_per_page": "Recepata po stranici",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Ukloni {food} sa svog popisa za kupovinu",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Izbrišite hranjive sastojke iz recepta",
|
||||
"Reset": "Ponovo postavi",
|
||||
"Reset_Search": "Poništi pretragu",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "Odabrano",
|
||||
"Servings": "Porcije",
|
||||
"Settings": "Postavke",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Podijeli",
|
||||
"ShoppingBackgroundSyncWarning": "Loša mreža, čeka se sinkronizacija...",
|
||||
"Shopping_Categories": "Kategorije Kupovine",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "Veličina",
|
||||
"Social_Authentication": "Autentifikacija putem društvenih mreža",
|
||||
"Sort_by_new": "Poredaj po novom",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "Vrijedi do",
|
||||
"View": "Pogled",
|
||||
"View_Recipes": "Pogledajte recepte",
|
||||
"Visibility": "",
|
||||
"Waiting": "Čekanje",
|
||||
"Warning": "Upozorenje",
|
||||
"Warning_Delete_Supermarket_Category": "Brisanje kategorije supermarketa također će izbrisati sve odnose na namirnice. Jeste li sigurni?",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "Brojevi tjedana",
|
||||
"Welcome": "Dobrodošli",
|
||||
"Year": "Godina",
|
||||
"Yes": "",
|
||||
"add_keyword": "Dodaj ključnu riječ",
|
||||
"additional_options": "Dodatne mogućnosti",
|
||||
"advanced": "Napredno",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Fiók",
|
||||
"Add": "Hozzáadás",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "{food} hozzáadása bevásárlólistához",
|
||||
"AddToShopping": "Hozzáadás a bevásárlólistához",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -15,6 +17,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ó",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "Automatizálás",
|
||||
"Automation": "Automatizálás",
|
||||
"Back": "Vissza",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Könyvjelző",
|
||||
"Books": "Könyvek",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -51,6 +63,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",
|
||||
@@ -66,6 +79,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": "",
|
||||
@@ -79,6 +93,7 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "Törlés",
|
||||
"DeleteShoppingConfirm": "Biztos, hogy az összes {food}-t el akarja távolítani a bevásárlólistáról?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "Alapanyag törlése",
|
||||
"Delete_Keyword": "Kulcsszó törlése",
|
||||
"Description": "Megnevezés",
|
||||
@@ -87,6 +102,7 @@
|
||||
"Disable_Amount": "Összeg kikapcsolása",
|
||||
"Disabled": "Kikapcsolva",
|
||||
"Documentation": "Dokumentáció",
|
||||
"DontChange": "",
|
||||
"Download": "Letöltés",
|
||||
"Drag_Here_To_Delete": "Törléshez húzza ide",
|
||||
"Edit": "Szerkesztés",
|
||||
@@ -119,16 +135,20 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "Étel cseréje",
|
||||
"Foods": "Alapanyagok",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Csoportosítva",
|
||||
"Hide_Food": "Alapanyag elrejtése",
|
||||
"Hide_Keyword": "Kulcsszavak elrejtése",
|
||||
"Hide_Keywords": "Kulcsszó elrejtése",
|
||||
"Hide_Recipes": "Receptek elrejtése",
|
||||
"Hide_as_header": "Fejlécként elrejtve",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Óra",
|
||||
"Hours": "Óra",
|
||||
"Icon": "Ikon",
|
||||
@@ -171,10 +191,13 @@
|
||||
"Learn_More": "Tudjon meg többet",
|
||||
"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",
|
||||
"Make_Ingredient": "Összetevő létrehozása",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Könyvek kezelése",
|
||||
"Manage_Emails": "Levelezés kezelése",
|
||||
"Meal_Plan": "Menüterv",
|
||||
@@ -183,10 +206,13 @@
|
||||
"Meal_Type_Required": "Étkezés megadása kötelező",
|
||||
"Meal_Types": "Étkezések",
|
||||
"Merge": "Összefűzés",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Kulcsszó összevonása",
|
||||
"Message": "Üzenet",
|
||||
"MissingProperties": "",
|
||||
"Month": "Hónap",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mozgatás",
|
||||
"MoveCategory": "Áthelyezés ide: ",
|
||||
"Move_Down": "Lefelé mozgatás",
|
||||
@@ -210,6 +236,7 @@
|
||||
"New_Unit": "Új mennyiségi egység",
|
||||
"Next_Day": "Következő nap",
|
||||
"Next_Period": "Következő periódus",
|
||||
"No": "",
|
||||
"NoCategory": "Nincs kategória kiválasztva.",
|
||||
"NoUnit": "",
|
||||
"No_ID": "Azonosító nem található, ezért nem törölhető.",
|
||||
@@ -248,6 +275,7 @@
|
||||
"Previous_Day": "Előző nap",
|
||||
"Previous_Period": "Előző periódus",
|
||||
"Print": "Nyomtatás",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Privát recept",
|
||||
"Private_Recipe_Help": "A recept csak Önnek és azoknak az embereknek jelenik meg, akikkel megosztotta.",
|
||||
"Properties": "Tulajdonságok",
|
||||
@@ -266,7 +294,9 @@
|
||||
"Recipes": "Receptek",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "Receptek oldalanként",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "{food} eltávolítása bevásárlólistáról",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Tápértékadatok törlése a receptből",
|
||||
"Reset": "Visszaállítás",
|
||||
"Reset_Search": "Keresés alaphelyzetbe állítása",
|
||||
@@ -287,6 +317,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",
|
||||
@@ -299,6 +330,7 @@
|
||||
"Single": "Egyetlen",
|
||||
"Size": "Méret",
|
||||
"Sort_by_new": "Rendezés legújabbak szerint",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -350,6 +382,7 @@
|
||||
"Valid Until": "Érvényes",
|
||||
"View": "Nézet",
|
||||
"View_Recipes": "Receptek megjelenítése",
|
||||
"Visibility": "",
|
||||
"Waiting": "Várakozás",
|
||||
"Warning": "Figyelmeztetés",
|
||||
"Warning_Delete_Supermarket_Category": "Egy szupermarket-kategória törlése az alapanyagokkal való összes kapcsolatot is törli. Biztos vagy benne?",
|
||||
@@ -358,6 +391,7 @@
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "Üdvözöljük",
|
||||
"Year": "Év",
|
||||
"Yes": "",
|
||||
"add_keyword": "Kulcsszó hozzáadása",
|
||||
"additional_options": "További lehetőségek",
|
||||
"advanced": "Haladó",
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"Add_nutrition_recipe": "Ավելացնել սննդայնություն բաղադրատոմսին",
|
||||
"Add_to_Book": "",
|
||||
"Add_to_Plan": "Ավելացնել պլանին",
|
||||
"Add_to_Shopping": "Ավելացնել գնումներին",
|
||||
"Advanced Search Settings": "Ընդլայնված փնտրման կարգավորումներ",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Apply": "",
|
||||
"Automate": "Ավտոմատացնել",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Books": "",
|
||||
"CREATE_ERROR": "",
|
||||
"Calories": "",
|
||||
@@ -17,17 +29,21 @@
|
||||
"Categories": "",
|
||||
"Category": "",
|
||||
"Close": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Create": "Ստեղծել",
|
||||
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
|
||||
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
|
||||
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
|
||||
"Credits": "",
|
||||
"DELETE_ERROR": "",
|
||||
"Date": "",
|
||||
"Delete": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "Ջնջել սննդամթերքը",
|
||||
"Delete_Keyword": "Ջնջել բանալի բառը",
|
||||
"Description": "Նկարագրություն",
|
||||
"DontChange": "",
|
||||
"Download": "Ներբեռնել",
|
||||
"Edit": "",
|
||||
"Edit_Food": "Խմբագրել սննդամթերքը",
|
||||
@@ -44,14 +60,18 @@
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Food": "Սննդամթերք",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Hide_Food": "Թաքցնել սննդամթերքը",
|
||||
"Hide_Keywords": "Թաքցնել բանալի բառը",
|
||||
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
|
||||
"Hide_as_header": "Թաքցնել որպես խորագիր",
|
||||
"Hierarchy": "",
|
||||
"IgnoreAccents": "",
|
||||
"IgnoreAccentsHelp": "",
|
||||
"Import": "Ներմուծել",
|
||||
@@ -61,13 +81,19 @@
|
||||
"Keywords": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Գրանցել եփելը",
|
||||
"Log_Recipe_Cooking": "Գրանցել բաղադրատոմսի օգտագործում",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Կարգավորել Գրքերը",
|
||||
"Meal_Plan": "Ճաշացուցակ",
|
||||
"Merge": "Միացնել",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Միացնել բանալի բառը",
|
||||
"MissingProperties": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Տեղափոխել",
|
||||
"Move_Food": "Տեղափոխել սննդամթերքը",
|
||||
"Move_Keyword": "Տեղափոխել բանալի բառը",
|
||||
@@ -76,6 +102,7 @@
|
||||
"New_Food": "Նոր սննդամթերք",
|
||||
"New_Keyword": "Նոր բանալի բառ",
|
||||
"New_Recipe": "Նոր բաղադրատոմս",
|
||||
"No": "",
|
||||
"NoUnit": "",
|
||||
"No_Results": "Արդյունքներ չկան",
|
||||
"Nutrition": "",
|
||||
@@ -88,6 +115,8 @@
|
||||
"PrecisionSearchHelp": "",
|
||||
"Preparation": "",
|
||||
"Print": "Տպել",
|
||||
"Private": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Proteins": "",
|
||||
"Rating": "",
|
||||
"Recently_Viewed": "Վերջերս դիտած",
|
||||
@@ -96,6 +125,8 @@
|
||||
"Recipe_Image": "Բաղադրատոմսի նկար",
|
||||
"Recipes": "Բաղադրատոմսեր",
|
||||
"Recipes_per_page": "Բաղադրատոմս էջում",
|
||||
"RemoveAllType": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Հեռացնել բաղադրատոմսի սննդայնությունը",
|
||||
"Reset_Search": "Զրոյացնել որոնումը",
|
||||
"Save": "",
|
||||
@@ -109,12 +140,14 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "Կարգավորումներ",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Category": "Գնումների կատեգորիա",
|
||||
"Shopping_list": "Գնումների ցուցակ",
|
||||
"Show_as_header": "Ցույց տալ որպես խորագիր",
|
||||
"Size": "",
|
||||
"Sort_by_new": "Տեսակավորել ըստ նորերի",
|
||||
"Space": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
"Step": "",
|
||||
@@ -132,7 +165,9 @@
|
||||
"Use_Plural_Unit_Simple": "",
|
||||
"View": "Դիտել",
|
||||
"View_Recipes": "Դիտել բաղադրատոմսերը",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"Yes": "",
|
||||
"all_fields_optional": "Բոլոր տողերը կամավոր են և կարող են մնալ դատարկ։",
|
||||
"and": "և",
|
||||
"confirm_delete": "Համոզվա՞ծ եք, որ ուզում եք ջնջել այս {օբյեկտը}։",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Add": "Tambahkan",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
"AddToShopping": "",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -15,12 +17,22 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "",
|
||||
"Auto_Planner": "",
|
||||
"Automate": "",
|
||||
"Automation": "Automatis",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "Buku",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -43,6 +55,7 @@
|
||||
"Coming_Soon": "",
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Salin",
|
||||
"Copy Link": "Salin Tautan",
|
||||
"Copy Token": "Salin Token",
|
||||
@@ -58,6 +71,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -70,6 +84,7 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "Menghapus",
|
||||
"DeleteShoppingConfirm": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "",
|
||||
"Delete_Keyword": "Hapus Kata Kunci",
|
||||
"Description": "",
|
||||
@@ -77,6 +92,7 @@
|
||||
"Disable_Amount": "Nonaktifkan Jumlah",
|
||||
"Disabled": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "Unduh",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Edit": "Sunting",
|
||||
@@ -107,16 +123,20 @@
|
||||
"FoodOnHand": "",
|
||||
"Food_Alias": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "Sembunyikan Kata Kunci",
|
||||
"Hide_Recipes": "Sembunyikan Resep",
|
||||
"Hide_as_header": "Sembunyikan sebagai tajuk",
|
||||
"Hierarchy": "",
|
||||
"Hour": "",
|
||||
"Hours": "",
|
||||
"Icon": "",
|
||||
@@ -156,10 +176,13 @@
|
||||
"Last_name": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Muat lebih banyak",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Log Memasak",
|
||||
"Log_Recipe_Cooking": "Log Resep Memasak",
|
||||
"Make_Header": "Buat Header",
|
||||
"Make_Ingredient": "Buat bahan",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Kelola Buku",
|
||||
"Manage_Emails": "",
|
||||
"Meal_Plan": "rencana makan",
|
||||
@@ -168,10 +191,13 @@
|
||||
"Meal_Type_Required": "",
|
||||
"Meal_Types": "",
|
||||
"Merge": "Menggabungkan",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Gabungkan Kata Kunci",
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Bergerak",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "Pindahkan kebawah",
|
||||
@@ -194,6 +220,7 @@
|
||||
"New_Unit": "",
|
||||
"Next_Day": "",
|
||||
"Next_Period": "",
|
||||
"No": "",
|
||||
"NoCategory": "",
|
||||
"NoUnit": "",
|
||||
"No_ID": "",
|
||||
@@ -226,6 +253,7 @@
|
||||
"Previous_Day": "",
|
||||
"Previous_Period": "",
|
||||
"Print": "Mencetak",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Resep Pribadi",
|
||||
"Private_Recipe_Help": "Resep hanya diperlihatkan kepada Anda dan orang-orang yang dibagikan resep tersebut.",
|
||||
"Protected": "Terlindung",
|
||||
@@ -242,7 +270,9 @@
|
||||
"Recipes": "Resep",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "Resep per Halaman",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Hapus nutrisi dari resep",
|
||||
"Reset": "",
|
||||
"Reset_Search": "Setel Ulang Pencarian",
|
||||
@@ -263,6 +293,7 @@
|
||||
"Selected": "Terpilih",
|
||||
"Servings": "Porsi",
|
||||
"Settings": "Pengaturan",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Bagikan",
|
||||
"Shopping_Categories": "Kategori Belanja",
|
||||
"Shopping_Category": "Kategori Belanja",
|
||||
@@ -276,6 +307,7 @@
|
||||
"Size": "Ukuran",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Urutkan berdasarkan baru",
|
||||
"Space": "",
|
||||
"Starting_Day": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -318,6 +350,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "Melihat",
|
||||
"View_Recipes": "Lihat Resep",
|
||||
"Visibility": "",
|
||||
"Waiting": "Menunggu",
|
||||
"Warning": "",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -325,6 +358,7 @@
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
"AddToShopping": "",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "",
|
||||
"App": "",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "",
|
||||
"Automation": "",
|
||||
"Back": "",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -69,6 +82,7 @@
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Created": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"CustomImageHelp": "",
|
||||
@@ -89,6 +103,7 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "",
|
||||
"DeleteShoppingConfirm": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "",
|
||||
"Delete_Food": "",
|
||||
"Delete_Keyword": "",
|
||||
@@ -98,6 +113,7 @@
|
||||
"Disable_Amount": "",
|
||||
"Disabled": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Edit": "",
|
||||
@@ -135,16 +151,20 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "",
|
||||
"Hide_Recipes": "",
|
||||
"Hide_as_header": "",
|
||||
"Hierarchy": "",
|
||||
"Hour": "",
|
||||
"Hours": "",
|
||||
"Icon": "",
|
||||
@@ -188,11 +208,14 @@
|
||||
"Learn_More": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Logo": "",
|
||||
"Make_Header": "",
|
||||
"Make_Ingredient": "",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "",
|
||||
"Manage_Emails": "",
|
||||
"Meal_Plan": "",
|
||||
@@ -201,10 +224,13 @@
|
||||
"Meal_Type_Required": "",
|
||||
"Meal_Types": "",
|
||||
"Merge": "",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "",
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -231,6 +257,7 @@
|
||||
"New_Unit": "",
|
||||
"Next_Day": "",
|
||||
"Next_Period": "",
|
||||
"No": "",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "",
|
||||
"NoUnit": "",
|
||||
@@ -271,6 +298,7 @@
|
||||
"Previous_Day": "",
|
||||
"Previous_Period": "",
|
||||
"Print": "",
|
||||
"Private": "",
|
||||
"Private_Recipe": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Properties": "",
|
||||
@@ -292,7 +320,9 @@
|
||||
"Recipes": "",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "",
|
||||
"Reset": "",
|
||||
"Reset_Search": "",
|
||||
@@ -313,6 +343,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"ShoppingBackgroundSyncWarning": "",
|
||||
"Shopping_Categories": "",
|
||||
@@ -330,6 +361,7 @@
|
||||
"Size": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"Space_Cosmetic_Settings": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
@@ -388,6 +420,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "",
|
||||
"View_Recipes": "",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"Warning": "",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -396,6 +429,7 @@
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Utilizza IA per importare le immagini delle ricette.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Attività",
|
||||
"Add": "Aggiungi",
|
||||
"AddAll": "Aggiungi tutto",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Aggiungi filtro",
|
||||
"AddFoodToShopping": "Aggiungi {food} alla tua lista della spesa",
|
||||
"AddMany": "Aggiungi molti",
|
||||
@@ -26,6 +28,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à",
|
||||
@@ -45,6 +53,10 @@
|
||||
"BaseUnit": "Unità di base",
|
||||
"BaseUnitHelp": "Unità standard per la conversione automatica di unità",
|
||||
"Basics": "Informazioni di base",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Libro",
|
||||
"Bookmarklet": "Segnalibro",
|
||||
"BookmarkletHelp1": "Trascina il pulsante seguente nella barra dei tuoi segnalibri",
|
||||
@@ -83,6 +95,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",
|
||||
@@ -105,6 +118,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",
|
||||
@@ -131,6 +145,7 @@
|
||||
"Delete": "Elimina",
|
||||
"DeleteConfirmQuestion": "Sei sicuro di voler eliminare questo oggetto?",
|
||||
"DeleteShoppingConfirm": "Sei sicuro di voler rimuovere tutto {food} dalla lista della spesa?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Elimina tutti",
|
||||
"Delete_Food": "Elimina alimento",
|
||||
"Delete_Keyword": "Elimina parola chiave",
|
||||
@@ -143,6 +158,7 @@
|
||||
"Disable_Amount": "Disabilita quantità",
|
||||
"Disabled": "Disabilitato",
|
||||
"Documentation": "Documentazione",
|
||||
"DontChange": "",
|
||||
"Down": "Giù",
|
||||
"Download": "Scarica",
|
||||
"DragToUpload": "Trascina e rilascia o fai clic per selezionare",
|
||||
@@ -196,11 +212,14 @@
|
||||
"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": "",
|
||||
"GroupBy": "Raggruppa per",
|
||||
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
|
||||
"Headline": "Intestazione",
|
||||
@@ -211,6 +230,7 @@
|
||||
"Hide_Keywords": "Nascondi parola chiave",
|
||||
"Hide_Recipes": "Nascondi ricette",
|
||||
"Hide_as_header": "Nascondi come intestazione",
|
||||
"Hierarchy": "",
|
||||
"History": "Cronologia",
|
||||
"HostedFreeVersion": "Stai utilizzando la versione gratuita di Tandoor",
|
||||
"Hour": "Ora",
|
||||
@@ -267,6 +287,8 @@
|
||||
"Link": "Collegamento",
|
||||
"Load": "Carica",
|
||||
"Load_More": "Carica altro",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registro ricette cucinate",
|
||||
"Log_Recipe_Cooking": "Aggiungi a ricette cucinate",
|
||||
"Logo": "Logo",
|
||||
@@ -295,6 +317,8 @@
|
||||
"ModelSelectResultsHelp": "Cerca altri risultati",
|
||||
"Monday": "Lunedì",
|
||||
"Month": "Mese",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Altro",
|
||||
"Move": "Sposta",
|
||||
"MoveCategory": "Sposta in: ",
|
||||
@@ -324,6 +348,7 @@
|
||||
"Next": "Successivo",
|
||||
"Next_Day": "Giorno successivo",
|
||||
"Next_Period": "Periodo successivo",
|
||||
"No": "",
|
||||
"NoCategory": "Nessuna categoria selezionata.",
|
||||
"NoMoreUndo": "Nessuna modifica da annullare.",
|
||||
"NoUnit": "Nessuna unità",
|
||||
@@ -376,6 +401,7 @@
|
||||
"Previous_Day": "Giorno precedente",
|
||||
"Previous_Period": "Periodo precedente",
|
||||
"Print": "Stampa",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Ricetta privata",
|
||||
"Private_Recipe_Help": "La ricetta viene mostrata solo a te e a chi l'hai condivisa.",
|
||||
"Profile": "Profilo",
|
||||
@@ -410,7 +436,9 @@
|
||||
"Recipes_In_Import": "Ricette nel tuo file di importazione",
|
||||
"Recipes_per_page": "Ricette per pagina",
|
||||
"Remove": "Rimuovi",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Rimuovi {food} dalla tua lista della spesa",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Elimina nutrienti dalla ricetta",
|
||||
"Reset": "Azzera",
|
||||
"ResetHelp": "Ripristina aiuto",
|
||||
@@ -444,6 +472,7 @@
|
||||
"Servings": "Porzioni",
|
||||
"ServingsText": "Testo porzioni",
|
||||
"Settings": "Impostazioni",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Condividi",
|
||||
"ShopLater": "Compra dopo",
|
||||
"ShopNow": "Compra subito",
|
||||
@@ -472,6 +501,7 @@
|
||||
"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": "",
|
||||
"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.",
|
||||
@@ -574,6 +604,7 @@
|
||||
"ViewLogHelp": "Cronologia delle ricette visualizzate. ",
|
||||
"View_Recipes": "Mostra ricette",
|
||||
"Viewed": "Visualizzata",
|
||||
"Visibility": "",
|
||||
"Waiting": "Attesa",
|
||||
"WaitingTime": "Tempo di attesa",
|
||||
"WarnPageLeave": "Ci sono modifiche non salvate che andranno perse. Vuoi comunque abbandonare la pagina?",
|
||||
@@ -587,6 +618,7 @@
|
||||
"Welcome": "Benvenuto",
|
||||
"WorkingTime": "Orario lavorativo",
|
||||
"Year": "Anno",
|
||||
"Yes": "",
|
||||
"YourSpaces": "I tuoi spazi",
|
||||
"active": "attivo",
|
||||
"add_keyword": "Aggiungi parola chiave",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
"AddToShopping": "",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "Suma",
|
||||
"App": "",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "",
|
||||
"Automation": "",
|
||||
"Back": "",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -51,6 +63,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -67,6 +80,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -81,6 +95,7 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "",
|
||||
"DeleteShoppingConfirm": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "",
|
||||
"Delete_Keyword": "Ištrinti raktažodį",
|
||||
"Description": "",
|
||||
@@ -89,6 +104,7 @@
|
||||
"Disable_Amount": "Išjungti sumą",
|
||||
"Disabled": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Edit": "",
|
||||
@@ -121,16 +137,20 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "Paslėpti raktažodį",
|
||||
"Hide_Recipes": "Paslėpti receptus",
|
||||
"Hide_as_header": "Slėpti kaip antraštę",
|
||||
"Hierarchy": "",
|
||||
"Hour": "",
|
||||
"Hours": "",
|
||||
"Icon": "",
|
||||
@@ -173,10 +193,13 @@
|
||||
"Learn_More": "",
|
||||
"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ę",
|
||||
"Make_Ingredient": "Padaryti ingredientą",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Tvarkyti knygas",
|
||||
"Manage_Emails": "",
|
||||
"Meal_Plan": "Maisto planas",
|
||||
@@ -185,10 +208,13 @@
|
||||
"Meal_Type_Required": "",
|
||||
"Meal_Types": "",
|
||||
"Merge": "",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Sujungti raktažodį",
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "Nuleisti žemyn",
|
||||
@@ -213,6 +239,7 @@
|
||||
"New_Unit": "",
|
||||
"Next_Day": "",
|
||||
"Next_Period": "",
|
||||
"No": "",
|
||||
"NoCategory": "",
|
||||
"NoUnit": "",
|
||||
"No_ID": "",
|
||||
@@ -252,6 +279,7 @@
|
||||
"Previous_Day": "",
|
||||
"Previous_Period": "",
|
||||
"Print": "",
|
||||
"Private": "",
|
||||
"Private_Recipe": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Properties": "",
|
||||
@@ -270,7 +298,9 @@
|
||||
"Recipes": "",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "Receptų skaičius per puslapį",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Ištrinti mitybos informaciją iš recepto",
|
||||
"Reset": "",
|
||||
"Reset_Search": "Iš naujo nustatyti paiešką",
|
||||
@@ -291,6 +321,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Categories": "",
|
||||
"Shopping_Category": "",
|
||||
@@ -304,6 +335,7 @@
|
||||
"Size": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Rūšiuoti pagal naujumą",
|
||||
"Space": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
"Starting_Day": "",
|
||||
@@ -358,6 +390,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "",
|
||||
"View_Recipes": "Žiūrėti receptus",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"Warning": "",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -366,6 +399,7 @@
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
"AddToShopping": "",
|
||||
"Add_Servings_to_Shopping": "",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "",
|
||||
"App": "",
|
||||
@@ -26,6 +34,10 @@
|
||||
"Automate": "",
|
||||
"Automation": "",
|
||||
"Back": "",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -52,6 +64,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -69,6 +82,7 @@
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Created": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"CustomImageHelp": "",
|
||||
@@ -90,6 +104,7 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "",
|
||||
"DeleteShoppingConfirm": "",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "",
|
||||
"Delete_Food": "",
|
||||
"Delete_Keyword": "",
|
||||
@@ -99,6 +114,7 @@
|
||||
"Disable_Amount": "",
|
||||
"Disabled": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Edit": "",
|
||||
@@ -136,16 +152,20 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "",
|
||||
"Hide_Recipes": "",
|
||||
"Hide_as_header": "",
|
||||
"Hierarchy": "",
|
||||
"Hour": "",
|
||||
"Hours": "",
|
||||
"Icon": "",
|
||||
@@ -189,11 +209,14 @@
|
||||
"Learn_More": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Logo": "",
|
||||
"Make_Header": "",
|
||||
"Make_Ingredient": "",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "",
|
||||
"Manage_Emails": "",
|
||||
"Meal_Plan": "",
|
||||
@@ -202,10 +225,13 @@
|
||||
"Meal_Type_Required": "",
|
||||
"Meal_Types": "",
|
||||
"Merge": "",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "",
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -232,6 +258,7 @@
|
||||
"New_Unit": "",
|
||||
"Next_Day": "",
|
||||
"Next_Period": "",
|
||||
"No": "",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "",
|
||||
"NoUnit": "",
|
||||
@@ -272,6 +299,7 @@
|
||||
"Previous_Day": "",
|
||||
"Previous_Period": "",
|
||||
"Print": "",
|
||||
"Private": "",
|
||||
"Private_Recipe": "",
|
||||
"Private_Recipe_Help": "",
|
||||
"Properties": "",
|
||||
@@ -293,7 +321,9 @@
|
||||
"Recipes": "",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "",
|
||||
"Reset": "",
|
||||
"Reset_Search": "",
|
||||
@@ -314,6 +344,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"ShoppingBackgroundSyncWarning": "",
|
||||
"Shopping_Categories": "",
|
||||
@@ -332,6 +363,7 @@
|
||||
"Size": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"Space_Cosmetic_Settings": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
@@ -390,6 +422,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "",
|
||||
"View_Recipes": "",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"Warning": "",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -398,6 +431,7 @@
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Add": "Legg til",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Legg til {food] i handlelisten din",
|
||||
"AddToShopping": "Legg til i handleliste",
|
||||
"Add_Servings_to_Shopping": "Legg til {servings} serveringer i handlelisten",
|
||||
@@ -15,6 +17,12 @@
|
||||
"Added_by": "Lagt til av",
|
||||
"Added_on": "Lagt til",
|
||||
"Advanced": "Avansert",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Justering",
|
||||
"Amount": "Mengde",
|
||||
"App": "App",
|
||||
@@ -25,6 +33,10 @@
|
||||
"Auto_Sort_Help": "Flytt alle ingredienser til det mest passende steget.",
|
||||
"Automate": "Automatiser",
|
||||
"Automation": "Automatiser",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "",
|
||||
"Books": "Bøker",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -50,6 +62,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "Fullført",
|
||||
"Conversion": "Omregn enhet",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopier",
|
||||
"Copy Link": "Kopier lenke",
|
||||
"Copy Token": "Kopier Token",
|
||||
@@ -66,6 +79,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.",
|
||||
@@ -86,6 +100,7 @@
|
||||
"DelayUntil": "Forsink til",
|
||||
"Delete": "Slett",
|
||||
"DeleteShoppingConfirm": "Er du sikker på at du fjerne alle {food} fra handlelisten?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Slett alle",
|
||||
"Delete_Food": "Slett Matrett",
|
||||
"Delete_Keyword": "Slett nøkkelord",
|
||||
@@ -95,6 +110,7 @@
|
||||
"Disable_Amount": "Deaktiver mengde",
|
||||
"Disabled": "",
|
||||
"Documentation": "",
|
||||
"DontChange": "",
|
||||
"Download": "Last ned",
|
||||
"Drag_Here_To_Delete": "Dra her for å slette",
|
||||
"Edit": "Rediger",
|
||||
@@ -127,16 +143,20 @@
|
||||
"FoodOnHand": "Du har {food} på lager.",
|
||||
"Food_Alias": "Matrett Alias",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Grupér",
|
||||
"Hide_Food": "Skjul Matrett",
|
||||
"Hide_Keyword": "Skjul nøkkelord",
|
||||
"Hide_Keywords": "Skjul nøkkelord",
|
||||
"Hide_Recipes": "Skjul oppskrifter",
|
||||
"Hide_as_header": "Skjul overskrift",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Time",
|
||||
"Hours": "Timer",
|
||||
"Icon": "Ikon",
|
||||
@@ -180,10 +200,13 @@
|
||||
"Learn_More": "Lær mer",
|
||||
"Link": "Lenke",
|
||||
"Load_More": "Last inn flere",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Loggfør tilbereding",
|
||||
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
|
||||
"Make_Header": "Bruk som overskrift",
|
||||
"Make_Ingredient": "Bruk som ingrediens",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Administrer bøker",
|
||||
"Manage_Emails": "Administrer e-poster",
|
||||
"Meal_Plan": "Måltidsplan",
|
||||
@@ -192,10 +215,13 @@
|
||||
"Meal_Type_Required": "Måltidstype er nødvendig",
|
||||
"Meal_Types": "Måltidstyper",
|
||||
"Merge": "Slå sammen",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Slå sammen nøkkelord",
|
||||
"Message": "Melding",
|
||||
"MissingProperties": "",
|
||||
"Month": "Måned",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Flytt",
|
||||
"MoveCategory": "Flytt til: ",
|
||||
"Move_Down": "Flytt ned",
|
||||
@@ -218,6 +244,7 @@
|
||||
"New_Unit": "Ny Enhet",
|
||||
"Next_Day": "Neste dag",
|
||||
"Next_Period": "Neste periode",
|
||||
"No": "",
|
||||
"NoCategory": "Ingen kategori valgt.",
|
||||
"NoMoreUndo": "Ingen endringer å angre.",
|
||||
"NoUnit": "",
|
||||
@@ -257,6 +284,7 @@
|
||||
"Previous_Day": "Forrige dag",
|
||||
"Previous_Period": "Forrige periode",
|
||||
"Print": "Skriv ut",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Privat Oppskrift",
|
||||
"Private_Recipe_Help": "Oppskriften er bare vist til deg og dem du har delt den med.",
|
||||
"Properties": "Egenskaper",
|
||||
@@ -277,7 +305,9 @@
|
||||
"Recipes": "Oppskrift",
|
||||
"Recipes_In_Import": "",
|
||||
"Recipes_per_page": "Oppskrifter per side",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Fjern {food} fra handelisten din",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Fjern næringsinnhold fra oppskrift",
|
||||
"Reset": "",
|
||||
"Reset_Search": "Nullstill søk",
|
||||
@@ -298,6 +328,7 @@
|
||||
"Selected": "Valgte",
|
||||
"Servings": "Porsjoner",
|
||||
"Settings": "Innstillinger",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Del",
|
||||
"ShoppingBackgroundSyncWarning": "Dårlig nettverkstilkobling, venter på synkronisering...",
|
||||
"Shopping_Categories": "Butikk Kategorier",
|
||||
@@ -314,6 +345,7 @@
|
||||
"Size": "Størrelse",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Sorter etter nyest",
|
||||
"Space": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "Startdato",
|
||||
"Starting_Day": "Dag uken skal state på",
|
||||
@@ -367,6 +399,7 @@
|
||||
"Valid Until": "",
|
||||
"View": "Visning",
|
||||
"View_Recipes": "Vis oppskrifter",
|
||||
"Visibility": "",
|
||||
"Waiting": "Venter",
|
||||
"Warning": "Advarsel",
|
||||
"Warning_Delete_Supermarket_Category": "",
|
||||
@@ -375,6 +408,7 @@
|
||||
"Week_Numbers": "Ukenummer",
|
||||
"Welcome": "Velkommen",
|
||||
"Year": "År",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
"additional_options": "",
|
||||
"advanced": "Avansert",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Activiteit",
|
||||
"Add": "Voeg toe",
|
||||
"AddAll": "Voeg alles toe",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Voeg filter toe",
|
||||
"AddFoodToShopping": "Voeg {food} toe aan je boodschappenlijst",
|
||||
"AddMany": "Voeg meerdere toe",
|
||||
@@ -27,6 +29,12 @@
|
||||
"Admin": "Beheer",
|
||||
"Advanced": "Geavanceerd",
|
||||
"Advanced Search Settings": "Geavanceerde zoekinstellingen",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Afstemming",
|
||||
"AllRecipes": "Alle recepten",
|
||||
"Amount": "Hoeveelheid",
|
||||
@@ -46,6 +54,10 @@
|
||||
"BaseUnit": "Basiseenheid",
|
||||
"BaseUnitHelp": "Standaardeenheid om automatische eenheden om te rekenen",
|
||||
"Basics": "Basisprincipes",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Boek",
|
||||
"Bookmarklet": "Bladwijzer",
|
||||
"BookmarkletHelp1": "Sleep de onderstaande knop naar je bladwijzerbalk",
|
||||
@@ -84,6 +96,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",
|
||||
@@ -106,6 +119,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",
|
||||
@@ -132,6 +146,7 @@
|
||||
"Delete": "Verwijder",
|
||||
"DeleteConfirmQuestion": "Weet je zeker dat je dit object wilt verwijderen?",
|
||||
"DeleteShoppingConfirm": "Weet je zeker dat je {food} van de boodschappenlijst wil verwijderen?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Verwijder allen",
|
||||
"Delete_Food": "Verwijder voedingsmiddel",
|
||||
"Delete_Keyword": "Verwijder trefwoord",
|
||||
@@ -144,6 +159,7 @@
|
||||
"Disable_Amount": "Schakel hoeveelheid uit",
|
||||
"Disabled": "Gedeactiveerd",
|
||||
"Documentation": "Documentatie",
|
||||
"DontChange": "",
|
||||
"Down": "Omlaag",
|
||||
"Download": "Download",
|
||||
"DragToUpload": "Slepen en neerzetten of klik om te selecteren",
|
||||
@@ -197,11 +213,14 @@
|
||||
"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": "",
|
||||
"GroupBy": "Groepeer per",
|
||||
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
|
||||
"Headline": "Koptekst",
|
||||
@@ -212,6 +231,7 @@
|
||||
"Hide_Keywords": "Verberg trefwoord",
|
||||
"Hide_Recipes": "Verberg recepten",
|
||||
"Hide_as_header": "Verberg als koptekst",
|
||||
"Hierarchy": "",
|
||||
"History": "Geschiedenis",
|
||||
"HostedFreeVersion": "Je gebruikt de gratis versie van Tandoor",
|
||||
"Hour": "Uur",
|
||||
@@ -268,6 +288,8 @@
|
||||
"Link": "Link",
|
||||
"Load": "Laden",
|
||||
"Load_More": "Laad meer",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registreer bereiding",
|
||||
"Log_Recipe_Cooking": "Bereiding registreren",
|
||||
"Logo": "Logo",
|
||||
@@ -296,6 +318,8 @@
|
||||
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
|
||||
"Monday": "Maandag",
|
||||
"Month": "Maand",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Meer",
|
||||
"Move": "Verplaats",
|
||||
"MoveCategory": "Verplaats naar: ",
|
||||
@@ -325,6 +349,7 @@
|
||||
"Next": "Volgende",
|
||||
"Next_Day": "Volgende dag",
|
||||
"Next_Period": "Volgende periode",
|
||||
"No": "",
|
||||
"NoCategory": "Geen categorie geselecteerd",
|
||||
"NoMoreUndo": "Geen veranderingen om ongedaan te maken.",
|
||||
"NoUnit": "Geen eenheid",
|
||||
@@ -377,6 +402,7 @@
|
||||
"Previous_Day": "Vorige dag",
|
||||
"Previous_Period": "Vorige periode",
|
||||
"Print": "Afdrukken",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Privé recept",
|
||||
"Private_Recipe_Help": "Recept is alleen zichtbaar voor jou en de mensen waar je het mee gedeeld hebt.",
|
||||
"Profile": "Profiel",
|
||||
@@ -411,7 +437,9 @@
|
||||
"Recipes_In_Import": "Recepten in je importbestand",
|
||||
"Recipes_per_page": "Recepten per pagina",
|
||||
"Remove": "Verwijder",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Verwijder {food} van je boodschappenlijst",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Verwijder voedingswaarde van recept",
|
||||
"Reset": "Herstel",
|
||||
"ResetHelp": "Hulp herstellen",
|
||||
@@ -445,6 +473,7 @@
|
||||
"Servings": "Porties",
|
||||
"ServingsText": "Portie tekst",
|
||||
"Settings": "Instellingen",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Deel",
|
||||
"ShopLater": "Later boodschappen doen",
|
||||
"ShopNow": "Nu boodschappen doen",
|
||||
@@ -473,6 +502,7 @@
|
||||
"Source": "Bron",
|
||||
"SourceImportHelp": "Importeer JSON in schema.org/recipe-formaat of html-pagina’s met json+ld-recepten of microdata.",
|
||||
"SourceImportSubtitle": "Importeer handmatig JSON of HTML.",
|
||||
"Space": "",
|
||||
"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.",
|
||||
@@ -575,6 +605,7 @@
|
||||
"ViewLogHelp": "Geschiedenis van bekeken recepten. ",
|
||||
"View_Recipes": "Bekijk Recepten",
|
||||
"Viewed": "Bekeken",
|
||||
"Visibility": "",
|
||||
"Waiting": "Wachten",
|
||||
"WaitingTime": "Wachttijd",
|
||||
"WarnPageLeave": "Er zijn niet-opgeslagen wijzigingen die verloren zullen gaan. Pagina toch verlaten?",
|
||||
@@ -588,6 +619,7 @@
|
||||
"Welcome": "Welkom",
|
||||
"WorkingTime": "Bereidingstijd",
|
||||
"Year": "Jaar",
|
||||
"Yes": "",
|
||||
"YourSpaces": "Jouw ruimtes",
|
||||
"active": "actief",
|
||||
"add_keyword": "Voeg trefwoord toe",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -9,6 +10,7 @@
|
||||
"Activity": "Aktywność",
|
||||
"Add": "Dodaj",
|
||||
"AddAll": "Dodaj wszystkie",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Dodaj filtr",
|
||||
"AddFoodToShopping": "Dodaj {food} do swojej listy zakupów",
|
||||
"AddMany": "Dodaj wiele",
|
||||
@@ -24,6 +26,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ść",
|
||||
@@ -43,6 +51,10 @@
|
||||
"BaseUnit": "Podstawowa jednostka",
|
||||
"BaseUnitHelp": "Standardowa jednostka dla automatyczne konwersji jednostek",
|
||||
"Basics": "Podstawy",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Książka",
|
||||
"Bookmarklet": "Skryptozakładka",
|
||||
"BookmarkletHelp1": "Przeciągnij następujący przycisk do twojego paska zakładek",
|
||||
@@ -78,6 +90,7 @@
|
||||
"Confirm": "Potwierdź",
|
||||
"Continue": "Kontynuuj",
|
||||
"Conversion": "Konwersja",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopiuj",
|
||||
"Copy Link": "Skopiuj link",
|
||||
"Copy Token": "Kopiuj Token",
|
||||
@@ -95,6 +108,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.",
|
||||
@@ -116,6 +130,7 @@
|
||||
"DelayUntil": "Opóźnij do",
|
||||
"Delete": "Usuń",
|
||||
"DeleteShoppingConfirm": "Czy na pewno chcesz usunąć wszystkie {food} z listy zakupów?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Usuń wszystko",
|
||||
"Delete_Food": "Usuń żywność",
|
||||
"Delete_Keyword": "Usuń słowo kluczowe",
|
||||
@@ -125,6 +140,7 @@
|
||||
"Disable_Amount": "Wyłącz ilość",
|
||||
"Disabled": "Wyłączone",
|
||||
"Documentation": "Dokumentacja",
|
||||
"DontChange": "",
|
||||
"Download": "Pobieranie",
|
||||
"Drag_Here_To_Delete": "Przeciągnij tutaj, aby usunąć",
|
||||
"Edit": "Edytuj",
|
||||
@@ -162,16 +178,20 @@
|
||||
"Food_Alias": "Alias żywności",
|
||||
"Food_Replace": "Zastąp produkt",
|
||||
"Foods": "Żywność",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Grupuj według",
|
||||
"Hide_Food": "Ukryj żywność",
|
||||
"Hide_Keyword": "Ukryj słowa kluczowe",
|
||||
"Hide_Keywords": "Ukryj słowo kluczowe",
|
||||
"Hide_Recipes": "Ukryj przepisy",
|
||||
"Hide_as_header": "Ukryj jako nagłówek",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Godzina",
|
||||
"Hours": "Godziny",
|
||||
"Icon": "Ikona",
|
||||
@@ -215,11 +235,14 @@
|
||||
"Learn_More": "Dowiedz się więcej",
|
||||
"Link": "Link",
|
||||
"Load_More": "Załaduj więcej",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zanotuj ugotowanie",
|
||||
"Log_Recipe_Cooking": "Zaloguj gotowanie przepisu",
|
||||
"Logo": "Logo",
|
||||
"Make_Header": "Utwórz nagłówek",
|
||||
"Make_Ingredient": "Utwórz składnik",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Zarządzaj książkami",
|
||||
"Manage_Emails": "Zarządzaj e-mailami",
|
||||
"Meal_Plan": "Plan posiłków",
|
||||
@@ -228,10 +251,13 @@
|
||||
"Meal_Type_Required": "Rodzaj posiłku jest wymagany",
|
||||
"Meal_Types": "Rodzaje posiłków",
|
||||
"Merge": "Scal",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Scal słowa kluczowe",
|
||||
"Message": "Wiadomość",
|
||||
"MissingProperties": "",
|
||||
"Month": "Miesiąc",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Przenieś",
|
||||
"MoveCategory": "Przenieś do: ",
|
||||
"Move_Down": "Przesunąć w dół",
|
||||
@@ -258,6 +284,7 @@
|
||||
"New_Unit": "Nowa jednostka",
|
||||
"Next_Day": "Następny dzień",
|
||||
"Next_Period": "Następny okres",
|
||||
"No": "",
|
||||
"NoCategory": "Nie wybrano kategorii.",
|
||||
"NoMoreUndo": "Brak zmian do wycofania.",
|
||||
"NoUnit": "",
|
||||
@@ -298,6 +325,7 @@
|
||||
"Previous_Day": "Poprzedni dzień",
|
||||
"Previous_Period": "Poprzedni okres",
|
||||
"Print": "Drukuj",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Prywatny przepis",
|
||||
"Private_Recipe_Help": "Przepis jest widoczny tylko dla Ciebie i dla osób, którym jest udostępniany.",
|
||||
"Properties": "Właściwości",
|
||||
@@ -319,7 +347,9 @@
|
||||
"Recipes": "Przepisy",
|
||||
"Recipes_In_Import": "Przepisy w pliku importu",
|
||||
"Recipes_per_page": "Przepisy na stronę",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Usuń {food} z listy zakupów",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Usuń wartości odżywcze z przepisu",
|
||||
"Reset": "Resetowanie",
|
||||
"Reset_Search": "Resetuj wyszukiwanie",
|
||||
@@ -340,6 +370,7 @@
|
||||
"Selected": "Wybrane",
|
||||
"Servings": "Porcje",
|
||||
"Settings": "Ustawienia",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Udostępnij",
|
||||
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
|
||||
"Shopping_Categories": "Kategorie zakupów",
|
||||
@@ -358,6 +389,7 @@
|
||||
"Size": "Rozmiar",
|
||||
"Social_Authentication": "Uwierzytelnianie społecznościowe",
|
||||
"Sort_by_new": "Sortuj według nowych",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -416,6 +448,7 @@
|
||||
"Valid Until": "Ważne do",
|
||||
"View": "Pogląd",
|
||||
"View_Recipes": "Przeglądaj przepisy",
|
||||
"Visibility": "",
|
||||
"Waiting": "Oczekiwanie",
|
||||
"Warning": "Ostrzeżenie",
|
||||
"Warning_Delete_Supermarket_Category": "Usunięcie kategorii supermarketu spowoduje również usunięcie wszystkich relacji z żywnością. Jesteś pewny?",
|
||||
@@ -424,6 +457,7 @@
|
||||
"Week_Numbers": "Numery tygodni",
|
||||
"Welcome": "Witamy",
|
||||
"Year": "Rok",
|
||||
"Yes": "",
|
||||
"add_keyword": "Dodaj słowo kluczowe",
|
||||
"additional_options": "Opcje dodatkowe",
|
||||
"advanced": "Zaawansowany",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Add": "Adicionar",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Adicionar {food} à sua lista de compras",
|
||||
"AddToShopping": "Adicionar á lista de compras",
|
||||
"Add_Servings_to_Shopping": "Adicionar {servings} doses ás compras",
|
||||
@@ -13,6 +15,12 @@
|
||||
"Added_by": "Adicionado por",
|
||||
"Added_on": "Adicionado a",
|
||||
"Advanced": "Avançado",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alinhamento",
|
||||
"Amount": "Quantidade",
|
||||
"Apply": "",
|
||||
@@ -21,6 +29,10 @@
|
||||
"Auto_Sort_Help": "Mover todos os ingredientes para o passo mais indicado.",
|
||||
"Automate": "Automatizar",
|
||||
"Automation": "Automação",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Books": "Livros",
|
||||
"CREATE_ERROR": "",
|
||||
"Calculator": "Calculadora",
|
||||
@@ -41,6 +53,7 @@
|
||||
"Coming_Soon": "",
|
||||
"Completed": "Completo",
|
||||
"Conversion": "Conversão",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copiar",
|
||||
"Copy Link": "Copiar Ligação",
|
||||
"Copy Token": "Copiar Chave",
|
||||
@@ -55,6 +68,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.",
|
||||
@@ -70,12 +84,14 @@
|
||||
"DelayUntil": "",
|
||||
"Delete": "Apagar",
|
||||
"DeleteShoppingConfirm": "Tem a certeza que pretende remover toda {food} da sua lista de compras?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Apagar todos",
|
||||
"Delete_Food": "Eliminar comida",
|
||||
"Delete_Keyword": "Eliminar Palavra Chave",
|
||||
"Description": "Descrição",
|
||||
"Description_Replace": "Substituir descrição",
|
||||
"Disable_Amount": "Desativar quantidade",
|
||||
"DontChange": "",
|
||||
"Download": "Transferência",
|
||||
"Drag_Here_To_Delete": "Arraste para aqui para eliminar",
|
||||
"Edit": "Editar",
|
||||
@@ -107,16 +123,20 @@
|
||||
"FoodOnHand": "Tem {food} disponível.",
|
||||
"Food_Alias": "Alcunha da comida",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Agrupar por",
|
||||
"Hide_Food": "Esconder comida",
|
||||
"Hide_Keyword": "",
|
||||
"Hide_Keywords": "Esconder palavra-chave",
|
||||
"Hide_Recipes": "Esconder Receitas",
|
||||
"Hide_as_header": "Esconder como cabeçalho",
|
||||
"Hierarchy": "",
|
||||
"Icon": "Ícone",
|
||||
"IgnoreAccents": "",
|
||||
"IgnoreAccentsHelp": "",
|
||||
@@ -147,10 +167,13 @@
|
||||
"Learn_More": "Aprenda mais",
|
||||
"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",
|
||||
"Make_Ingredient": "Fazer ingrediente",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Gerenciar Livros",
|
||||
"Meal_Plan": "Plano de Refeição",
|
||||
"Meal_Plan_Days": "Planos de alimentação futuros",
|
||||
@@ -158,9 +181,12 @@
|
||||
"Meal_Type_Required": "Tipo de refeição é necessário",
|
||||
"Meal_Types": "Tipos de refeições",
|
||||
"Merge": "Juntar",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Unir palavra-chave",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mês",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover para: ",
|
||||
"Move_Down": "Mover para baixo",
|
||||
@@ -178,6 +204,7 @@
|
||||
"New_Unit": "Nova Unidade",
|
||||
"Next_Day": "Dia seguinte",
|
||||
"Next_Period": "Próximo período",
|
||||
"No": "",
|
||||
"NoCategory": "Nenhuma categoria selecionada.",
|
||||
"NoMoreUndo": "Nenhuma alteração para ser desfeita.",
|
||||
"NoUnit": "",
|
||||
@@ -215,6 +242,7 @@
|
||||
"Previous_Day": "Dia anterior",
|
||||
"Previous_Period": "Período anterior",
|
||||
"Print": "Imprimir",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Receita Privada",
|
||||
"Private_Recipe_Help": "A receita só é mostrada ás pessoas com que foi partilhada.",
|
||||
"Properties": "Propriedades",
|
||||
@@ -235,7 +263,9 @@
|
||||
"Recipe_Image": "Imagem da Receita",
|
||||
"Recipes": "Receitas",
|
||||
"Recipes_per_page": "Receitas por página",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Remover {food} da sua lista de compras",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Remover valor nutricional da receita",
|
||||
"Reset": "Reiniciar",
|
||||
"Reset_Search": "Repor Pesquisa",
|
||||
@@ -253,6 +283,7 @@
|
||||
"Selected": "Selecionado",
|
||||
"Servings": "Doses",
|
||||
"Settings": "Definições",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Partilhar",
|
||||
"Shopping_Categories": "Categorias de Compras",
|
||||
"Shopping_Category": "Categoria de Compras",
|
||||
@@ -265,6 +296,7 @@
|
||||
"Show_as_header": "Mostrar como cabeçalho",
|
||||
"Size": "Tamanho",
|
||||
"Sort_by_new": "Ordenar por mais recente",
|
||||
"Space": "",
|
||||
"StartDate": "Data de início",
|
||||
"Starting_Day": "Dia de início da semana",
|
||||
"StartsWith": "",
|
||||
@@ -308,12 +340,14 @@
|
||||
"User": "Utilizador",
|
||||
"View": "Vista",
|
||||
"View_Recipes": "Ver Receitas",
|
||||
"Visibility": "",
|
||||
"Waiting": "Em espera",
|
||||
"Warning": "Aviso",
|
||||
"Week": "Semana",
|
||||
"Week_Numbers": "Números das semanas",
|
||||
"Welcome": "Bem-vindo",
|
||||
"Year": "Ano",
|
||||
"Yes": "",
|
||||
"add_keyword": "Adicionar Palavra Chave",
|
||||
"advanced": "",
|
||||
"advanced_search_settings": "Configurações Avançadas de Pesquisa",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Use IA para importar imagens das receitas.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Atividade",
|
||||
"Add": "Adicionar",
|
||||
"AddAll": "Adicionar todos",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Adicionar Filtro",
|
||||
"AddFoodToShopping": "Incluir {food} na sua lista de compras",
|
||||
"AddMany": "Adicionar muitos",
|
||||
@@ -25,6 +27,12 @@
|
||||
"Added_on": "Incluído Em",
|
||||
"Admin": "Administrador",
|
||||
"Advanced": "Avançado",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alinhamento",
|
||||
"AllRecipes": "Todas Receitas",
|
||||
"Amount": "Quantidade",
|
||||
@@ -44,6 +52,10 @@
|
||||
"BaseUnit": "Unidade Base",
|
||||
"BaseUnitHelp": "Unidade padrão para conversão de unidades",
|
||||
"Basics": "Básicos",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Livro",
|
||||
"Bookmarklet": "Marcador",
|
||||
"BookmarkletHelp1": "Arraste o seguinte botão para sua barra de favoritos",
|
||||
@@ -82,6 +94,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",
|
||||
@@ -104,6 +117,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",
|
||||
@@ -130,6 +144,7 @@
|
||||
"Delete": "Deletar",
|
||||
"DeleteConfirmQuestion": "Tem certeza que quer excluir esse objeto?",
|
||||
"DeleteShoppingConfirm": "Tem certeza que deseja remover todas {food} de sua lista de compras?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Excluir tudo",
|
||||
"Delete_Food": "Deletar Comida",
|
||||
"Delete_Keyword": "Deletar palavra-chave",
|
||||
@@ -142,6 +157,7 @@
|
||||
"Disable_Amount": "Desabilitar Quantidade",
|
||||
"Disabled": "Desabilitado",
|
||||
"Documentation": "Documentação",
|
||||
"DontChange": "",
|
||||
"Down": "Abaixo",
|
||||
"Download": "Baixar",
|
||||
"DragToUpload": "Clique e arraste ou clique para selecionar",
|
||||
@@ -195,11 +211,14 @@
|
||||
"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": "",
|
||||
"GroupBy": "Agrupar Por",
|
||||
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
|
||||
"Headline": "Título",
|
||||
@@ -210,6 +229,7 @@
|
||||
"Hide_Keywords": "Esconder palavra-chave",
|
||||
"Hide_Recipes": "Esconder Receitas",
|
||||
"Hide_as_header": "Esconder cabeçalho",
|
||||
"Hierarchy": "",
|
||||
"History": "Histórico",
|
||||
"HostedFreeVersion": "Você está utilizando a versão gratuita do Tandoor",
|
||||
"Hour": "Hora",
|
||||
@@ -266,6 +286,8 @@
|
||||
"Link": "Link",
|
||||
"Load": "Carregar",
|
||||
"Load_More": "Carregar mais",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registro de Cozinha",
|
||||
"Log_Recipe_Cooking": "Registrar receitas feitas",
|
||||
"Logo": "Logotipo",
|
||||
@@ -283,10 +305,13 @@
|
||||
"Meal_Type_Required": "Tipo de comida é obrigatório",
|
||||
"Meal_Types": "Tipos de Comida",
|
||||
"Merge": "Mesclar",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Mesclar palavra-chave",
|
||||
"Message": "Mensagem",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mês",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover Para: ",
|
||||
"Move_Down": "Mover para baixo",
|
||||
@@ -313,6 +338,7 @@
|
||||
"New_Unit": "Nova Unidade",
|
||||
"Next_Day": "Próximo Dia",
|
||||
"Next_Period": "Próximo Período",
|
||||
"No": "",
|
||||
"NoCategory": "Nenhuma categoria selecionada.",
|
||||
"NoMoreUndo": "Nenhuma alteração para desfazer.",
|
||||
"NoUnit": "",
|
||||
@@ -350,6 +376,7 @@
|
||||
"Previous_Day": "Dia Anterior",
|
||||
"Previous_Period": "Período Anterior",
|
||||
"Print": "Imprimir",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Receita privada",
|
||||
"Private_Recipe_Help": "Receita é visível somente para você e para pessoas compartilhadas.",
|
||||
"Properties": "Propriedades",
|
||||
@@ -371,7 +398,9 @@
|
||||
"Recipes": "Receitas",
|
||||
"Recipes_In_Import": "Receitas no seu arquivo de importação",
|
||||
"Recipes_per_page": "Receitas por página",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Remover {food} da sua lista de compras",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Deletar dados nutricionais da receita",
|
||||
"Reset": "Reiniciar",
|
||||
"Reset_Search": "Resetar Busca",
|
||||
@@ -389,6 +418,7 @@
|
||||
"Selected": "Selecionado",
|
||||
"Servings": "Porções",
|
||||
"Settings": "Configurações",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartilhar",
|
||||
"ShoppingBackgroundSyncWarning": "Rede ruim, aguardando sincronização...",
|
||||
"Shopping_Categories": "Categorias de Mercado",
|
||||
@@ -407,6 +437,7 @@
|
||||
"Size": "Tamanho",
|
||||
"Social_Authentication": "Autenticação social",
|
||||
"Sort_by_new": "Ordenar por novos",
|
||||
"Space": "",
|
||||
"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",
|
||||
@@ -461,6 +492,7 @@
|
||||
"Valid Until": "Válido Até",
|
||||
"View": "Visualizar",
|
||||
"View_Recipes": "Ver Receitas",
|
||||
"Visibility": "",
|
||||
"Waiting": "Espera",
|
||||
"Warning": "Alerta",
|
||||
"Warning_Delete_Supermarket_Category": "Excluir uma categoria de supermercado também excluirá todas as relações com alimentos. Tem certeza?",
|
||||
@@ -469,6 +501,7 @@
|
||||
"Week_Numbers": "Números da Semana",
|
||||
"Welcome": "Bem vindo",
|
||||
"Year": "Ano",
|
||||
"Yes": "",
|
||||
"add_keyword": "Incluir Palavra-Chave",
|
||||
"additional_options": "Opções Adicionais",
|
||||
"advanced": "Avançado",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Cont",
|
||||
"Add": "Adaugă",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Adăugă {food} în lista de cumpărături",
|
||||
"AddToShopping": "Adaugă la lista de cumpărături",
|
||||
"Add_Servings_to_Shopping": "Adăugă {servings} porții la cumpărături",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_on": "Adăugat la",
|
||||
"Advanced": "Avansat",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Amount": "Cantitate",
|
||||
"App": "Aplicație",
|
||||
"Apply": "",
|
||||
@@ -25,6 +33,10 @@
|
||||
"Auto_Sort_Help": "Mutați toate ingredientele la cel mai potrivit pas.",
|
||||
"Automate": "Automatizat",
|
||||
"Automation": "Automatizare",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Bookmarklet": "Marcaj",
|
||||
"Books": "Cărți",
|
||||
"CREATE_ERROR": "",
|
||||
@@ -48,6 +60,7 @@
|
||||
"Coming_Soon": "În curând",
|
||||
"Comments_setting": "Afișează comentarii",
|
||||
"Completed": "Completat",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copie",
|
||||
"Copy Link": "Copiere link",
|
||||
"Copy Token": "Copiere token",
|
||||
@@ -64,6 +77,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": "",
|
||||
@@ -76,6 +90,7 @@
|
||||
"DelayUntil": "Amână până la",
|
||||
"Delete": "Șterge",
|
||||
"DeleteShoppingConfirm": "Sunteți sigur că doriți să eliminați toate {food} din lista de cumpărături?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_Food": "Ștergere mâncare",
|
||||
"Delete_Keyword": "Ștergere cuvânt cheie",
|
||||
"Description": "Descriere",
|
||||
@@ -84,6 +99,7 @@
|
||||
"Disable_Amount": "Dezactivare cantitate",
|
||||
"Disabled": "Dezactivat",
|
||||
"Documentation": "Documentație",
|
||||
"DontChange": "",
|
||||
"Download": "Descarcă",
|
||||
"Drag_Here_To_Delete": "Mută aici pentru a șterge",
|
||||
"Edit": "Editează",
|
||||
@@ -114,16 +130,20 @@
|
||||
"FoodOnHand": "Aveți {food} la îndemână.",
|
||||
"Food_Alias": "Pseudonim mâncare",
|
||||
"Foods": "Alimente",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Grupat de",
|
||||
"Hide_Food": "Ascunde mâncare",
|
||||
"Hide_Keyword": "Ascunde cuvintele cheie",
|
||||
"Hide_Keywords": "Ascunde cuvânt cheie",
|
||||
"Hide_Recipes": "Ascunde rețetele",
|
||||
"Hide_as_header": "Ascunderea ca antet",
|
||||
"Hierarchy": "",
|
||||
"Hour": "Oră",
|
||||
"Hours": "Ore",
|
||||
"Icon": "Iconiță",
|
||||
@@ -165,10 +185,13 @@
|
||||
"Last_name": "Nume de familie",
|
||||
"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",
|
||||
"Make_Ingredient": "Create ingredient",
|
||||
"ManageSubscription": "",
|
||||
"Manage_Books": "Gestionarea cărților",
|
||||
"Manage_Emails": "Gestionarea e-mailurilor",
|
||||
"Meal_Plan": "Plan de alimentare",
|
||||
@@ -177,10 +200,13 @@
|
||||
"Meal_Type_Required": "Tipul mesei este necesar",
|
||||
"Meal_Types": "Tipuri de mese",
|
||||
"Merge": "Unire",
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Unește cuvânt cheie",
|
||||
"Message": "Mesaj",
|
||||
"MissingProperties": "",
|
||||
"Month": "Lună",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mută",
|
||||
"MoveCategory": "Mută la: ",
|
||||
"Move_Down": "Deplasați-vă în jos",
|
||||
@@ -203,6 +229,7 @@
|
||||
"New_Unit": "Unitate nouă",
|
||||
"Next_Day": "Ziua următoare",
|
||||
"Next_Period": "Perioada următoare",
|
||||
"No": "",
|
||||
"NoCategory": "Nicio categorie selectată.",
|
||||
"NoUnit": "",
|
||||
"No_ID": "ID-ul nu a fost găsit, nu se poate șterge.",
|
||||
@@ -238,6 +265,7 @@
|
||||
"Previous_Day": "Ziua precedentă",
|
||||
"Previous_Period": "Perioada precedentă",
|
||||
"Print": "Tipărește",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Rețetă privată",
|
||||
"Private_Recipe_Help": "Rețeta este arătată doar ție și oamenilor cu care este împărtășită.",
|
||||
"Protected": "Protejat",
|
||||
@@ -254,7 +282,9 @@
|
||||
"Recipes": "Rețete",
|
||||
"Recipes_In_Import": "Rețete în fișierul de import",
|
||||
"Recipes_per_page": "Rețete pe pagină",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Șterge {food} din lista de cumpărături",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Ștergere a nutriției din rețetă",
|
||||
"Reset": "Resetare",
|
||||
"Reset_Search": "Resetarea căutării",
|
||||
@@ -275,6 +305,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",
|
||||
@@ -288,6 +319,7 @@
|
||||
"Size": "Marime",
|
||||
"Social_Authentication": "Autentificare socială",
|
||||
"Sort_by_new": "Sortare după nou",
|
||||
"Space": "",
|
||||
"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": "",
|
||||
@@ -337,6 +369,7 @@
|
||||
"Valid Until": "Valabil până la",
|
||||
"View": "Vizualizare",
|
||||
"View_Recipes": "Vizionare rețete",
|
||||
"Visibility": "",
|
||||
"Waiting": "Așteptare",
|
||||
"Warning": "Atenționare",
|
||||
"Warning_Delete_Supermarket_Category": "Ștergerea unei categorii de supermarketuri va șterge, de asemenea, toate relațiile cu alimentele. Sunteți sigur?",
|
||||
@@ -344,6 +377,7 @@
|
||||
"Week": "Săptămână",
|
||||
"Week_Numbers": "Numerele săptămânii",
|
||||
"Year": "An",
|
||||
"Yes": "",
|
||||
"add_keyword": "Adăugare cuvânt cheie",
|
||||
"additional_options": "Opțiuni suplimentare",
|
||||
"advanced": "Avansat",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Используй AI для импорта изображений рецептов.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Активность",
|
||||
"Add": "Добавить",
|
||||
"AddAll": "Добавить все",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Добавить фильтр",
|
||||
"AddFoodToShopping": "Добавить {food} в ваш список покупок",
|
||||
"AddMany": "Добавить несколько",
|
||||
@@ -26,6 +28,12 @@
|
||||
"Admin": "Админ",
|
||||
"Advanced": "Расширенный",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Выравнивание",
|
||||
"AllRecipes": "Все рецепты",
|
||||
"Amount": "Количество",
|
||||
@@ -45,6 +53,10 @@
|
||||
"BaseUnit": "Базовая единица измерения",
|
||||
"BaseUnitHelp": "Стандартная единица для автоконвертации",
|
||||
"Basics": "Основные понятия",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Книга",
|
||||
"Bookmarklet": "Букмарклет",
|
||||
"BookmarkletHelp1": "Перетащите эту кнопку в панель закладок",
|
||||
@@ -83,6 +95,7 @@
|
||||
"Continue": "Продолжить",
|
||||
"Conversion": "Преобразование",
|
||||
"ConversionsHelp": "С помощью преобразований вы можете рассчитывать количество продукта в разных единицах измерения. В настоящее время это используется только для расчёта свойств, но в будущем может применяться и в других частях Tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Журнал приготовления",
|
||||
"CookLogHelp": "История приготовлений по рецептам. ",
|
||||
"Cooked": "Приготовлено",
|
||||
@@ -105,6 +118,7 @@
|
||||
"Create_New_Unit": "Добавить единицу измерения",
|
||||
"Created": "Создано",
|
||||
"CreatedBy": "Создано пользователем",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Текущий период",
|
||||
"Custom Filter": "Пользовательский фильтр",
|
||||
@@ -131,6 +145,7 @@
|
||||
"Delete": "Удалить",
|
||||
"DeleteConfirmQuestion": "Вы уверены, что хотите удалить этот объект?",
|
||||
"DeleteShoppingConfirm": "Вы уверены, что хотите удалить все {food} из вашего списка покупок?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Удалить всё",
|
||||
"Delete_Food": "Удалить элемент",
|
||||
"Delete_Keyword": "Удалить ключевое слово",
|
||||
@@ -143,6 +158,7 @@
|
||||
"Disable_Amount": "Деактивировать количество",
|
||||
"Disabled": "Отключено",
|
||||
"Documentation": "Документация",
|
||||
"DontChange": "",
|
||||
"Down": "Вниз",
|
||||
"Download": "Загрузить",
|
||||
"DragToUpload": "Перетащите сюда или нажмите для выбора",
|
||||
@@ -196,11 +212,14 @@
|
||||
"Food_Replace": "Замена продукта",
|
||||
"Foods": "Продукты",
|
||||
"Friday": "Пятница",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Полнотекстовый",
|
||||
"FulltextHelp": "Поля, используемые в полнотекстовом поиске. Важно: методы поиска web, phrase и raw применимы только к полнотекстовым полям.",
|
||||
"Fuzzy": "Нечёткий",
|
||||
"FuzzySearchHelp": "Нечёткий поиск позволяет находить записи, даже если в написании есть ошибки или отличия.",
|
||||
"GettingStarted": "Начало работы",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"GroupBy": "Сгруппировать по",
|
||||
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
|
||||
"Headline": "Заголовок",
|
||||
@@ -211,6 +230,7 @@
|
||||
"Hide_Keywords": "Скрыть ключевое слово",
|
||||
"Hide_Recipes": "Скрыть рецепт",
|
||||
"Hide_as_header": "Скрыть заголовок",
|
||||
"Hierarchy": "",
|
||||
"History": "История",
|
||||
"HostedFreeVersion": "Текущая версия: бесплатная",
|
||||
"Hour": "Час",
|
||||
@@ -267,6 +287,8 @@
|
||||
"Link": "Гиперссылка",
|
||||
"Load": "Загрузить",
|
||||
"Load_More": "Загрузить еще",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Журнал приготовления",
|
||||
"Log_Recipe_Cooking": "Журнал приготовления",
|
||||
"Logo": "Логотип",
|
||||
@@ -294,6 +316,8 @@
|
||||
"ModelSelectResultsHelp": "Показать больше результатов",
|
||||
"Monday": "Понедельник",
|
||||
"Month": "Месяц",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Ещё",
|
||||
"Move": "Переместить",
|
||||
"MoveCategory": "Переместить в: ",
|
||||
@@ -323,6 +347,7 @@
|
||||
"Next": "Следующий",
|
||||
"Next_Day": "Следующий день",
|
||||
"Next_Period": "Следующий период",
|
||||
"No": "",
|
||||
"NoCategory": "Категория не выбрана.",
|
||||
"NoMoreUndo": "Нет изменений, которые можно было бы отменить.",
|
||||
"No_ID": "ID не найден, удаление не возможно.",
|
||||
@@ -374,6 +399,7 @@
|
||||
"Previous_Day": "Предыдущий день",
|
||||
"Previous_Period": "Предыдущий период",
|
||||
"Print": "Распечатать",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Приватный Рецепт",
|
||||
"Private_Recipe_Help": "Рецепт виден только вам и людям, с которыми им поделились.",
|
||||
"Profile": "Профиль",
|
||||
@@ -408,7 +434,9 @@
|
||||
"Recipes_In_Import": "Рецепты в вашем файле импорта",
|
||||
"Recipes_per_page": "Рецептов на странице",
|
||||
"Remove": "Удалить",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Удалить {food} из вашего списка покупок",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Уберите питательные вещества из рецепта",
|
||||
"Reset": "Сбросить",
|
||||
"ResetHelp": "Сбросить подсказки",
|
||||
@@ -442,6 +470,7 @@
|
||||
"Servings": "Порции",
|
||||
"ServingsText": "Описание порций",
|
||||
"Settings": "Настройки",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Поделиться",
|
||||
"ShopLater": "Купить позже",
|
||||
"ShopNow": "Купить сейчас",
|
||||
@@ -470,6 +499,7 @@
|
||||
"Source": "Источник",
|
||||
"SourceImportHelp": "Импортируйте JSON в формате schema.org/recipe или HTML-страницы с рецептами в формате JSON-LD или микроданных.",
|
||||
"SourceImportSubtitle": "Импортировать JSON или HTML вручную.",
|
||||
"Space": "",
|
||||
"SpaceLimitExceeded": "Ваше пространство превысило один из лимитов, некоторые функции могут быть ограничены.",
|
||||
"SpaceLimitReached": "В этом пространстве достигнут лимит. Новые объекты данного типа создавать нельзя.",
|
||||
"SpaceMemberHelp": "Для добавления пользователей создайте пригласительную ссылку и передайте её человеку, которого хотите пригласить.",
|
||||
@@ -572,6 +602,7 @@
|
||||
"ViewLogHelp": "История просмотренных рецептов. ",
|
||||
"View_Recipes": "Просмотр рецепта",
|
||||
"Viewed": "Просмотрено",
|
||||
"Visibility": "",
|
||||
"Waiting": "Ожидание",
|
||||
"WaitingTime": "Время ожидания",
|
||||
"WarnPageLeave": "Есть несохраненные изменения, которые будут потеряны. Всё равно покинуть страницу?",
|
||||
@@ -585,6 +616,7 @@
|
||||
"Welcome": "Добро пожаловать",
|
||||
"WorkingTime": "Время работы",
|
||||
"Year": "Год",
|
||||
"Yes": "",
|
||||
"YourSpaces": "Ваши пространства",
|
||||
"active": "активно",
|
||||
"add_keyword": "Добавить ключевое слово",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "Umetna inteligenca",
|
||||
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -11,6 +12,7 @@
|
||||
"Activity": "Aktivnost",
|
||||
"Add": "Dodaj",
|
||||
"AddAll": "Dodaj vse",
|
||||
"AddChild": "",
|
||||
"AddFilter": "Dodaj filter",
|
||||
"AddFoodToShopping": "Dodaj {food} v nakupovalni listek",
|
||||
"AddMany": "Dodaj veliko",
|
||||
@@ -26,6 +28,12 @@
|
||||
"Admin": "Skrbnik",
|
||||
"Advanced": "Napredno",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Poravnava",
|
||||
"AllRecipes": "Vsi recepti",
|
||||
"Amount": "Količina",
|
||||
@@ -45,6 +53,10 @@
|
||||
"BaseUnit": "Osnovna enota",
|
||||
"BaseUnitHelp": "Standardna enota za samodejno pretvorbo enot",
|
||||
"Basics": "Osnove",
|
||||
"BatchDeleteConfirm": "",
|
||||
"BatchDeleteHelp": "",
|
||||
"BatchEdit": "",
|
||||
"BatchEditUpdatingItemsCount": "",
|
||||
"Book": "Knjiga",
|
||||
"Bookmarklet": "Zaznamek",
|
||||
"BookmarkletHelp1": "Povlecite naslednji gumb v vrstico z zaznamki",
|
||||
@@ -83,6 +95,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",
|
||||
@@ -105,6 +118,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",
|
||||
@@ -131,6 +145,7 @@
|
||||
"Delete": "Izbriši",
|
||||
"DeleteConfirmQuestion": "Ali ste prepričani, da želite izbrisati ta objekt?",
|
||||
"DeleteShoppingConfirm": "Si prepričan/a, da želiš odstraniti VSO {food} iz nakupovalnega listka?",
|
||||
"DeleteSomething": "",
|
||||
"Delete_All": "Izbriši vse",
|
||||
"Delete_Food": "Izbriši hrano",
|
||||
"Delete_Keyword": "Izbriši ključno besedo",
|
||||
@@ -143,6 +158,7 @@
|
||||
"Disable_Amount": "Onemogoči količino",
|
||||
"Disabled": "Onemogočeno",
|
||||
"Documentation": "Dokumentacija",
|
||||
"DontChange": "",
|
||||
"Down": "Navzdol",
|
||||
"Download": "Prenesi",
|
||||
"DragToUpload": "Povlecite in spustite ali kliknite za izbiro",
|
||||
@@ -196,11 +212,14 @@
|
||||
"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": "",
|
||||
"GroupBy": "Združi po",
|
||||
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
|
||||
"Headline": "Glavni naslov",
|
||||
@@ -211,6 +230,7 @@
|
||||
"Hide_Keywords": "Skrij ključno besedo",
|
||||
"Hide_Recipes": "Skrij recept",
|
||||
"Hide_as_header": "Skrij kot glavo",
|
||||
"Hierarchy": "",
|
||||
"History": "Zgodovina",
|
||||
"HostedFreeVersion": "Uporabljate brezplačno različico Tandoorja",
|
||||
"Hour": "Ura",
|
||||
@@ -267,6 +287,8 @@
|
||||
"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",
|
||||
@@ -295,6 +317,8 @@
|
||||
"ModelSelectResultsHelp": "Išči več rezultatov",
|
||||
"Monday": "Ponedeljek",
|
||||
"Month": "Mesec",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Več",
|
||||
"Move": "Premakni",
|
||||
"MoveCategory": "Premakni v: ",
|
||||
@@ -324,6 +348,7 @@
|
||||
"Next": "Naprej",
|
||||
"Next_Day": "Naslednji Dan",
|
||||
"Next_Period": "Naslednje obdobje",
|
||||
"No": "",
|
||||
"NoCategory": "Brez kategorije",
|
||||
"NoMoreUndo": "Ni sprememb, ki bi jih bilo mogoče razveljaviti.",
|
||||
"NoUnit": "Brez enote",
|
||||
@@ -376,6 +401,7 @@
|
||||
"Previous_Day": "Prejšnji Dan",
|
||||
"Previous_Period": "Prejšnje obdobje",
|
||||
"Print": "Natisni",
|
||||
"Private": "",
|
||||
"Private_Recipe": "Zasebni Recept",
|
||||
"Private_Recipe_Help": "Recept je prikazan samo vam in osebam, s katerimi ga delite.",
|
||||
"Profile": "Profil",
|
||||
@@ -410,7 +436,9 @@
|
||||
"Recipes_In_Import": "Recepti v vaši uvozni datoteki",
|
||||
"Recipes_per_page": "Receptov na stran",
|
||||
"Remove": "Odstrani",
|
||||
"RemoveAllType": "",
|
||||
"RemoveFoodFromShopping": "Odstrani {food} iz nakupovalnega listka",
|
||||
"RemoveParent": "",
|
||||
"Remove_nutrition_recipe": "Receptu izbriši hranilno vrednost",
|
||||
"Reset": "Ponastavi",
|
||||
"ResetHelp": "Pomoč pri ponastavitvi",
|
||||
@@ -444,6 +472,7 @@
|
||||
"Servings": "Porcije",
|
||||
"ServingsText": "Besedilo o porcijah",
|
||||
"Settings": "Nastavitve",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Deli",
|
||||
"ShopLater": "Nakupujte pozneje",
|
||||
"ShopNow": "Nakupujte zdaj",
|
||||
@@ -472,6 +501,7 @@
|
||||
"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": "",
|
||||
"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.",
|
||||
@@ -574,6 +604,7 @@
|
||||
"ViewLogHelp": "Zgodovina ogledanih receptov. ",
|
||||
"View_Recipes": "Preglej recepte",
|
||||
"Viewed": "Ogledano",
|
||||
"Visibility": "",
|
||||
"Waiting": "Čakanje",
|
||||
"WaitingTime": "Čakalni čas",
|
||||
"WarnPageLeave": "Nekatere spremembe niso shranjene in bodo izgubljene. Želite vseeno zapustiti stran?",
|
||||
@@ -587,6 +618,7 @@
|
||||
"Welcome": "Dobrodošli",
|
||||
"WorkingTime": "Delovni čas",
|
||||
"Year": "Leto",
|
||||
"Yes": "",
|
||||
"YourSpaces": "Vaši prostori",
|
||||
"active": "aktiven",
|
||||
"add_keyword": "Dodaj ključno besedo",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user