mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b72897b222 | ||
|
|
bca1ebbf99 | ||
|
|
f0342d4568 | ||
|
|
81f62de500 | ||
|
|
f783949a61 | ||
|
|
820fad1b5c | ||
|
|
1169abd942 | ||
|
|
48e175f58f | ||
|
|
5450e18342 | ||
|
|
ea590f8e49 | ||
|
|
13626ca11b | ||
|
|
f53fe1e3c4 | ||
|
|
d177316b47 | ||
|
|
338db1fac2 | ||
|
|
377619473c | ||
|
|
000962c5bb | ||
|
|
9228c1d59f | ||
|
|
27007de7a0 | ||
|
|
29c99b66a1 | ||
|
|
bc179f430d | ||
|
|
58c412ad95 | ||
|
|
4f248afe76 | ||
|
|
f722d24eaa | ||
|
|
723b74509f | ||
|
|
ad4b1393dd | ||
|
|
04bab7072c | ||
|
|
6391cee9eb | ||
|
|
14884fc0d4 | ||
|
|
f2191f79dd | ||
|
|
c2533d9ea2 | ||
|
|
db72fdb1bb | ||
|
|
78252662cb | ||
|
|
4e078bf477 | ||
|
|
2e9e226fe0 | ||
|
|
18cfbd80ab | ||
|
|
4d284b4fff | ||
|
|
b1128dd134 | ||
|
|
3aebf58406 | ||
|
|
f3816a77df | ||
|
|
e4183d79ab | ||
|
|
f4aa1a083f | ||
|
|
ed5508b576 | ||
|
|
040e247487 | ||
|
|
5d28c7b17d | ||
|
|
15b2df07f2 | ||
|
|
ed8f97e9e0 | ||
|
|
034f68fc28 | ||
|
|
0158087a0b | ||
|
|
cb6bfd741d | ||
|
|
afeee5f7cb | ||
|
|
b43d6e08d4 | ||
|
|
1188624376 | ||
|
|
9ac837c969 | ||
|
|
fc4b017d30 | ||
|
|
4636ac28f9 | ||
|
|
397912e87f | ||
|
|
d0b860e623 | ||
|
|
8a90ed1274 | ||
|
|
286d707347 | ||
|
|
98d308aee9 | ||
|
|
a7c5240227 | ||
|
|
75fcff8e70 | ||
|
|
2f27cf4deb | ||
|
|
686b595f45 | ||
|
|
0f9f9e8f7c | ||
|
|
7be7c5b954 | ||
|
|
0853a9ec64 | ||
|
|
fa3daee965 | ||
|
|
e6abdf8cd4 | ||
|
|
741e9eb370 | ||
|
|
7db523d8c4 | ||
|
|
41f0060c43 | ||
|
|
5572833f64 | ||
|
|
780e441a3b | ||
|
|
c4fd2d0b4e | ||
|
|
1c6618f452 | ||
|
|
8c96a75a1e | ||
|
|
f099e2e5d3 | ||
|
|
774c05e76f | ||
|
|
b08c39e284 | ||
|
|
ae036cfa9a | ||
|
|
37628c1735 | ||
|
|
530a6db35c | ||
|
|
2930093da0 | ||
|
|
b7e63a466b |
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -12,8 +12,8 @@ jobs:
|
||||
python-version: ["3.12"]
|
||||
node-version: ["22"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1.5.1
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1.5.3
|
||||
with:
|
||||
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
|
||||
version: 1.0
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
if: github.repository_owner == 'TandoorRecipes' && ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',)
|
||||
|
||||
@@ -26,6 +26,7 @@ class ImportExportBase(forms.Form):
|
||||
PAPRIKA = 'PAPRIKA'
|
||||
NEXTCLOUD = 'NEXTCLOUD'
|
||||
MEALIE = 'MEALIE'
|
||||
MEALIE1 = 'MEALIE1'
|
||||
CHOWDOWN = 'CHOWDOWN'
|
||||
SAFFRON = 'SAFFRON'
|
||||
CHEFTAP = 'CHEFTAP'
|
||||
@@ -46,7 +47,7 @@ class ImportExportBase(forms.Form):
|
||||
PDF = 'PDF'
|
||||
GOURMET = 'GOURMET'
|
||||
|
||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'),
|
||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (MEALIE1, 'Mealie1'), (CHOWDOWN, 'Chowdown'),
|
||||
(SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'),
|
||||
(DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
|
||||
@@ -75,6 +76,11 @@ class ImportForm(ImportExportBase):
|
||||
files = MultipleFileField(required=True)
|
||||
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'),
|
||||
required=False)
|
||||
meal_plans = forms.BooleanField(required=False)
|
||||
shopping_lists = forms.BooleanField(required=False)
|
||||
nutrition_per_serving = forms.BooleanField(required=False) # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose
|
||||
|
||||
|
||||
class ExportForm(ImportExportBase):
|
||||
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
|
||||
all = forms.BooleanField(required=False)
|
||||
|
||||
83
cookbook/helper/ai_helper.py
Normal file
83
cookbook/helper/ai_helper.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum
|
||||
from litellm import CustomLogger
|
||||
|
||||
from cookbook.models import AiLog
|
||||
from recipes import settings
|
||||
|
||||
|
||||
def get_monthly_token_usage(space):
|
||||
"""
|
||||
returns the number of credits the space has used in the current month
|
||||
"""
|
||||
token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
|
||||
if token_usage is None:
|
||||
token_usage = 0
|
||||
return token_usage
|
||||
|
||||
|
||||
def has_monthly_token(space):
|
||||
"""
|
||||
checks if the monthly credit limit has been exceeded
|
||||
"""
|
||||
return get_monthly_token_usage(space) < space.ai_credits_monthly
|
||||
|
||||
|
||||
def can_perform_ai_request(space):
|
||||
return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled
|
||||
|
||||
|
||||
class AiCallbackHandler(CustomLogger):
|
||||
space = None
|
||||
user = None
|
||||
ai_provider = None
|
||||
|
||||
def __init__(self, space, user, ai_provider):
|
||||
super().__init__()
|
||||
self.space = space
|
||||
self.user = user
|
||||
self.ai_provider = ai_provider
|
||||
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
pass
|
||||
|
||||
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
|
||||
pass
|
||||
|
||||
def log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
self.create_ai_log(kwargs, response_obj, start_time, end_time)
|
||||
|
||||
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
self.create_ai_log(kwargs, response_obj, start_time, end_time)
|
||||
|
||||
def create_ai_log(self, kwargs, response_obj, start_time, end_time):
|
||||
credit_cost = 0
|
||||
credits_from_balance = False
|
||||
if self.ai_provider.log_credit_cost:
|
||||
credit_cost = kwargs.get("response_cost", 0) * 100
|
||||
|
||||
if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
|
||||
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
|
||||
if remaining_balance < 0:
|
||||
remaining_balance = 0
|
||||
if settings.HOSTED:
|
||||
self.space.ai_enabled = False
|
||||
|
||||
self.space.ai_credits_balance = remaining_balance
|
||||
credits_from_balance = True
|
||||
self.space.save()
|
||||
|
||||
AiLog.objects.create(
|
||||
created_by=self.user,
|
||||
space=self.space,
|
||||
ai_provider=self.ai_provider,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
input_tokens=response_obj['usage']['prompt_tokens'],
|
||||
output_tokens=response_obj['usage']['completion_tokens'],
|
||||
function=AiLog.F_FILE_IMPORT,
|
||||
credit_cost=credit_cost,
|
||||
credits_from_balance=credits_from_balance,
|
||||
)
|
||||
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)
|
||||
@@ -3,17 +3,19 @@ import inspect
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
|
||||
from oauth2_provider.models import AccessToken
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
|
||||
from cookbook.models import Recipe, ShareLink, UserSpace
|
||||
import random
|
||||
from cookbook.models import Recipe, ShareLink, UserSpace, Space
|
||||
|
||||
|
||||
def get_allowed_groups(groups_required):
|
||||
@@ -331,6 +333,25 @@ class CustomRecipePermission(permissions.BasePermission):
|
||||
or has_group_permission(request.user, ['user'])) and obj.space == request.space
|
||||
|
||||
|
||||
class CustomAiProviderPermission(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission class for the AiProvider api endpoint
|
||||
users: can read all
|
||||
admins: can read and write
|
||||
superusers: can read and write + write providers without a space
|
||||
"""
|
||||
message = _('You do not have the required permissions to view this page!')
|
||||
|
||||
def has_permission(self, request, view): # user is either at least a user and the request is safe
|
||||
return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser)
|
||||
|
||||
# editing of global providers allowed for superusers, space providers by admins and users can read only access
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return ((obj.space is None and request.user.is_superuser)
|
||||
or (obj.space == request.space and has_group_permission(request.user, ['admin']))
|
||||
or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS))
|
||||
|
||||
|
||||
class CustomUserPermission(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission class for user api endpoint
|
||||
@@ -437,3 +458,36 @@ class IsReadOnlyDRF(permissions.BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.method in SAFE_METHODS
|
||||
|
||||
|
||||
class IsCreateDRF(permissions.BasePermission):
|
||||
message = 'You cannot interact with this object, you can only create'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.method == 'POST'
|
||||
|
||||
|
||||
def create_space_for_user(user, name=None):
|
||||
with scopes_disabled():
|
||||
if not name:
|
||||
name = f"{user.username}'s Space"
|
||||
|
||||
if Space.objects.filter(name=name).exists():
|
||||
name = f'{name} #{random.randrange(1, 10 ** 5)}'
|
||||
|
||||
created_space = Space(name=name,
|
||||
created_by=user,
|
||||
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
|
||||
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
|
||||
max_users=settings.SPACE_DEFAULT_MAX_USERS,
|
||||
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
|
||||
ai_enabled=settings.SPACE_AI_ENABLED,
|
||||
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,
|
||||
space_setup_completed=False, )
|
||||
created_space.save()
|
||||
|
||||
UserSpace.objects.filter(user=user).update(active=False)
|
||||
user_space = UserSpace.objects.create(space=created_space, user=user, active=True)
|
||||
user_space.groups.add(Group.objects.filter(name='admin').get())
|
||||
|
||||
return user_space
|
||||
|
||||
@@ -319,10 +319,10 @@ def clean_instruction_string(instruction):
|
||||
.replace("", _('reverse rotation')) \
|
||||
.replace("", _('careful rotation')) \
|
||||
.replace("", _('knead')) \
|
||||
.replace("Andicken ", _('thicken')) \
|
||||
.replace("Erwärmen ", _('warm up')) \
|
||||
.replace("Fermentieren ", _('ferment')) \
|
||||
.replace("Sous-vide ", _("sous-vide"))
|
||||
.replace("", _('thicken')) \
|
||||
.replace("", _('warm up')) \
|
||||
.replace("", _('ferment')) \
|
||||
.replace("", _("sous-vide"))
|
||||
|
||||
|
||||
def parse_instructions(instructions):
|
||||
@@ -403,6 +403,8 @@ def parse_servings_text(servings):
|
||||
|
||||
|
||||
def parse_time(recipe_time):
|
||||
if not recipe_time:
|
||||
return 0
|
||||
if type(recipe_time) not in [int, float]:
|
||||
try:
|
||||
recipe_time = float(re.search(r'\d+', recipe_time).group())
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from psycopg2.errors import UniqueViolation
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
import random
|
||||
|
||||
from cookbook.helper.permission_helper import create_space_for_user
|
||||
from cookbook.models import Space, UserSpace
|
||||
from cookbook.views import views
|
||||
from recipes import settings
|
||||
|
||||
@@ -34,16 +41,28 @@ class ScopeMiddleware:
|
||||
if request.path.startswith(prefix + '/switch-space/'):
|
||||
return self.get_response(request)
|
||||
|
||||
with scopes_disabled():
|
||||
if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path:
|
||||
return views.space_overview(request)
|
||||
if request.path.startswith(prefix + '/invite/'):
|
||||
return self.get_response(request)
|
||||
|
||||
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
|
||||
user_space = request.user.userspace_set.filter(active=True).first()
|
||||
|
||||
if not user_space:
|
||||
return views.space_overview(request)
|
||||
if not user_space and request.user.userspace_set.count() > 0:
|
||||
# if the users has a userspace but nothing is active, activate the first one
|
||||
user_space = request.user.userspace_set.first()
|
||||
if user_space:
|
||||
user_space.active = True
|
||||
user_space.save()
|
||||
|
||||
if not user_space:
|
||||
if 'signup_token' in request.session:
|
||||
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
|
||||
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
|
||||
else:
|
||||
# if user does not yet have a space create one for him
|
||||
user_space = create_space_for_user(request.user)
|
||||
|
||||
# TODO remove the need for this view
|
||||
if user_space.groups.count() == 0 and not reverse('account_logout') in request.path:
|
||||
return views.no_groups(request)
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ class Integration:
|
||||
files = None
|
||||
export_type = None
|
||||
ignored_recipes = []
|
||||
import_log = None
|
||||
import_duplicates = False
|
||||
|
||||
import_meal_plans = True
|
||||
import_shopping_lists = True
|
||||
nutrition_per_serving = False
|
||||
|
||||
def __init__(self, request, export_type):
|
||||
"""
|
||||
@@ -102,7 +108,7 @@ class Integration:
|
||||
"""
|
||||
return True
|
||||
|
||||
def do_import(self, files, il, import_duplicates):
|
||||
def do_import(self, files, il, import_duplicates, meal_plans=True, shopping_lists=True, nutrition_per_serving=False):
|
||||
"""
|
||||
Imports given files
|
||||
:param import_duplicates: if true duplicates are imported as well
|
||||
@@ -111,6 +117,12 @@ class Integration:
|
||||
:return: HttpResponseRedirect to the recipe search showing all imported recipes
|
||||
"""
|
||||
with scope(space=self.request.space):
|
||||
self.import_log = il
|
||||
self.import_duplicates = import_duplicates
|
||||
|
||||
self.import_meal_plans = meal_plans
|
||||
self.import_shopping_lists = shopping_lists
|
||||
self.nutrition_per_serving = nutrition_per_serving
|
||||
|
||||
try:
|
||||
self.files = files
|
||||
@@ -166,20 +178,24 @@ class Integration:
|
||||
il.total_recipes = len(new_file_list)
|
||||
file_list = new_file_list
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
||||
if isinstance(self, cookbook.integration.mealie1.Mealie1):
|
||||
# since the mealie 1.0 export is a backup and not a classic recipe export we treat it a bit differently
|
||||
recipes = self.get_recipe_from_file(import_zip)
|
||||
else:
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
||||
import_zip.close()
|
||||
elif '.json' in f['name'] or '.xml' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
|
||||
data_list = self.split_recipe_file(f['file'])
|
||||
|
||||
342
cookbook/integration/mealie1.py
Normal file
342
cookbook/integration/mealie1.py
Normal file
@@ -0,0 +1,342 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from cookbook.helper import ingredient_parser
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step, Food, Unit, SupermarketCategory, PropertyType, Property, MealType, MealPlan, CookLog, ShoppingListEntry
|
||||
|
||||
|
||||
class Mealie1(Integration):
|
||||
"""
|
||||
integration for mealie past version 1.0
|
||||
"""
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
mealie_database = json.loads(BytesIO(file.read('database.json')).getvalue().decode("utf-8"))
|
||||
self.import_log.total_recipes = len(mealie_database['recipes'])
|
||||
self.import_log.msg += f"Importing {len(mealie_database["categories"]) + len(mealie_database["tags"])} tags and categories as keywords...\n"
|
||||
self.import_log.save()
|
||||
|
||||
keywords_categories_dict = {}
|
||||
for c in mealie_database['categories']:
|
||||
if keyword := Keyword.objects.filter(name=c['name'], space=self.request.space).first():
|
||||
keywords_categories_dict[c['id']] = keyword.pk
|
||||
else:
|
||||
keyword = Keyword.objects.create(name=c['name'], space=self.request.space)
|
||||
keywords_categories_dict[c['id']] = keyword.pk
|
||||
|
||||
keywords_tags_dict = {}
|
||||
for t in mealie_database['tags']:
|
||||
if keyword := Keyword.objects.filter(name=t['name'], space=self.request.space).first():
|
||||
keywords_tags_dict[t['id']] = keyword.pk
|
||||
else:
|
||||
keyword = Keyword.objects.create(name=t['name'], space=self.request.space)
|
||||
keywords_tags_dict[t['id']] = keyword.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["multi_purpose_labels"])} multi purpose labels as supermarket categories...\n"
|
||||
self.import_log.save()
|
||||
|
||||
supermarket_categories_dict = {}
|
||||
for m in mealie_database['multi_purpose_labels']:
|
||||
if supermarket_category := SupermarketCategory.objects.filter(name=m['name'], space=self.request.space).first():
|
||||
supermarket_categories_dict[m['id']] = supermarket_category.pk
|
||||
else:
|
||||
supermarket_category = SupermarketCategory.objects.create(name=m['name'], space=self.request.space)
|
||||
supermarket_categories_dict[m['id']] = supermarket_category.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["ingredient_foods"])} foods...\n"
|
||||
self.import_log.save()
|
||||
|
||||
foods_dict = {}
|
||||
for f in mealie_database['ingredient_foods']:
|
||||
if food := Food.objects.filter(name=f['name'], space=self.request.space).first():
|
||||
foods_dict[f['id']] = food.pk
|
||||
else:
|
||||
food = {'name': f['name'],
|
||||
'plural_name': f['plural_name'],
|
||||
'description': f['description'],
|
||||
'space': self.request.space}
|
||||
|
||||
if f['label_id'] and f['label_id'] in supermarket_categories_dict:
|
||||
food['supermarket_category_id'] = supermarket_categories_dict[f['label_id']]
|
||||
|
||||
food = Food.objects.create(**food)
|
||||
if f['on_hand']:
|
||||
food.onhand_users.add(self.request.user)
|
||||
foods_dict[f['id']] = food.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["ingredient_units"])} units...\n"
|
||||
self.import_log.save()
|
||||
|
||||
units_dict = {}
|
||||
for u in mealie_database['ingredient_units']:
|
||||
if unit := Unit.objects.filter(name=u['name'], space=self.request.space).first():
|
||||
units_dict[u['id']] = unit.pk
|
||||
else:
|
||||
unit = Unit.objects.create(name=u['name'], plural_name=u['plural_name'], description=u['description'], space=self.request.space)
|
||||
units_dict[u['id']] = unit.pk
|
||||
|
||||
recipes_dict = {}
|
||||
recipe_property_factor_dict = {}
|
||||
recipes = []
|
||||
recipe_keyword_relation = []
|
||||
for r in mealie_database['recipes']:
|
||||
if Recipe.objects.filter(space=self.request.space, name=r['name']).exists() and not self.import_duplicates:
|
||||
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
|
||||
self.import_log.save()
|
||||
else:
|
||||
recipe = Recipe.objects.create(
|
||||
waiting_time=parse_time(r['perform_time']),
|
||||
working_time=parse_time(r['prep_time']),
|
||||
description=r['description'][:512],
|
||||
name=r['name'],
|
||||
source_url=r['org_url'],
|
||||
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1,
|
||||
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "",
|
||||
internal=True,
|
||||
created_at=r['created_at'],
|
||||
space=self.request.space,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
||||
if not self.nutrition_per_serving:
|
||||
recipe_property_factor_dict[r['id']] = recipe.servings
|
||||
|
||||
self.import_log.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.import_log.imported_recipes += 1
|
||||
self.import_log.save()
|
||||
|
||||
recipes.append(recipe)
|
||||
recipes_dict[r['id']] = recipe.pk
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipe.pk, keyword_id=self.keyword.pk))
|
||||
|
||||
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_instructions"])} instructions...\n"
|
||||
self.import_log.save()
|
||||
|
||||
steps_relation = []
|
||||
first_step_of_recipe_dict = {}
|
||||
for s in mealie_database['recipe_instructions']:
|
||||
if s['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
|
||||
order=s['position'],
|
||||
name=s['title'],
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
|
||||
if s['recipe_id'] not in first_step_of_recipe_dict:
|
||||
first_step_of_recipe_dict[s['recipe_id']] = step.pk
|
||||
|
||||
for n in mealie_database['notes']:
|
||||
if n['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=n['text'],
|
||||
name=n['title'],
|
||||
order=100,
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk))
|
||||
|
||||
Recipe.steps.through.objects.bulk_create(steps_relation)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
|
||||
self.import_log.save()
|
||||
|
||||
ingredients_relation = []
|
||||
for i in mealie_database['recipes_ingredients']:
|
||||
if i['recipe_id'] in recipes_dict:
|
||||
if i['title']:
|
||||
title_ingredient = Ingredient.objects.create(
|
||||
note=i['title'],
|
||||
is_header=True,
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=title_ingredient.pk))
|
||||
if i['food_id']:
|
||||
ingredient = Ingredient.objects.create(
|
||||
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
|
||||
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
|
||||
original_text=i['original_text'],
|
||||
order=i['position'],
|
||||
amount=i['quantity'],
|
||||
note=i['note'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
|
||||
elif i['note'].strip():
|
||||
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
ingredient = Ingredient.objects.create(
|
||||
food=f,
|
||||
unit=u,
|
||||
amount=amount,
|
||||
note=note,
|
||||
original_text=i['original_text'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
|
||||
Step.ingredients.through.objects.bulk_create(ingredients_relation)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
|
||||
self.import_log.save()
|
||||
|
||||
recipe_keyword_relation = []
|
||||
for rC in mealie_database['recipes_to_categories']:
|
||||
if rC['recipe_id'] in recipes_dict:
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rC['recipe_id']], keyword_id=keywords_categories_dict[rC['category_id']]))
|
||||
|
||||
for rT in mealie_database['recipes_to_tags']:
|
||||
if rT['recipe_id'] in recipes_dict:
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rT['recipe_id']], keyword_id=keywords_tags_dict[rT['tag_id']]))
|
||||
|
||||
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_nutrition"])} properties...\n"
|
||||
self.import_log.save()
|
||||
|
||||
property_types_dict = {
|
||||
'calories': PropertyType.objects.get_or_create(name=_('Calories'), space=self.request.space, defaults={'unit': 'kcal', 'fdc_id': 1008})[0],
|
||||
'carbohydrate_content': PropertyType.objects.get_or_create(name=_('Carbohydrates'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1005})[0],
|
||||
'cholesterol_content': PropertyType.objects.get_or_create(name=_('Cholesterol'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1253})[0],
|
||||
'fat_content': PropertyType.objects.get_or_create(name=_('Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1004})[0],
|
||||
'fiber_content': PropertyType.objects.get_or_create(name=_('Fiber'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1079})[0],
|
||||
'protein_content': PropertyType.objects.get_or_create(name=_('Protein'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1003})[0],
|
||||
'saturated_fat_content': PropertyType.objects.get_or_create(name=_('Saturated Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1258})[0],
|
||||
'sodium_content': PropertyType.objects.get_or_create(name=_('Sodium'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1093})[0],
|
||||
'sugar_content': PropertyType.objects.get_or_create(name=_('Sugar'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1063})[0],
|
||||
'trans_fat_content': PropertyType.objects.get_or_create(name=_('Trans Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1257})[0],
|
||||
'unsaturated_fat_content': PropertyType.objects.get_or_create(name=_('Unsaturated Fat'), space=self.request.space, defaults={'unit': 'g'})[0],
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
recipe_properties_relation = []
|
||||
properties_relation = []
|
||||
for r in mealie_database['recipe_nutrition']:
|
||||
if r['recipe_id'] in recipes_dict:
|
||||
for key in property_types_dict:
|
||||
if r[key]:
|
||||
properties_relation.append(
|
||||
Property(property_type_id=property_types_dict[key].pk,
|
||||
property_amount=Decimal(str(r[key])) / (
|
||||
Decimal(str(recipe_property_factor_dict[r['recipe_id']])) if r['recipe_id'] in recipe_property_factor_dict else 1),
|
||||
open_data_food_slug=r['recipe_id'],
|
||||
space=self.request.space))
|
||||
properties = Property.objects.bulk_create(properties_relation)
|
||||
property_ids = []
|
||||
for p in properties:
|
||||
recipe_properties_relation.append(Recipe.properties.through(recipe_id=recipes_dict[p.open_data_food_slug], property_id=p.pk))
|
||||
property_ids.append(p.pk)
|
||||
Recipe.properties.through.objects.bulk_create(recipe_properties_relation, ignore_conflicts=True)
|
||||
Property.objects.filter(id__in=property_ids).update(open_data_food_slug=None)
|
||||
|
||||
# delete unused property types
|
||||
for pT in property_types_dict:
|
||||
try:
|
||||
property_types_dict[pT].delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_comments"]) + len(mealie_database["recipe_timeline_events"])} comments and cook logs...\n"
|
||||
self.import_log.save()
|
||||
|
||||
cook_log_list = []
|
||||
for c in mealie_database['recipe_comments']:
|
||||
if c['recipe_id'] in recipes_dict:
|
||||
cook_log_list.append(CookLog(
|
||||
recipe_id=recipes_dict[c['recipe_id']],
|
||||
comment=c['text'],
|
||||
created_at=c['created_at'],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
for c in mealie_database['recipe_timeline_events']:
|
||||
if c['recipe_id'] in recipes_dict:
|
||||
if c['event_type'] == 'comment':
|
||||
cook_log_list.append(CookLog(
|
||||
recipe_id=recipes_dict[c['recipe_id']],
|
||||
comment=c['message'],
|
||||
created_at=c['created_at'],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
CookLog.objects.bulk_create(cook_log_list)
|
||||
|
||||
if self.import_meal_plans:
|
||||
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
|
||||
self.import_log.save()
|
||||
|
||||
meal_types_dict = {}
|
||||
meal_plans = []
|
||||
for m in mealie_database['group_meal_plans']:
|
||||
if m['recipe_id'] in recipes_dict:
|
||||
if not m['entry_type'] in meal_types_dict:
|
||||
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
|
||||
meal_types_dict[m['entry_type']] = meal_type.pk
|
||||
meal_plans.append(MealPlan(
|
||||
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
|
||||
title=m['title'] if m['title'] else "",
|
||||
note=m['text'] if m['text'] else "",
|
||||
from_date=m['date'],
|
||||
to_date=m['date'],
|
||||
meal_type_id=meal_types_dict[m['entry_type']],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
MealPlan.objects.bulk_create(meal_plans)
|
||||
|
||||
if self.import_shopping_lists:
|
||||
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
|
||||
self.import_log.save()
|
||||
|
||||
shopping_list_items = []
|
||||
for sli in mealie_database['shopping_list_items']:
|
||||
if not sli['checked']:
|
||||
if sli['food_id']:
|
||||
shopping_list_items.append(ShoppingListEntry(
|
||||
amount=sli['quantity'],
|
||||
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
|
||||
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
elif not sli['food_id'] and sli['note'].strip():
|
||||
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
shopping_list_items.append(ShoppingListEntry(
|
||||
amount=amount,
|
||||
unit=u,
|
||||
food=f,
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
ShoppingListEntry.objects.bulk_create(shopping_list_items)
|
||||
|
||||
self.import_log.msg += f"Importing Images. This might take some time ...\n"
|
||||
self.import_log.save()
|
||||
for r in mealie_database['recipes']:
|
||||
try:
|
||||
if recipe := Recipe.objects.filter(pk=recipes_dict[r['id']]).first():
|
||||
self.import_recipe_image(recipe, BytesIO(file.read(f'data/recipes/{str(uuid.UUID(str(r['id'])))}/images/original.webp')), filetype='.webp')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return recipes
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0228_space_space_setup_completed.py
Normal file
18
cookbook/migrations/0228_space_space_setup_completed.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-10 20:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0001_squashed_0227_space_ai_default_provider_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='space_setup_completed',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -329,6 +329,13 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
|
||||
space_setup_completed = models.BooleanField(default=True)
|
||||
|
||||
ai_enabled = models.BooleanField(default=True)
|
||||
ai_credits_monthly = models.IntegerField(default=100)
|
||||
ai_credits_balance = models.DecimalField(default=0, max_digits=16, decimal_places=4)
|
||||
ai_default_provider = models.ForeignKey("AiProvider", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_ai_default_provider')
|
||||
|
||||
internal_note = models.TextField(blank=True, null=True)
|
||||
|
||||
def safe_delete(self):
|
||||
@@ -341,6 +348,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
BookmarkletImport.objects.filter(space=self).delete()
|
||||
CustomFilter.objects.filter(space=self).delete()
|
||||
|
||||
AiLog.objects.filter(space=self).delete()
|
||||
AiProvider.objects.filter(space=self).delete()
|
||||
|
||||
Property.objects.filter(space=self).delete()
|
||||
PropertyType.objects.filter(space=self).delete()
|
||||
|
||||
@@ -393,6 +403,41 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class AiProvider(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.TextField(blank=True)
|
||||
# AiProviders can be global, so space=null is allowed (configurable by superusers)
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||
|
||||
api_key = models.CharField(max_length=2048)
|
||||
model_name = models.CharField(max_length=256)
|
||||
url = models.CharField(max_length=2048, blank=True, null=True)
|
||||
log_credit_cost = models.BooleanField(default=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class AiLog(models.Model, PermissionModelMixin):
|
||||
F_FILE_IMPORT = 'FILE_IMPORT'
|
||||
|
||||
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
|
||||
function = models.CharField(max_length=64)
|
||||
credit_cost = models.DecimalField(max_digits=16, decimal_places=4)
|
||||
# if credits from balance were used, else its from monthly quota
|
||||
credits_from_balance = models.BooleanField(default=False)
|
||||
|
||||
input_tokens = models.IntegerField(default=0)
|
||||
output_tokens = models.IntegerField(default=0)
|
||||
start_time = models.DateTimeField(null=True)
|
||||
end_time = models.DateTimeField(null=True)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class ConnectorConfig(models.Model, PermissionModelMixin):
|
||||
HOMEASSISTANT = 'HomeAssistant'
|
||||
CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),)
|
||||
|
||||
@@ -24,8 +24,9 @@ from rest_framework.fields import IntegerField
|
||||
|
||||
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.ai_helper import get_monthly_token_usage
|
||||
from cookbook.helper.image_processing import is_file_type_allowed
|
||||
from cookbook.helper.permission_helper import above_space_limit
|
||||
from cookbook.helper.permission_helper import above_space_limit, create_space_for_user
|
||||
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||
@@ -36,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
|
||||
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields)
|
||||
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
|
||||
|
||||
@@ -325,12 +326,53 @@ class UserFileViewSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ('id', 'file', 'file_download', 'file_size_kb', 'preview', 'created_by', 'created_at')
|
||||
|
||||
|
||||
class AiProviderSerializer(serializers.ModelSerializer):
|
||||
api_key = serializers.CharField(required=False, write_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data = self.handle_global_space_logic(validated_data)
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data = self.handle_global_space_logic(validated_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def handle_global_space_logic(self, validated_data):
|
||||
"""
|
||||
allow superusers to create AI providers without a space but make sure everyone else only uses their own space
|
||||
"""
|
||||
if ('space' not in validated_data or not validated_data['space']) and self.context['request'].user.is_superuser:
|
||||
validated_data['space'] = None
|
||||
else:
|
||||
validated_data['space'] = self.context['request'].space
|
||||
|
||||
return validated_data
|
||||
|
||||
class Meta:
|
||||
model = AiProvider
|
||||
fields = ('id', 'name', 'description', 'api_key', 'model_name', 'url', 'log_credit_cost', 'space', 'created_at', 'updated_at')
|
||||
read_only_fields = ('created_at', 'updated_at',)
|
||||
|
||||
|
||||
class AiLogSerializer(serializers.ModelSerializer):
|
||||
ai_provider = AiProviderSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AiLog
|
||||
fields = ('id', 'ai_provider', 'function', 'credit_cost', 'credits_from_balance', 'input_tokens', 'output_tokens', 'start_time', 'end_time', 'created_by', 'created_at',
|
||||
'updated_at')
|
||||
read_only_fields = ('__all__',)
|
||||
|
||||
|
||||
class SpaceSerializer(WritableNestedModelSerializer):
|
||||
created_by = UserSerializer(read_only=True)
|
||||
user_count = serializers.SerializerMethodField('get_user_count')
|
||||
recipe_count = serializers.SerializerMethodField('get_recipe_count')
|
||||
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
|
||||
food_inherit = FoodInheritFieldSerializer(many=True)
|
||||
user_count = serializers.SerializerMethodField('get_user_count', read_only=True)
|
||||
recipe_count = serializers.SerializerMethodField('get_recipe_count', read_only=True)
|
||||
file_size_mb = serializers.SerializerMethodField('get_file_size_mb', read_only=True)
|
||||
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used', read_only=True)
|
||||
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
|
||||
food_inherit = FoodInheritFieldSerializer(many=True, required=False)
|
||||
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||
@@ -350,6 +392,10 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
def get_recipe_count(self, obj):
|
||||
return Recipe.objects.filter(space=obj).count()
|
||||
|
||||
@extend_schema_field(int)
|
||||
def get_ai_monthly_credits_used(self, obj):
|
||||
return get_monthly_token_usage(obj)
|
||||
|
||||
@extend_schema_field(float)
|
||||
def get_file_size_mb(self, obj):
|
||||
try:
|
||||
@@ -358,7 +404,36 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
return 0
|
||||
|
||||
def create(self, validated_data):
|
||||
raise ValidationError('Cannot create using this endpoint')
|
||||
if Space.objects.filter(created_by=self.context['request'].user).count() >= self.context['request'].user.userpreference.max_owned_spaces:
|
||||
raise serializers.ValidationError(
|
||||
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({self.context['request'].user.userpreference.max_owned_spaces})')
|
||||
|
||||
name = None
|
||||
if 'name' in validated_data:
|
||||
name = validated_data['name']
|
||||
user_space = create_space_for_user(self.context['request'].user, name)
|
||||
return user_space.space
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data = self.filter_superuser_parameters(validated_data)
|
||||
|
||||
if 'name' in validated_data:
|
||||
if Space.objects.filter(Q(name=validated_data['name']), ~Q(pk=instance.pk)).exists():
|
||||
raise ValidationError(_('Space Name must be unique.'))
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def filter_superuser_parameters(self, validated_data):
|
||||
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:
|
||||
del validated_data['ai_enabled']
|
||||
|
||||
if 'ai_credits_monthly' in validated_data and not self.context['request'].user.is_superuser:
|
||||
del validated_data['ai_credits_monthly']
|
||||
|
||||
if 'ai_credits_balance' in validated_data and not self.context['request'].user.is_superuser:
|
||||
del validated_data['ai_credits_balance']
|
||||
|
||||
return validated_data
|
||||
|
||||
class Meta:
|
||||
model = Space
|
||||
@@ -366,10 +441,11 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
|
||||
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color',
|
||||
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
|
||||
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg', 'ai_credits_monthly',
|
||||
'ai_credits_balance', 'ai_monthly_credits_used', 'ai_enabled', 'ai_default_provider', 'space_setup_completed')
|
||||
read_only_fields = (
|
||||
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
|
||||
'demo',)
|
||||
'demo', 'ai_monthly_credits_used')
|
||||
|
||||
|
||||
class UserSpaceSerializer(WritableNestedModelSerializer):
|
||||
@@ -1038,7 +1114,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'private','servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
|
||||
'internal', 'private', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
|
||||
)
|
||||
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
|
||||
# TODO find a solution (custom schema?) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
|
||||
@@ -1134,6 +1210,35 @@ class RecipeBatchUpdateSerializer(serializers.Serializer):
|
||||
clear_description = serializers.BooleanField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class FoodBatchUpdateSerializer(serializers.Serializer):
|
||||
foods = serializers.ListField(child=serializers.IntegerField())
|
||||
|
||||
category = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
substitute_add = serializers.ListField(child=serializers.IntegerField())
|
||||
substitute_remove = serializers.ListField(child=serializers.IntegerField())
|
||||
substitute_set = serializers.ListField(child=serializers.IntegerField())
|
||||
substitute_remove_all = serializers.BooleanField(default=False)
|
||||
|
||||
inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
|
||||
inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
|
||||
inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
|
||||
inherit_fields_remove_all = serializers.BooleanField(default=False)
|
||||
|
||||
child_inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
|
||||
child_inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
|
||||
child_inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
|
||||
child_inherit_fields_remove_all = serializers.BooleanField(default=False)
|
||||
|
||||
substitute_children = serializers.BooleanField(required=False, allow_null=True)
|
||||
substitute_siblings = serializers.BooleanField(required=False, allow_null=True)
|
||||
ignore_shopping = serializers.BooleanField(required=False, allow_null=True)
|
||||
on_hand = serializers.BooleanField(required=False, allow_null=True)
|
||||
|
||||
parent_remove = serializers.BooleanField(required=False, allow_null=True)
|
||||
parent_set = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
shared = UserSerializer(many=True, required=False)
|
||||
|
||||
@@ -1564,7 +1669,6 @@ class ServerSettingsSerializer(serializers.Serializer):
|
||||
# TODO add all other relevant settings including path/url related ones?
|
||||
shopping_min_autosync_interval = serializers.CharField()
|
||||
enable_pdf_export = serializers.BooleanField()
|
||||
enable_ai_import = serializers.BooleanField()
|
||||
disable_external_connectors = serializers.BooleanField()
|
||||
terms_url = serializers.CharField()
|
||||
privacy_url = serializers.CharField()
|
||||
@@ -1788,6 +1892,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AiImportSerializer(serializers.Serializer):
|
||||
ai_provider_id = serializers.IntegerField()
|
||||
file = serializers.FileField(allow_null=True)
|
||||
text = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
recipe_id = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
|
||||
168
cookbook/tests/api/test_api_ai_provider.py
Normal file
168
cookbook/tests/api/test_api_ai_provider.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import MealType, PropertyType, AiProvider
|
||||
|
||||
LIST_URL = 'api:aiprovider-list'
|
||||
DETAIL_URL = 'api:aiprovider-detail'
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def obj_1(space_1, a1_s1):
|
||||
return AiProvider.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def obj_2(space_1, a1_s1):
|
||||
return AiProvider.objects.get_or_create(name='test_2', space=None)[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 200],
|
||||
['a1_s1', 200],
|
||||
])
|
||||
def test_list_permission(arg, request):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
assert c.get(reverse(LIST_URL)).status_code == arg[1]
|
||||
|
||||
|
||||
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
|
||||
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
|
||||
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
|
||||
|
||||
obj_1.space = space_2
|
||||
obj_1.save()
|
||||
|
||||
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
|
||||
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
|
||||
|
||||
obj_1.space = None
|
||||
obj_1.save()
|
||||
|
||||
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
|
||||
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 403],
|
||||
['a1_s1', 200],
|
||||
['g1_s2', 403],
|
||||
['u1_s2', 403],
|
||||
['a1_s2', 404],
|
||||
])
|
||||
def test_update(arg, request, obj_1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.patch(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_1.id}
|
||||
),
|
||||
{'name': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
assert r.status_code == arg[1]
|
||||
if r.status_code == 200:
|
||||
assert response['name'] == 'new'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 403],
|
||||
['a1_s1', 403],
|
||||
['g1_s2', 403],
|
||||
['u1_s2', 403],
|
||||
['a1_s2', 403],
|
||||
['s1_s1', 200],
|
||||
])
|
||||
def test_update_global(arg, request, obj_2):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.patch(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_2.id}
|
||||
),
|
||||
{'name': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
assert r.status_code == arg[1]
|
||||
if r.status_code == 200:
|
||||
assert response['name'] == 'new'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 403],
|
||||
['a1_s1', 201],
|
||||
])
|
||||
def test_add(arg, request, u1_s2):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.post(
|
||||
reverse(LIST_URL),
|
||||
{'name': 'test', 'api_key': 'test', 'model_name': 'test'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
assert r.status_code == arg[1]
|
||||
if r.status_code == 201:
|
||||
assert response['name'] == 'test'
|
||||
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||
assert r.status_code == 200
|
||||
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete(a1_s1, a1_s2, obj_1):
|
||||
# admins cannot delete foreign space providers
|
||||
r = a1_s2.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_1.id}
|
||||
)
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
# admins can delete their space providers
|
||||
r = a1_s1.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_1.id}
|
||||
)
|
||||
)
|
||||
|
||||
assert r.status_code == 204
|
||||
with scopes_disabled():
|
||||
assert AiProvider.objects.count() == 0
|
||||
|
||||
|
||||
def test_delete_global(a1_s1, s1_s1, obj_2):
|
||||
# admins cant delete global providers
|
||||
r = a1_s1.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_2.id}
|
||||
)
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
||||
# superusers can delete global providers
|
||||
r = s1_s1.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_2.id}
|
||||
)
|
||||
)
|
||||
|
||||
assert r.status_code == 204
|
||||
with scopes_disabled():
|
||||
assert AiProvider.objects.count() == 0
|
||||
@@ -7,6 +7,7 @@ from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import UserSpace
|
||||
from recipes import settings
|
||||
|
||||
LIST_URL = 'api:space-list'
|
||||
DETAIL_URL = 'api:space-detail'
|
||||
@@ -45,7 +46,6 @@ def test_list_multiple(u1_s1, space_1, space_2):
|
||||
assert u1_response['id'] == space_1.id
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
@@ -70,9 +70,9 @@ def test_update(arg, request, space_1, a1_s1):
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 403],
|
||||
['a1_s1', 405],
|
||||
['g1_s1', 201],
|
||||
['u1_s1', 201],
|
||||
['a1_s1', 201],
|
||||
])
|
||||
def test_add(arg, request, u1_s2):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
@@ -90,3 +90,59 @@ def test_delete(u1_s1, u1_s2, a1_s1, space_1):
|
||||
# event the space owner cannot delete his space over the api (this might change later but for now it's only available in the UI)
|
||||
r = a1_s1.delete(reverse(DETAIL_URL, args={space_1.id}))
|
||||
assert r.status_code == 405
|
||||
|
||||
|
||||
def test_superuser_parameters(space_1, a1_s1, s1_s1):
|
||||
# ------- test as normal user -------
|
||||
response = a1_s1.post(reverse(LIST_URL), {'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
response = json.loads(response.content)
|
||||
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
|
||||
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
|
||||
assert response['ai_credits_balance'] == 0
|
||||
|
||||
space_1.created_by = auth.get_user(a1_s1)
|
||||
space_1.ai_enabled = False
|
||||
space_1.ai_credits_monthly = 0
|
||||
space_1.ai_credits_balance = 0
|
||||
space_1.save()
|
||||
|
||||
response = a1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
space_1.refresh_from_db()
|
||||
assert space_1.ai_enabled == False
|
||||
assert space_1.ai_credits_monthly == 0
|
||||
assert space_1.ai_credits_balance == 0
|
||||
|
||||
# ------- test as superuser -------
|
||||
|
||||
response = s1_s1.post(reverse(LIST_URL),
|
||||
{'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
response = json.loads(response.content)
|
||||
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
|
||||
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
|
||||
assert response['ai_credits_balance'] == 0
|
||||
|
||||
space_1.created_by = auth.get_user(s1_s1)
|
||||
space_1.ai_enabled = False
|
||||
space_1.ai_credits_monthly = 0
|
||||
space_1.ai_credits_balance = 0
|
||||
space_1.save()
|
||||
|
||||
response = s1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
space_1.refresh_from_db()
|
||||
assert space_1.ai_enabled == True
|
||||
assert space_1.ai_credits_monthly == 100
|
||||
assert space_1.ai_credits_balance == 100
|
||||
|
||||
@@ -5,6 +5,8 @@ from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import UserSpace
|
||||
|
||||
LIST_URL = 'api:userspace-list'
|
||||
DETAIL_URL = 'api:userspace-detail'
|
||||
|
||||
@@ -13,10 +15,10 @@ DETAIL_URL = 'api:userspace-detail'
|
||||
['a_u', 403, 0],
|
||||
['g1_s1', 200, 1], # sees only own user space
|
||||
['u1_s1', 200, 1],
|
||||
['a1_s1', 200, 3], # sees user space of all users in space
|
||||
['a2_s1', 200, 1],
|
||||
['a1_s1', 200, 4], # admins can see all other members
|
||||
['a2_s1', 200, 4],
|
||||
])
|
||||
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
|
||||
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1, a2_s1):
|
||||
space_1.created_by = auth.get_user(a1_s1)
|
||||
space_1.save()
|
||||
|
||||
@@ -27,6 +29,18 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
|
||||
assert len(json.loads(result.content)['results']) == arg[2]
|
||||
|
||||
|
||||
def test_list_all_personal(space_2, u1_s1):
|
||||
result = u1_s1.get(reverse('api:userspace-all-personal'))
|
||||
assert result.status_code == 200
|
||||
assert len(json.loads(result.content)) == 1
|
||||
|
||||
UserSpace.objects.create(user=auth.get_user(u1_s1), space=space_2)
|
||||
|
||||
result = u1_s1.get(reverse('api:userspace-all-personal'))
|
||||
assert result.status_code == 200
|
||||
assert len(json.loads(result.content)) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
|
||||
@@ -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')
|
||||
@@ -76,10 +78,11 @@ urlpatterns = [
|
||||
|
||||
path('setup/', views.setup, name='view_setup'),
|
||||
path('no-group/', views.no_groups, name='view_no_group'),
|
||||
path('space-overview/', views.space_overview, name='view_space_overview'),
|
||||
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
|
||||
path('no-perm/', views.no_perm, name='view_no_perm'),
|
||||
#path('space-overview/', views.space_overview, name='view_space_overview'),
|
||||
#path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
|
||||
#path('no-perm/', views.no_perm, name='view_no_perm'),
|
||||
path('invite/<slug:token>', views.invite_link, name='view_invite'),
|
||||
path('invite/<slug:token>/', views.invite_link, name='view_invite'),
|
||||
|
||||
path('system/', views.system, name='view_system'),
|
||||
path('plugin/update/', views.plugin_update, name='view_plugin_update'),
|
||||
|
||||
@@ -18,8 +18,6 @@ import litellm
|
||||
import redis
|
||||
import requests
|
||||
from PIL import UnidentifiedImageError
|
||||
from PIL.ImImagePlugin import number
|
||||
from PIL.features import check
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
@@ -35,7 +33,6 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.datetime_safe import date
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -65,6 +62,8 @@ from cookbook.connectors.connector_manager import ConnectorManager, ActionType
|
||||
from cookbook.forms import ImportForm, ImportExportBase
|
||||
from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.HelperFunctions import str2bool, validate_import_url
|
||||
from cookbook.helper.ai_helper import has_monthly_token, can_perform_ai_request, AiCallbackHandler
|
||||
from cookbook.helper.batch_edit_helper import add_to_relation, remove_from_relation, remove_all_from_relation, set_relation
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.open_data_importer import OpenDataImporter
|
||||
@@ -74,7 +73,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, Cus
|
||||
CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF,
|
||||
above_space_limit,
|
||||
group_required, has_group_permission, is_space_owner,
|
||||
switch_user_active_space
|
||||
switch_user_active_space, CustomAiProviderPermission, IsCreateDRF
|
||||
)
|
||||
from cookbook.helper.recipe_search import RecipeSearch
|
||||
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
|
||||
@@ -85,7 +84,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
|
||||
RecipeBookEntry, ShareLink, ShoppingListEntry,
|
||||
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields
|
||||
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider
|
||||
)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
@@ -110,12 +109,13 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
|
||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
|
||||
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
|
||||
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
|
||||
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer
|
||||
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
|
||||
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer
|
||||
)
|
||||
from cookbook.version_info import TANDOOR_VERSION
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT, AI_API_KEY, AI_MODEL_NAME
|
||||
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT
|
||||
|
||||
DateExample = OpenApiExample('Date Format', value='1972-12-05', request_only=True)
|
||||
BeforeDateExample = OpenApiExample('Before Date Format', value='-1972-12-05', request_only=True)
|
||||
@@ -131,7 +131,7 @@ class LoggingMixin(object):
|
||||
|
||||
if settings.REDIS_HOST:
|
||||
try:
|
||||
d = date.today().isoformat()
|
||||
d = timezone.now().isoformat()
|
||||
space = request.space
|
||||
endpoint = request.resolver_match.url_name
|
||||
|
||||
@@ -179,7 +179,10 @@ class StandardFilterModelViewSet(viewsets.ModelViewSet):
|
||||
queryset = self.queryset
|
||||
query = self.request.query_params.get('query', None)
|
||||
if query is not None:
|
||||
queryset = queryset.filter(name__icontains=query)
|
||||
try:
|
||||
queryset = queryset.filter(name__icontains=query)
|
||||
except FieldError:
|
||||
pass
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
if updated_at is not None:
|
||||
@@ -541,9 +544,9 @@ class GroupViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Space.objects
|
||||
serializer_class = SpaceSerializer
|
||||
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
permission_classes = [((IsReadOnlyDRF | IsCreateDRF) & CustomIsGuest) | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
pagination_class = DefaultPagination
|
||||
http_method_names = ['get', 'patch']
|
||||
http_method_names = ['get', 'post', 'put', 'patch']
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(
|
||||
@@ -562,7 +565,7 @@ class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = UserSpace.objects
|
||||
serializer_class = UserSpaceSerializer
|
||||
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
|
||||
permission_classes = [(CustomIsSpaceOwner | (IsReadOnlyDRF & CustomIsUser) | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
|
||||
http_method_names = ['get', 'put', 'patch', 'delete']
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
@@ -576,10 +579,23 @@ class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
if internal_note is not None:
|
||||
self.queryset = self.queryset.filter(internal_note=internal_note)
|
||||
|
||||
if is_space_owner(self.request.user, self.request.space):
|
||||
# >= admins can see all users, guest/user can only see themselves
|
||||
if has_group_permission(self.request.user, ['admin']):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
else:
|
||||
return self.queryset.filter(user=self.request.user, space=self.request.space)
|
||||
return self.queryset.filter(space=self.request.space, user=self.request.user)
|
||||
|
||||
@extend_schema(responses=UserSpaceSerializer(many=True))
|
||||
@decorators.action(detail=False, pagination_class=DefaultPagination, methods=['GET'], serializer_class=UserSpaceSerializer, )
|
||||
def all_personal(self, request):
|
||||
"""
|
||||
return all userspaces for the user requesting the endpoint
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
with scopes_disabled():
|
||||
self.queryset = self.queryset.filter(user=self.request.user)
|
||||
return Response(self.serializer_class(self.queryset.all(), many=True, context={'request': self.request}).data)
|
||||
|
||||
|
||||
class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
@@ -617,6 +633,29 @@ class SearchPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
return self.queryset.filter(user=self.request.user)
|
||||
|
||||
|
||||
class AiProviderViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = AiProvider.objects
|
||||
serializer_class = AiProviderSerializer
|
||||
permission_classes = [CustomAiProviderPermission & CustomTokenHasReadWriteScope]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
# read only access to all space and global AiProviders
|
||||
with scopes_disabled():
|
||||
return self.queryset.filter(Q(space=self.request.space) | Q(space__isnull=True))
|
||||
|
||||
|
||||
class AiLogViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = AiLog.objects
|
||||
serializer_class = AiLogSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
http_method_names = ['get']
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class StorageViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
# TODO handle delete protect error and adjust test
|
||||
queryset = Storage.objects
|
||||
@@ -915,6 +954,94 @@ class FoodViewSet(LoggingMixin, TreeMixin):
|
||||
content = {'error': True, 'msg': e.args[0]}
|
||||
return Response(content, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@decorators.action(detail=False, methods=['PUT'], serializer_class=FoodBatchUpdateSerializer)
|
||||
def batch_update(self, request):
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
foods = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space)
|
||||
safe_food_ids = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space).values_list('id', flat=True)
|
||||
|
||||
if 'category' in serializer.validated_data:
|
||||
foods.update(supermarket_category_id=serializer.validated_data['category'])
|
||||
|
||||
if 'ignore_shopping' in serializer.validated_data and serializer.validated_data['ignore_shopping'] is not None:
|
||||
foods.update(ignore_shopping=serializer.validated_data['ignore_shopping'])
|
||||
|
||||
if 'on_hand' in serializer.validated_data and serializer.validated_data['on_hand'] is not None:
|
||||
if serializer.validated_data['on_hand']:
|
||||
user_relation = []
|
||||
for f in safe_food_ids:
|
||||
user_relation.append(Food.onhand_users.through(food_id=f, user_id=request.user.id))
|
||||
Food.onhand_users.through.objects.bulk_create(user_relation, ignore_conflicts=True, unique_fields=('food_id', 'user_id',))
|
||||
else:
|
||||
Food.onhand_users.through.objects.filter(food_id__in=safe_food_ids, user_id=request.user.id).delete()
|
||||
|
||||
if 'substitute_children' in serializer.validated_data and serializer.validated_data['substitute_children'] is not None:
|
||||
foods.update(substitute_children=serializer.validated_data['substitute_children'])
|
||||
|
||||
if 'substitute_siblings' in serializer.validated_data and serializer.validated_data['substitute_siblings'] is not None:
|
||||
foods.update(substitute_siblings=serializer.validated_data['substitute_siblings'])
|
||||
|
||||
# ---------- substitutes -------------
|
||||
if 'substitute_add' in serializer.validated_data:
|
||||
add_to_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_add'])
|
||||
|
||||
if 'substitute_remove' in serializer.validated_data:
|
||||
remove_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_remove'])
|
||||
|
||||
if 'substitute_set' in serializer.validated_data and len(serializer.validated_data['substitute_set']) > 0:
|
||||
set_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_set'])
|
||||
|
||||
if 'substitute_remove_all' in serializer.validated_data and serializer.validated_data['substitute_remove_all']:
|
||||
remove_all_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids)
|
||||
|
||||
# ---------- inherit fields -------------
|
||||
if 'inherit_fields_add' in serializer.validated_data:
|
||||
add_to_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_add'])
|
||||
|
||||
if 'inherit_fields_remove' in serializer.validated_data:
|
||||
remove_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_remove'])
|
||||
|
||||
if 'inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['inherit_fields_set']) > 0:
|
||||
set_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_set'])
|
||||
|
||||
if 'inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['inherit_fields_remove_all']:
|
||||
remove_all_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids)
|
||||
|
||||
# ---------- child inherit fields -------------
|
||||
if 'child_inherit_fields_add' in serializer.validated_data:
|
||||
add_to_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_add'])
|
||||
|
||||
if 'child_inherit_fields_remove' in serializer.validated_data:
|
||||
remove_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_remove'])
|
||||
|
||||
if 'child_inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['child_inherit_fields_set']) > 0:
|
||||
set_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_set'])
|
||||
|
||||
if 'child_inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['child_inherit_fields_remove_all']:
|
||||
remove_all_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids)
|
||||
|
||||
# ------- parent --------
|
||||
if self.model.node_order_by:
|
||||
node_location = 'sorted'
|
||||
else:
|
||||
node_location = 'last'
|
||||
|
||||
if 'parent_remove' in serializer.validated_data and serializer.validated_data['parent_remove']:
|
||||
for f in foods:
|
||||
f.move(Food.get_first_root_node(), f'{node_location}-sibling')
|
||||
|
||||
if 'parent_set' in serializer.validated_data:
|
||||
parent_food = Food.objects.filter(space=request.space, id=serializer.validated_data['parent_set']).first()
|
||||
if parent_food:
|
||||
for f in foods:
|
||||
f.move(parent_food, f'{node_location}-child')
|
||||
|
||||
return Response({}, 200)
|
||||
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
|
||||
@extend_schema_view(list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='order_field', description='Field to order recipe books on', type=str,
|
||||
@@ -1108,7 +1235,19 @@ class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
return self.serializer_class
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.filter(step__recipe__space=self.request.space)
|
||||
queryset = self.queryset.prefetch_related('food',
|
||||
'food__properties',
|
||||
'food__properties__property_type',
|
||||
'food__inherit_fields',
|
||||
'food__supermarket_category',
|
||||
'food__onhand_users',
|
||||
'food__substitute',
|
||||
'food__child_inherit_fields',
|
||||
'unit',
|
||||
'unit__unit_conversion_base_relation',
|
||||
'unit__unit_conversion_base_relation__base_unit',
|
||||
'unit__unit_conversion_converted_relation',
|
||||
'unit__unit_conversion_converted_relation__converted_unit', ).filter(step__recipe__space=self.request.space)
|
||||
food = self.request.query_params.get('food', None)
|
||||
if food and re.match(r'^(\d)+$', food):
|
||||
queryset = queryset.filter(food_id=food)
|
||||
@@ -1779,8 +1918,8 @@ class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
if internal_note is not None:
|
||||
self.queryset = self.queryset.filter(internal_note=internal_note)
|
||||
|
||||
unused = self.request.query_params.get('unused', False)
|
||||
if unused:
|
||||
used = self.request.query_params.get('used', False)
|
||||
if not used:
|
||||
self.queryset = self.queryset.filter(used_by=None)
|
||||
|
||||
if is_space_owner(self.request.user, self.request.space):
|
||||
@@ -2000,6 +2139,24 @@ class AiImportView(APIView):
|
||||
if serializer.is_valid():
|
||||
# TODO max file size check
|
||||
|
||||
if 'ai_provider_id' not in serializer.validated_data:
|
||||
response = {
|
||||
'error': True,
|
||||
'msg': _('You must select an AI provider to perform your request.'),
|
||||
}
|
||||
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not can_perform_ai_request(request.space):
|
||||
response = {
|
||||
'error': True,
|
||||
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
|
||||
}
|
||||
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
|
||||
|
||||
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider)]
|
||||
|
||||
messages = []
|
||||
uploaded_file = serializer.validated_data['file']
|
||||
|
||||
@@ -2068,7 +2225,15 @@ class AiImportView(APIView):
|
||||
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
ai_response = completion(api_key=AI_API_KEY, model=AI_MODEL_NAME, response_format={"type": "json_object"}, messages=messages, )
|
||||
ai_request = {
|
||||
'api_key': ai_provider.api_key,
|
||||
'model': ai_provider.model_name,
|
||||
'response_format': {"type": "json_object"},
|
||||
'messages': messages,
|
||||
}
|
||||
if ai_provider.url:
|
||||
ai_request['api_base'] = ai_provider.url
|
||||
ai_response = completion(**ai_request)
|
||||
except BadRequestError as err:
|
||||
response = {
|
||||
'error': True,
|
||||
@@ -2127,7 +2292,13 @@ class AppImportView(APIView):
|
||||
files = []
|
||||
for f in request.FILES.getlist('files'):
|
||||
files.append({'file': io.BytesIO(f.read()), 'name': f.name})
|
||||
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
|
||||
t = threading.Thread(target=integration.do_import,
|
||||
args=[files, il, form.cleaned_data['duplicates']],
|
||||
kwargs={'meal_plans': form.cleaned_data['meal_plans'],
|
||||
'shopping_lists': form.cleaned_data['shopping_lists'],
|
||||
'nutrition_per_serving': form.cleaned_data['nutrition_per_serving']
|
||||
}
|
||||
)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
@@ -2373,7 +2544,6 @@ class ServerSettingsViewSet(viewsets.GenericViewSet):
|
||||
# Attention: No login required, do not return sensitive data
|
||||
s['shopping_min_autosync_interval'] = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
|
||||
s['enable_pdf_export'] = settings.ENABLE_PDF_EXPORT
|
||||
s['enable_ai_import'] = settings.AI_API_KEY != ''
|
||||
s['disable_external_connectors'] = settings.DISABLE_EXTERNAL_CONNECTORS
|
||||
s['terms_url'] = settings.TERMS_URL
|
||||
s['privacy_url'] = settings.PRIVACY_URL
|
||||
@@ -2546,10 +2716,9 @@ def ingredient_from_string(request):
|
||||
|
||||
if unit:
|
||||
if unit_obj := Unit.objects.filter(space=request.space).filter(Q(name=unit) | Q(plural_name=unit)).first():
|
||||
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
else:
|
||||
unit_obj = Unit.objects.create(space=request.space, name=unit)
|
||||
ingredient['food'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
ingredient['unit'] = {'name': unit.name, 'id': unit.id}
|
||||
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
|
||||
|
||||
return JsonResponse(ingredient, status=200)
|
||||
|
||||
@@ -17,6 +17,7 @@ from cookbook.integration.copymethat import CopyMeThat
|
||||
from cookbook.integration.default import Default
|
||||
from cookbook.integration.domestica import Domestica
|
||||
from cookbook.integration.mealie import Mealie
|
||||
from cookbook.integration.mealie1 import Mealie1
|
||||
from cookbook.integration.mealmaster import MealMaster
|
||||
from cookbook.integration.melarecipes import MelaRecipes
|
||||
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
|
||||
@@ -45,6 +46,8 @@ def get_integration(request, export_type):
|
||||
return NextcloudCookbook(request, export_type)
|
||||
if export_type == ImportExportBase.MEALIE:
|
||||
return Mealie(request, export_type)
|
||||
if export_type == ImportExportBase.MEALIE1:
|
||||
return Mealie1(request, export_type)
|
||||
if export_type == ImportExportBase.CHOWDOWN:
|
||||
return Chowdown(request, export_type)
|
||||
if export_type == ImportExportBase.SAFFRON:
|
||||
|
||||
@@ -54,7 +54,7 @@ def hook(request, token):
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
|
||||
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
|
||||
ShoppingListEntry.objects.create(food=f, unit=u, amount=max(1, amount), created_by=request.user, space=request.space)
|
||||
|
||||
return JsonResponse({'data': data['message']['text']})
|
||||
except Exception:
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.datetime_safe import date
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_spectacular.views import SpectacularRedocView, SpectacularSwaggerView
|
||||
@@ -42,6 +42,9 @@ def index(request, path=None, resource=None):
|
||||
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
|
||||
return HttpResponseRedirect(reverse_lazy('view_setup'))
|
||||
|
||||
if 'signup_token' in request.session:
|
||||
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
|
||||
|
||||
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
|
||||
return render(request, 'frontend/tandoor.html', {})
|
||||
else:
|
||||
@@ -97,7 +100,8 @@ def space_overview(request):
|
||||
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
|
||||
max_users=settings.SPACE_DEFAULT_MAX_USERS,
|
||||
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
|
||||
)
|
||||
ai_enabled=settings.SPACE_AI_ENABLED,
|
||||
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY, )
|
||||
|
||||
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
|
||||
user_space.groups.add(Group.objects.filter(name='admin').get())
|
||||
@@ -222,7 +226,7 @@ def system(request):
|
||||
total_stats = ['All', int(r.get('api:request-count'))]
|
||||
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
d = (timezone.now() - timedelta(days=i)).isoformat()
|
||||
api_stats[0].append(d)
|
||||
api_space_stats[0].append(d)
|
||||
total_stats.append(int(r.get(f'api:request-count:{d}')) if r.get(f'api:request-count:{d}') else 0)
|
||||
@@ -233,7 +237,7 @@ def system(request):
|
||||
endpoint = x[0].decode('utf-8')
|
||||
endpoint_stats = [endpoint, x[1]]
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
d = (timezone.now() - timedelta(days=i)).isoformat()
|
||||
endpoint_stats.append(r.zscore(f'api:endpoint-request-count:{d}', endpoint))
|
||||
api_stats.append(endpoint_stats)
|
||||
|
||||
@@ -242,7 +246,7 @@ def system(request):
|
||||
if space := Space.objects.filter(pk=s).first():
|
||||
space_stats = [space.name, x[1]]
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
d = (timezone.now() - timedelta(days=i)).isoformat()
|
||||
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
|
||||
api_space_stats.append(space_stats)
|
||||
|
||||
@@ -321,7 +325,7 @@ def invite_link(request, token):
|
||||
try:
|
||||
token = UUID(token, version=4)
|
||||
except ValueError:
|
||||
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
|
||||
print('Malformed Invite Link supplied!')
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
|
||||
@@ -330,22 +334,17 @@ def invite_link(request, token):
|
||||
link.used_by = request.user
|
||||
link.save()
|
||||
|
||||
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
|
||||
|
||||
if request.user.userspace_set.count() == 1:
|
||||
user_space.active = True
|
||||
user_space.save()
|
||||
UserSpace.objects.filter(user=request.user).update(active=False)
|
||||
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=True)
|
||||
|
||||
user_space.groups.add(link.group)
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
|
||||
return HttpResponseRedirect(reverse('view_space_overview'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
else:
|
||||
request.session['signup_token'] = str(token)
|
||||
return HttpResponseRedirect(reverse('account_signup'))
|
||||
|
||||
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
|
||||
return HttpResponseRedirect(reverse('view_space_overview'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
|
||||
def report_share_abuse(request, token):
|
||||
|
||||
18
docs/features/ai.md
Normal file
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.
|
||||
@@ -97,10 +97,17 @@ Follow these steps to import your recipes
|
||||
|
||||
Mealie provides structured data similar to nextcloud.
|
||||
|
||||
!!! WARNING "Versions"
|
||||
There are two different versions of the Mealie importer. One for all backups created prior to Version 1.0 and one for all after.
|
||||
|
||||
!!! INFO "Versions"
|
||||
The Mealie UI does not indicate weather or not nutrition information is stored per serving or per recipe. This choice is left to the user. During the import you will have to choose
|
||||
how Tandoor should treat your nutrition data.
|
||||
|
||||
To migrate your recipes
|
||||
|
||||
1. Go to your Mealie settings and create a new Backup.
|
||||
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server).
|
||||
1. Go to your Mealie admin settings and create a new backup.
|
||||
2. Download the backup.
|
||||
3. Upload the entire `.zip` file to the importer page and import everything.
|
||||
|
||||
## Chowdown
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
|
||||
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
|
||||
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = extract_bool('SPACE_DEFAULT_ALLOW_SHARING', True)
|
||||
SPACE_AI_ENABLED = extract_bool('SPACE_AI_ENABLED', True)
|
||||
SPACE_AI_CREDITS_MONTHLY = int(os.getenv('SPACE_AI_CREDITS_MONTHLY', 10000))
|
||||
|
||||
INTERNAL_IPS = extract_comma_list('INTERNAL_IPS', '127.0.0.1')
|
||||
|
||||
@@ -137,8 +139,6 @@ HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
|
||||
|
||||
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
|
||||
|
||||
AI_API_KEY = os.getenv('AI_API_KEY', '')
|
||||
AI_MODEL_NAME = os.getenv('AI_MODEL_NAME', 'gemini/gemini-2.0-flash')
|
||||
AI_RATELIMIT = os.getenv('AI_RATELIMIT', '60/hour')
|
||||
|
||||
SHARING_ABUSE = extract_bool('SHARING_ABUSE', False)
|
||||
@@ -565,8 +565,6 @@ else:
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
@@ -594,8 +592,18 @@ LANGUAGES = [
|
||||
|
||||
AWS_ENABLED = True if os.getenv('S3_ACCESS_KEY', False) else False
|
||||
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
# Serve static files with gzip
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
if os.getenv('S3_ACCESS_KEY', ''):
|
||||
DEFAULT_FILE_STORAGE = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
|
||||
STORAGES['default']['BACKEND'] = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
|
||||
|
||||
AWS_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY', '')
|
||||
AWS_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', '')
|
||||
@@ -610,14 +618,9 @@ if os.getenv('S3_ACCESS_KEY', ''):
|
||||
if os.getenv('S3_CUSTOM_DOMAIN', ''):
|
||||
AWS_S3_CUSTOM_DOMAIN = os.getenv('S3_CUSTOM_DOMAIN', '')
|
||||
|
||||
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
|
||||
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
|
||||
else:
|
||||
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
|
||||
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
|
||||
|
||||
# Serve static files with gzip
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
|
||||
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
|
||||
|
||||
# settings for cross site origin (CORS)
|
||||
# all origins allowed to support bookmarklet
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Django==4.2.22
|
||||
Django==5.2.6
|
||||
cryptography===45.0.5
|
||||
django-annoying==0.10.6
|
||||
django-cleanup==9.0.0
|
||||
django-crispy-forms==2.4
|
||||
crispy-bootstrap4==2025.6
|
||||
djangorestframework==3.15.2
|
||||
djangorestframework==3.16.1
|
||||
drf-spectacular==0.27.1
|
||||
drf-spectacular-sidecar==2025.7.1
|
||||
drf-spectacular-sidecar==2025.8.1
|
||||
drf-writable-nested==0.7.2
|
||||
django-oauth-toolkit==2.4.0
|
||||
django-debug-toolbar==4.3.0
|
||||
@@ -16,7 +16,7 @@ lxml==5.3.1
|
||||
Markdown==3.7
|
||||
Pillow==11.3.0
|
||||
psycopg2-binary==2.9.10
|
||||
python-dotenv==1.0.0
|
||||
python-dotenv==1.1.1
|
||||
requests==2.32.4
|
||||
six==1.17.0
|
||||
webdavclient3==3.14.6
|
||||
@@ -33,9 +33,9 @@ recipe-scrapers==15.8.0
|
||||
django-scopes==2.0.0
|
||||
django-treebeard==4.7.1
|
||||
django-cors-headers==4.6.0
|
||||
django-storages==1.14.2
|
||||
django-storages==1.14.6
|
||||
boto3==1.28.75
|
||||
django-prometheus==2.3.1
|
||||
django-prometheus==2.4.1
|
||||
django-hCaptcha==0.2.0
|
||||
python-ldap==3.4.4
|
||||
django-auth-ldap==4.6.0
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-simple-calendar": "7.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.9.3"
|
||||
"vuetify": "^3.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
@@ -35,10 +35,10 @@
|
||||
"esbuild-register": "^3.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.2",
|
||||
"vite": "7.1.5",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8",
|
||||
"vue-tsc": "^3.0.6",
|
||||
"workbox-background-sync": "^7.3.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-core": "^7.3.0",
|
||||
|
||||
@@ -156,13 +156,16 @@ const router = useRouter()
|
||||
const isPrintMode = useMediaQuery('print')
|
||||
|
||||
onMounted(() => {
|
||||
useUserPreferenceStore()
|
||||
useUserPreferenceStore().init()
|
||||
})
|
||||
|
||||
/**
|
||||
* global title update handler, might be overridden by page specific handlers
|
||||
*/
|
||||
router.afterEach((to, from) => {
|
||||
if(to.name == 'StartPage' && !useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined &&!useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
|
||||
router.push({name: 'WelcomePage'})
|
||||
}
|
||||
nextTick(() => {
|
||||
if (to.meta.title) {
|
||||
title.value = t(to.meta.title)
|
||||
|
||||
@@ -18,6 +18,7 @@ let routes = [
|
||||
{path: '/', component: () => import("@/pages/StartPage.vue"), name: 'StartPage' },
|
||||
{path: '/search', redirect: {name: 'StartPage'}},
|
||||
{path: '/test', component: () => import("@/pages/TestPage.vue"), name: 'view_test'},
|
||||
{path: '/welcome', component: () => import("@/pages/WelcomePage.vue"), name: 'WelcomePage', meta: {title: 'Welcome'}},
|
||||
{path: '/help', component: () => import("@/pages/HelpPage.vue"), name: 'HelpPage', meta: {title: 'Help'}},
|
||||
{
|
||||
path: '/settings', component: () => import("@/pages/SettingsPage.vue"), name: 'SettingsPage', redirect: '/settings/account',
|
||||
@@ -28,8 +29,6 @@ let routes = [
|
||||
{path: 'meal-plan', component: () => import("@/components/settings/MealPlanSettings.vue"), name: 'MealPlanSettings', meta: {title: 'Settings'}},
|
||||
{path: 'search', component: () => import("@/components/settings/SearchSettings.vue"), name: 'SearchSettings', meta: {title: 'Settings'}},
|
||||
{path: 'space', component: () => import("@/components/settings/SpaceSettings.vue"), name: 'SpaceSettings', meta: {title: 'Settings'}},
|
||||
{path: 'space-members', component: () => import("@/components/settings/SpaceMemberSettings.vue"), name: 'SpaceMemberSettings', meta: {title: 'Settings'}},
|
||||
{path: 'user-space', component: () => import("@/components/settings/UserSpaceSettings.vue"), name: 'UserSpaceSettings', meta: {title: 'Settings'}},
|
||||
{path: 'open-data-import', component: () => import("@/components/settings/OpenDataImportSettings.vue"), name: 'OpenDataImportSettings', meta: {title: 'Settings'}},
|
||||
{path: 'export', component: () => import("@/components/settings/ExportDataSettings.vue"), name: 'ExportDataSettings', meta: {title: 'Settings'}},
|
||||
{path: 'api', component: () => import("@/components/settings/ApiSettings.vue"), name: 'ApiSettings', meta: {title: 'Settings'}},
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 13 KiB |
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>
|
||||
@@ -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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
|
||||
<v-row v-if=" props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
|
||||
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
|
||||
<v-col>
|
||||
<v-progress-linear :model-value="(props.importLog.importedRecipes/props.importLog.totalRecipes)*100" height="24" color="primary">
|
||||
{{ props.importLog.importedRecipes }} / {{ props.importLog.totalRecipes }}
|
||||
@@ -8,9 +8,9 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
|
||||
<v-col>
|
||||
<v-textarea :model-value="importLog.msg" max-rows="25" auto-grow></v-textarea>
|
||||
<v-textarea :model-value="importLog.msg" max-rows="25" :loading="importLog.running" auto-grow></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<recipe-image :height="itemHeight" :width="itemHeight" :recipe="mealPlan.recipe"></recipe-image>
|
||||
</div>
|
||||
<div class="flex-column flex-grow-0 pa-1">
|
||||
<span class="font-light" :class="{'two-line-text': detailedItems,'one-line-text': !detailedItems,}">
|
||||
<span class="font-light" :class="{'three-line-text': detailedItems,'one-line-text': !detailedItems,}">
|
||||
<i class="fas fa-shopping-cart fa-xs float-left" v-if="mealPlan.shopping"/>
|
||||
{{ itemTitle }}
|
||||
</span>
|
||||
@@ -82,4 +82,13 @@ const itemTitle = computed(() => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.three-line-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,9 +98,9 @@ const planItems = computed(() => {
|
||||
*/
|
||||
const calendarItemHeight = computed(() => {
|
||||
if (lgAndUp.value && useUserPreferenceStore().deviceSettings.mealplan_displayPeriod == 'week') {
|
||||
return '2.6rem'
|
||||
return '3.5rem'
|
||||
} else {
|
||||
return '1.3rem'
|
||||
return '1.6rem'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ const hasFoodProperties = computed(() => {
|
||||
let propertiesFound = false
|
||||
for (const [key, fp] of Object.entries(recipe.value.foodProperties)) {
|
||||
if (fp.total_value !== 0) {
|
||||
console.log(fp, fp.total_value)
|
||||
propertiesFound = true
|
||||
}
|
||||
}
|
||||
@@ -189,7 +190,7 @@ const dialogProperty = ref<undefined | PropertyWrapper>(undefined)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (!hasFoodProperties) {
|
||||
if (!hasFoodProperties.value) {
|
||||
sourceSelectedToShow.value = "recipe"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,9 +33,8 @@
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ c.createdBy.displayName }}
|
||||
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ c.comment }}</v-list-item-subtitle>
|
||||
<span>{{ c.comment }}</span>
|
||||
|
||||
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
|
||||
|
||||
@@ -49,7 +48,7 @@
|
||||
<template #append>
|
||||
<v-list-item-action class="flex-column align-end">
|
||||
<v-rating density="comfortable" size="x-small" color="tandoor" v-model="c.rating" half-increments readonly
|
||||
v-if="c.rating != undefined"></v-rating>
|
||||
v-if="c.rating != undefined" style="overflow: hidden"></v-rating>
|
||||
<v-spacer></v-spacer>
|
||||
<v-tooltip location="top" :text="DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_MED)" v-if="c.createdAt != undefined">
|
||||
<template v-slot:activator="{ props }">
|
||||
@@ -121,6 +120,7 @@ function recLoadCookLog(recipeId: number, page: number = 1) {
|
||||
* reset new cook log from with proper defaults
|
||||
*/
|
||||
function resetForm() {
|
||||
newCookLog.value = {} as CookLog
|
||||
newCookLog.value.servings = props.recipe.servings
|
||||
newCookLog.value.createdAt = new Date()
|
||||
newCookLog.value.recipe = props.recipe.id!
|
||||
|
||||
@@ -121,11 +121,16 @@
|
||||
<template v-if="recipe.filePath">
|
||||
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
|
||||
|
||||
<v-card :title="$t('AI')" prepend-icon="$ai" @click="aiConvertRecipe()" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading"
|
||||
<v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
|
||||
v-if="!recipe.internal">
|
||||
<v-card-text>
|
||||
Convert the recipe using AI
|
||||
{{$t('ConvertUsingAI')}}
|
||||
|
||||
<model-select model="AiProvider" v-model="selectedAiProvider">
|
||||
<template #append>
|
||||
<v-btn @click="aiConvertRecipe()" icon="fa-solid fa-person-running" color="success"></v-btn>
|
||||
</template>
|
||||
</model-select>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -191,7 +196,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
|
||||
import {ApiApi, Recipe} from "@/openapi"
|
||||
import {AiProvider, ApiApi, Recipe} from "@/openapi"
|
||||
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
|
||||
import StepsOverview from "@/components/display/StepsOverview.vue";
|
||||
import RecipeActivity from "@/components/display/RecipeActivity.vue";
|
||||
@@ -207,6 +212,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
import {useFileApi} from "@/composables/useFileApi.ts";
|
||||
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
|
||||
const {request, release} = useWakeLock()
|
||||
const {doAiImport, fileApiLoading} = useFileApi()
|
||||
@@ -217,6 +223,8 @@ const recipe = defineModel<Recipe>({required: true})
|
||||
const servings = ref(1)
|
||||
const showFullRecipeName = ref(false)
|
||||
|
||||
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
|
||||
|
||||
/**
|
||||
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
|
||||
*/
|
||||
@@ -249,7 +257,7 @@ onBeforeUnmount(() => {
|
||||
function aiConvertRecipe() {
|
||||
let api = new ApiApi()
|
||||
|
||||
doAiImport(null, '', recipe.value.id!).then(r => {
|
||||
doAiImport(selectedAiProvider.value.id!,null, '', recipe.value.id!).then(r => {
|
||||
if (r.recipe) {
|
||||
recipe.value.internal = true
|
||||
recipe.value.steps = r.recipe.steps
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-list-item class="swipe-container border-t-sm" :id="itemContainerId" @touchend="handleSwipe()"
|
||||
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;"
|
||||
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
|
||||
>
|
||||
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
|
||||
@@ -7,15 +7,15 @@
|
||||
<!-- </div>-->
|
||||
|
||||
|
||||
<div class="flex-grow-1 p-2" @click="dialog = true;">
|
||||
<div class="flex-grow-1 p-2">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2">
|
||||
<div class="d-flex flex-column pr-2 pl-4">
|
||||
<span v-for="a in amounts" v-bind:key="a.key">
|
||||
<span>
|
||||
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
|
||||
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
|
||||
<span :class="{'text-disabled': a.checked || a.delayed}" class="text-no-wrap">
|
||||
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1)">{{ $n(a.amount) }}</span>
|
||||
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1) || a.unit">{{ $n(a.amount) }}</span>
|
||||
<span class="ms-1" v-if="a.unit">{{ pluralString(a.unit, a.amount) }}</span>
|
||||
</span>
|
||||
</b>
|
||||
@@ -30,10 +30,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template v-slot:[checkBtnSlot]>
|
||||
<v-btn color="success" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);"
|
||||
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
|
||||
</v-btn>
|
||||
<div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);">
|
||||
<v-btn color="success" size="large"
|
||||
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
|
||||
</v-btn>
|
||||
</div>
|
||||
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
|
||||
</template>
|
||||
|
||||
|
||||
72
vue3/src/components/display/SpaceLimitsInfo.vue
Normal file
72
vue3/src/components/display/SpaceLimitsInfo.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-row v-if="props.space.name != undefined">
|
||||
<v-col cols="12" md="4">
|
||||
<v-card :to="{name: 'SearchPage'}">
|
||||
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
|
||||
<v-card-text>{{ $n(props.space.recipeCount) }} / {{ props.space.maxRecipes == 0 ? '∞' : $n(props.space.maxRecipes) }}</v-card-text>
|
||||
<v-progress-linear :color="isSpaceAboveRecipeLimit(props.space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(props.space.recipeCount / props.space.maxRecipes) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card :to="{name: 'ModelListPage', params: {model: 'UserSpace'}}">
|
||||
|
||||
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
|
||||
<v-card-text>{{ $n(props.space.userCount) }} / {{ props.space.maxUsers == 0 ? '∞' : $n(props.space.maxUsers) }}</v-card-text>
|
||||
<v-progress-linear :color="isSpaceAboveUserLimit(props.space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(props.space.userCount / props.space.maxUsers) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card :to="{name: 'ModelListPage', params: {model: 'UserFile'}}">
|
||||
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
|
||||
<v-card-text v-if="props.space.maxFileStorageMb > -1">{{ $n(Math.round(props.space.fileSizeMb)) }} /
|
||||
{{ props.space.maxFileStorageMb == 0 ? '∞' : $n(props.space.maxFileStorageMb) }}
|
||||
MB
|
||||
</v-card-text>
|
||||
<v-card-text v-if="props.space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
|
||||
<v-progress-linear v-if="props.space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(props.space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(props.space.fileSizeMb / props.space.maxFileStorageMb) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
|
||||
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('MonthlyCredits') }}</v-card-title>
|
||||
<v-card-text>{{ $n(props.space.aiMonthlyCreditsUsed) }} / {{ $n(props.space.aiCreditsMonthly) }} {{ $t('Credits') }}
|
||||
</v-card-text>
|
||||
<v-progress-linear :model-value="props.space.aiMonthlyCreditsUsed" :max="props.space.aiCreditsMonthly" height="10"
|
||||
></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
|
||||
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('AiCreditsBalance') }}</v-card-title>
|
||||
<v-card-text>{{ $n(props.space.aiCreditsBalance) }} {{ $t('Credits') }}
|
||||
</v-card-text>
|
||||
<v-progress-linear height="10"
|
||||
></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {PropType} from "vue";
|
||||
import {Space} from "@/openapi";
|
||||
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils.ts";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
|
||||
const props = defineProps({
|
||||
space: {type: {} as PropType<Space>, required: true},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -71,7 +71,7 @@ const mergedIngredients = computed(() => {
|
||||
// Add ingredients from steps
|
||||
props.steps.forEach(step => {
|
||||
step.ingredients.forEach(ingredient => {
|
||||
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
|
||||
if (ingredient.food && !ingredient.isHeader ) {
|
||||
ingredients.push(ingredient);
|
||||
}
|
||||
});
|
||||
@@ -80,7 +80,7 @@ const mergedIngredients = computed(() => {
|
||||
if (step.stepRecipeData) {
|
||||
step.stepRecipeData.steps?.forEach((subStep: Step) => {
|
||||
subStep.ingredients.forEach((ingredient: Ingredient) => {
|
||||
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
|
||||
if (ingredient.food && !ingredient.isHeader) {
|
||||
ingredients.push(ingredient);
|
||||
}
|
||||
});
|
||||
|
||||
47
vue3/src/components/display/ThankYouNote.vue
Normal file
47
vue3/src/components/display/ThankYouNote.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
|
||||
<v-alert-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
|
||||
{{ $t('ThankYou') }}!
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert-title>
|
||||
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
|
||||
</v-alert>
|
||||
|
||||
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
|
||||
<v-alert-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
|
||||
{{ $t('ThankYou') }}!
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
|
||||
</v-btn>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert-title>
|
||||
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
|
||||
</v-alert>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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)
|
||||
|
||||
51
vue3/src/components/inputs/LanguageSelect.vue
Normal file
51
vue3/src/components/inputs/LanguageSelect.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<v-select
|
||||
:label="$t('Language')"
|
||||
v-model="$i18n.locale"
|
||||
:items="availableLocalizations"
|
||||
item-title="language"
|
||||
item-value="code"
|
||||
@update:model-value="updateLanguage()"
|
||||
></v-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ApiApi, Localization} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const availableLocalizations = ref([] as Localization[])
|
||||
const {locale} = useI18n()
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiLocalizationList().then(r => {
|
||||
availableLocalizations.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* update the django language cookie
|
||||
* this is used by django to inject the language into the template which in turn
|
||||
* sets the frontend language in i18n.ts when the frontend is initialized
|
||||
*/
|
||||
function updateLanguage() {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
|
||||
location.reload()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ApiApi, UserFile, UserFileFromJSON} from "@/openapi";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {DateTime} from "luxon";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {getCookie} from "@/utils/cookie";
|
||||
@@ -131,8 +131,13 @@ const tableHeaders = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
//TODO move to open function of file tab
|
||||
loadFiles()
|
||||
|
||||
})
|
||||
|
||||
watch(() => dialog.value, (value, oldValue) => {
|
||||
if (value && !oldValue) {
|
||||
loadFiles()
|
||||
}
|
||||
})
|
||||
|
||||
function loadFiles() {
|
||||
|
||||
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>
|
||||
@@ -54,7 +54,7 @@
|
||||
<properties-editor v-model="editingObj.properties" :amount-for="propertiesAmountFor"></properties-editor>
|
||||
|
||||
<!-- TODO remove once append to body for model select is working properly -->
|
||||
<v-spacer style="margin-top: 60px;"></v-spacer>
|
||||
<v-spacer style="margin-top: 80px;"></v-spacer>
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</v-card>
|
||||
</v-form>
|
||||
<!-- TODO remove once append to body for model select is working properly -->
|
||||
<v-spacer style="margin-top: 60px;"></v-spacer>
|
||||
<v-spacer style="margin-top: 80px;"></v-spacer>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="hierarchy">
|
||||
@@ -119,6 +119,9 @@
|
||||
mode="tags"></ModelSelect>
|
||||
<ModelSelect model="FoodInheritField" v-model="editingObj.childInheritFields" :label="$t('ChildInheritFields')" :hint="$t('ChildInheritFields_help')"
|
||||
mode="tags"></ModelSelect>
|
||||
|
||||
<!-- TODO remove once append to body for model select is working properly -->
|
||||
<v-spacer style="margin-top: 100px;"></v-spacer>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="misc">
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
142
vue3/src/components/model_editors/SpaceEditor.vue
Normal file
142
vue3/src/components/model_editors/SpaceEditor.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<model-editor-base
|
||||
:loading="loading"
|
||||
:dialog="dialog"
|
||||
@save="saveObject"
|
||||
@delete="deleteObject"
|
||||
@close="emit('close'); editingObjChanged = false"
|
||||
:is-update="isUpdate()"
|
||||
:is-changed="editingObjChanged"
|
||||
:model-class="modelClass"
|
||||
:object-name="editingObjName()">
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-tabs v-model="tab" :disabled="loading" grow>
|
||||
<v-tab value="space">{{ $t('Space') }}</v-tab>
|
||||
<v-tab value="cosmetic">{{ $t('Cosmetic') }}</v-tab>
|
||||
<v-tab value="ai">{{ $t('AI') }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-tabs-window v-model="tab">
|
||||
<v-tabs-window-item value="space">
|
||||
<v-form :disabled="loading">
|
||||
|
||||
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
|
||||
|
||||
<user-file-field v-model="editingObj.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
|
||||
|
||||
<v-textarea v-model="editingObj.message" :label="$t('Message')" clearable></v-textarea>
|
||||
|
||||
<space-limits-info :space="editingObj" :show-thank-you="false" v-if="isUpdate()"></space-limits-info>
|
||||
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="cosmetic">
|
||||
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
|
||||
<v-color-picker v-model="editingObj.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
|
||||
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
|
||||
<v-btn class="mb-4" @click="editingObj.navBgColor = ''">{{ $t('Reset') }}</v-btn>
|
||||
|
||||
<user-file-field v-model="editingObj.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
|
||||
|
||||
<user-file-field v-model="editingObj.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
|
||||
<user-file-field v-model="editingObj.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
|
||||
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="ai">
|
||||
<p class="text-disabled font-italic text-body-2">
|
||||
<span v-if="useUserPreferenceStore().serverSettings.hosted">
|
||||
{{ $t('AISettingsHostedHelp') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('SettingsOnlySuperuser') }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<v-checkbox v-model="editingObj.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
|
||||
|
||||
<template v-if="editingObj.aiEnabled">
|
||||
<model-select model="AiProvider" :label="$t('Default')" v-model="editingObj.aiDefaultProvider"></model-select>
|
||||
|
||||
<v-number-input v-model="editingObj.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')"
|
||||
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
|
||||
<v-number-input v-model="editingObj.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')"
|
||||
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
|
||||
|
||||
</template>
|
||||
</v-tabs-window-item>
|
||||
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
</model-editor-base>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType, ref, watch} from "vue";
|
||||
import {ApiApi, ConnectorConfig, Space} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import UserFileField from "@/components/inputs/UserFileField.vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import editor from "mavon-editor";
|
||||
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<Space>, required: false, default: null},
|
||||
itemId: {type: [Number, String], required: false, default: undefined},
|
||||
itemDefaults: {type: {} as PropType<Space>, required: false, default: {} as Space},
|
||||
dialog: {type: Boolean, default: false}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
|
||||
const {
|
||||
setupState,
|
||||
deleteObject,
|
||||
saveObject,
|
||||
isUpdate,
|
||||
editingObjName,
|
||||
loading,
|
||||
editingObj,
|
||||
editingObjChanged,
|
||||
modelClass
|
||||
} = useModelEditorFunctions<Space>('Space', emit)
|
||||
|
||||
/**
|
||||
* watch prop changes and re-initialize editor
|
||||
* required to embed editor directly into pages and be able to change item from the outside
|
||||
*/
|
||||
watch([() => props.item, () => props.itemId], () => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
// object specific data (for selects/display)
|
||||
|
||||
const tab = ref("space")
|
||||
|
||||
onMounted(() => {
|
||||
initializeEditor()
|
||||
})
|
||||
|
||||
/**
|
||||
* component specific state setup logic
|
||||
*/
|
||||
function initializeEditor() {
|
||||
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<v-form>
|
||||
<p class="text-h6">{{ $t('Profile') }}</p>
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
<v-text-field :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
|
||||
|
||||
<thank-you-note></thank-you-note>
|
||||
|
||||
<v-text-field class="mt-3" :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
|
||||
|
||||
<!-- <v-label>Avatar</v-label><br/>-->
|
||||
<!-- <v-avatar class="mt-3 mb-3" style="height: 10vh; width: 10vh" color="info">V</v-avatar> Feature coming in a future Version of Tandoor.-->
|
||||
@@ -39,6 +42,7 @@ import {ApiApi, User} from "@/openapi";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls";
|
||||
import ThankYouNote from "@/components/display/ThankYouNote.vue";
|
||||
|
||||
const {getDjangoUrl} = useDjangoUrls()
|
||||
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
<p class="text-h6">{{ $t('Cosmetic') }}</p>
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
|
||||
<v-select
|
||||
:label="$t('Language')"
|
||||
v-model="$i18n.locale"
|
||||
:items="availableLocalizations"
|
||||
item-title="language"
|
||||
item-value="code"
|
||||
@update:model-value="updateLanguage()"
|
||||
></v-select>
|
||||
<language-select></language-select>
|
||||
|
||||
<v-label>{{$t('Nav_Color')}}</v-label>
|
||||
<v-color-picker v-model="useUserPreferenceStore().userSettings.navBgColor" mode="hex" :modes="['hex']" show-swatches :swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
|
||||
@@ -54,10 +47,10 @@ import {ApiApi, Localization} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import LanguageSelect from "@/components/inputs/LanguageSelect.vue";
|
||||
|
||||
const {locale, t} = useI18n()
|
||||
const {t} = useI18n()
|
||||
|
||||
const availableLocalizations = ref([] as Localization[])
|
||||
const availableDefaultPages = ref([
|
||||
{page: 'SEARCH', label: t('Search')},
|
||||
{page: 'SHOPPING', label: t('Shopping_list')},
|
||||
@@ -65,29 +58,10 @@ const availableDefaultPages = ref([
|
||||
{page: 'BOOKS', label: t('Books')},
|
||||
])
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiLocalizationList().then(r => {
|
||||
availableLocalizations.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* update the django language cookie
|
||||
* this is used by django to inject the language into the template which in turn
|
||||
* sets the frontend language in i18n.ts when the frontend is initialized
|
||||
*/
|
||||
function updateLanguage() {
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
|
||||
location.reload()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<p class="text-h6">{{ $t('Open_Data_Import') }}</p>
|
||||
<p class="text-h4">{{ $t('Open_Data_Import') }}</p>
|
||||
<v-divider></v-divider>
|
||||
<p class="text-subtitle-2">{{ $t('Data_Import_Info') }}</p>
|
||||
<v-btn href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</v-btn>
|
||||
<p class="text-subtitle-1">{{ $t('Data_Import_Info') }} <a href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</a></p>
|
||||
|
||||
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-2" v-model="requestData.selectedVersion" :loading="loading"></v-select>
|
||||
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-4" v-model="requestData.selectedVersion" :loading="loading"></v-select>
|
||||
|
||||
<v-row v-if="requestData.selectedVersion">
|
||||
<v-col>
|
||||
@@ -29,10 +28,10 @@
|
||||
<td>{{ metadata[requestData.selectedVersion][d] }}</td>
|
||||
<td>
|
||||
<template v-if="responseData[d]">
|
||||
<i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }} <br/>
|
||||
<i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }} <br/>
|
||||
<i class="fas fa-forward"></i> {{ responseData[d].totalUntouched}} {{ $t('Unchanged') }} <br/>
|
||||
<i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}
|
||||
<p v-if="responseData[d].totalCreated > 0" ><i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }}</p>
|
||||
<p v-if="responseData[d].totalUpdated > 0"><i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }}</p>
|
||||
<p v-if="responseData[d].totalUntouched > 0"><i class="fas fa-forward"></i> {{ responseData[d].totalUntouched }} {{ $t('Unchanged') }}</p>
|
||||
<p v-if="responseData[d].totalErrored > 0"><i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}</p>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -102,7 +101,6 @@ function importOpenData() {
|
||||
})
|
||||
|
||||
api.apiImportOpenDataCreate({importOpenData: requestData.value}).then(r => {
|
||||
console.log(r)
|
||||
responseData.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<v-form>
|
||||
<p class="text-h6">{{ $t('SpaceMembers') }}</p>
|
||||
<v-divider></v-divider>
|
||||
<p class="text-subtitle-2">{{ $t('SpaceMemberHelp') }}</p>
|
||||
|
||||
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10" class="mt-3">
|
||||
<template #item.groups="{item}">
|
||||
<span v-for="g in item.groups">{{ g.name }} </span>
|
||||
</template>
|
||||
|
||||
<template #item.edit="{item}">
|
||||
<v-btn color="edit" size="small" v-if="item.user.id != useUserPreferenceStore().activeSpace.createdBy.id">
|
||||
<v-icon icon="$edit"></v-icon>
|
||||
<model-edit-dialog model="UserSpace" :item="item" @delete="deleteUserSpace(item)" class="mt-2"></model-edit-dialog>
|
||||
</v-btn>
|
||||
<v-chip color="edit" v-else>{{ $t('Owner') }}</v-chip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<p class="text-h6 mt-3">{{ $t('Invites') }}
|
||||
<v-btn size="small" class="float-right" prepend-icon="$create" color="create">
|
||||
{{ $t('New') }}
|
||||
<model-edit-dialog model="InviteLink" @delete="deleteInviteLink" @create="item => spaceInviteLinks.push(item)" class="mt-2"></model-edit-dialog>
|
||||
</v-btn>
|
||||
</p>
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
|
||||
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
|
||||
<template #item.reusable="{item}">
|
||||
<v-icon icon="fa-solid fa-check" color="success" v-if="item.reusable"></v-icon>
|
||||
<v-icon icon="fa-solid fa-times" color="error" v-if="!item.reusable"></v-icon>
|
||||
</template>
|
||||
|
||||
<template #item.edit="{item}">
|
||||
<btn-copy size="small" :copy-value="inviteLinkUrl(item)" class="me-1"></btn-copy>
|
||||
<v-btn color="edit" size="small">
|
||||
<v-icon icon="$edit"></v-icon>
|
||||
<model-edit-dialog model="InviteLink" :item="item" @delete="deleteInviteLink(item)" class="mt-2"></model-edit-dialog>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ApiApi, InviteLink, UserSpace} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import BtnCopy from "@/components/buttons/BtnCopy.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const spaceUserSpaces = ref([] as UserSpace[])
|
||||
const spaceInviteLinks = ref([] as InviteLink[])
|
||||
|
||||
const userTableHeaders = [
|
||||
{title: t('Username'), key: 'user.username'},
|
||||
{title: t('Role'), key: 'groups'},
|
||||
{title: t('Edit'), key: 'edit', align: 'end'},
|
||||
]
|
||||
|
||||
const inviteTableHeaders = [
|
||||
{title: 'ID', key: 'id'},
|
||||
{title: t('Email'), key: 'email'},
|
||||
{title: t('Role'), key: 'group.name'},
|
||||
{title: t('Reusable'), key: 'reusable'},
|
||||
{title: t('Edit'), key: 'edit', align: 'end'},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiUserSpaceList().then(r => {
|
||||
spaceUserSpaces.value = r.results
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
|
||||
api.apiInviteLinkList({unused: true}).then(r => {
|
||||
spaceInviteLinks.value = r.results
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* delete userspace from client list (database handled by editor)
|
||||
* @param userSpace UserSpace object that was deleted
|
||||
*/
|
||||
function deleteUserSpace(userSpace: UserSpace) {
|
||||
spaceUserSpaces.value.splice(spaceUserSpaces.value.indexOf(userSpace) - 1, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete invite link from client list (database handled by editor)
|
||||
* @param inviteLink InviteLink object that was deleted
|
||||
*/
|
||||
function deleteInviteLink(inviteLink: InviteLink) {
|
||||
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink) - 1, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* returns url for invite link
|
||||
* @param inviteLink InviteLink object to create url for
|
||||
*/
|
||||
function inviteLinkUrl(inviteLink: InviteLink) {
|
||||
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,144 +3,17 @@
|
||||
<p class="text-h6">{{ useUserPreferenceStore().activeSpace.name }}</p>
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
|
||||
<v-row v-if="space.name != undefined">
|
||||
<v-col cols="12" md="4">
|
||||
<v-card>
|
||||
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
|
||||
<v-card-text>{{ $n(space.recipeCount) }} / {{ space.maxRecipes == 0 ? '∞' : $n(space.maxRecipes) }}</v-card-text>
|
||||
<v-progress-linear :color="isSpaceAboveRecipeLimit(space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(space.recipeCount / space.maxRecipes) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card>
|
||||
|
||||
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
|
||||
<v-card-text>{{ $n(space.userCount) }} / {{ space.maxUsers == 0 ? '∞' : $n(space.maxUsers) }}</v-card-text>
|
||||
<v-progress-linear :color="isSpaceAboveUserLimit(space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(space.userCount / space.maxUsers) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card>
|
||||
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
|
||||
<v-card-text v-if="space.maxFileStorageMb > -1">{{ $n(Math.round(space.fileSizeMb)) }} / {{ space.maxFileStorageMb == 0 ? '∞' : $n(space.maxFileStorageMb) }}
|
||||
MB
|
||||
</v-card-text>
|
||||
<v-card-text v-if="space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
|
||||
<v-progress-linear v-if="space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(space) ? 'error' : 'success'" height="10"
|
||||
:model-value="(space.fileSizeMb / space.maxFileStorageMb) * 100"></v-progress-linear>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="mt-3 mb-3"></v-divider>
|
||||
|
||||
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
|
||||
<v-alert-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
|
||||
{{ $t('ThankYou') }}!
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert-title>
|
||||
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
|
||||
</v-alert>
|
||||
|
||||
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
|
||||
<v-alert-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
|
||||
{{ $t('ThankYou') }}!
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
|
||||
</v-btn>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-alert-title>
|
||||
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
|
||||
</v-alert>
|
||||
|
||||
|
||||
<p class="text-h6 mt-2">{{ $t('Settings') }}</p>
|
||||
<v-divider class="mb-2"></v-divider>
|
||||
|
||||
<user-file-field v-model="space.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
|
||||
|
||||
|
||||
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
|
||||
|
||||
<!-- <model-select v-model="space.foodInherit" model="FoodInheritField" mode="tags"></model-select>-->
|
||||
|
||||
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
|
||||
|
||||
<v-divider class="mt-4 mb-2"></v-divider>
|
||||
<h2>{{$t('Cosmetic')}}</h2>
|
||||
<span>{{$t('Space_Cosmetic_Settings')}}</span>
|
||||
|
||||
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
|
||||
<v-color-picker v-model="space.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
|
||||
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
|
||||
<v-btn class="mb-4" @click="space.navBgColor = ''">{{$t('Reset')}}</v-btn>
|
||||
|
||||
<user-file-field v-model="space.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
|
||||
|
||||
<user-file-field v-model="space.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
|
||||
<user-file-field v-model="space.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
|
||||
<user-file-field v-model="space.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
|
||||
|
||||
|
||||
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
|
||||
<space-editor :item-id="useUserPreferenceStore().activeSpace.id!"></space-editor>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ApiApi, Space} from "@/openapi";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import UserFileField from "@/components/inputs/UserFileField.vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils";
|
||||
|
||||
const space = ref({} as Space)
|
||||
|
||||
onMounted(() => {
|
||||
let api = new ApiApi()
|
||||
api.apiSpaceCurrentRetrieve().then(r => {
|
||||
space.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
|
||||
function updateSpace() {
|
||||
let api = new ApiApi()
|
||||
api.apiSpacePartialUpdate({id: space.value.id, patchedSpace: space.value}).then(r => {
|
||||
space.value = r
|
||||
useUserPreferenceStore().activeSpace = Object.assign({}, space.value)
|
||||
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS, space.value)
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
})
|
||||
}
|
||||
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
|
||||
import SpaceEditor from "@/components/model_editors/SpaceEditor.vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<p class="text-h6">
|
||||
{{ $t('YourSpaces') }}
|
||||
<v-btn color="create" prepend-icon="$add" class="float-right" size="small" :href="getDjangoUrl('space-overview')">{{$t('New')}}</v-btn>
|
||||
</p>
|
||||
<v-divider></v-divider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="6" v-for="s in spaces" :key="s.id">
|
||||
<v-card @click="useUserPreferenceStore().switchSpace(s)">
|
||||
<v-img height="200px" cover :src="(s.image !== undefined) ? s.image?.preview : recipeDefaultImage" :alt="$t('Image')"></v-img>
|
||||
<v-card-title>{{ s.name }}
|
||||
<v-chip variant="tonal" density="compact" color="error" v-if="s.id == useUserPreferenceStore().activeSpace.id">{{ $t('active') }}</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>{{ $t('created_by') }} {{ s.createdBy.displayName }}</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ApiApi, Space} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import recipeDefaultImage from '../../assets/recipe_no_image.svg'
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls";
|
||||
|
||||
const {getDjangoUrl} = useDjangoUrls()
|
||||
|
||||
const spaces = ref([] as Space[])
|
||||
|
||||
onMounted(() => {
|
||||
const api = new ApiApi()
|
||||
|
||||
api.apiSpaceList().then(r => {
|
||||
spaces.value = r.results
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
import {useDjangoUrls} from "@/composables/useDjangoUrls";
|
||||
import {ref} from "vue";
|
||||
import {getCookie} from "@/utils/cookie";
|
||||
import {RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
|
||||
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
|
||||
import {tr} from "vuetify/locale";
|
||||
|
||||
|
||||
/**
|
||||
@@ -86,7 +87,7 @@ export function useFileApi() {
|
||||
* @param text text to import
|
||||
* @param recipeId id of a recipe to use as import base (for external recipes
|
||||
*/
|
||||
function doAiImport(file: File | null, text: string = '', recipeId: string = '') {
|
||||
function doAiImport(providerId: number, file: File | null, text: string = '', recipeId: string = '') {
|
||||
let formData = new FormData()
|
||||
|
||||
if (file != null) {
|
||||
@@ -96,6 +97,7 @@ export function useFileApi() {
|
||||
}
|
||||
formData.append('text', text)
|
||||
formData.append('recipe_id', recipeId)
|
||||
formData.append('ai_provider_id', providerId)
|
||||
fileApiLoading.value = true
|
||||
|
||||
return fetch(getDjangoUrl(`api/ai-import/`), {
|
||||
@@ -116,12 +118,18 @@ export function useFileApi() {
|
||||
* @param files array to import
|
||||
* @param app app to import
|
||||
* @param includeDuplicates if recipes that were found as duplicates should be imported as well
|
||||
* @param mealPlans if meal plans should be imported
|
||||
* @param shoppingLists if shopping lists should be imported
|
||||
* @param nutritionPerServing if nutrition information should be treated as per serving (if false its treated as per recipe)
|
||||
* @returns Promise resolving to the import ID of the app import
|
||||
*/
|
||||
function doAppImport(files: File[], app: string, includeDuplicates: boolean) {
|
||||
function doAppImport(files: File[], app: string, includeDuplicates: boolean, mealPlans: boolean = true, shoppingLists: boolean = true, nutritionPerServing: boolean = false,) {
|
||||
let formData = new FormData()
|
||||
formData.append('type', app);
|
||||
formData.append('duplicates', includeDuplicates ? 'true' : 'false')
|
||||
formData.append('meal_plans', mealPlans ? 'true' : 'false')
|
||||
formData.append('shopping_lists', shoppingLists ? 'true' : 'false')
|
||||
formData.append('nutrition_per_serving', nutritionPerServing ? 'true' : 'false')
|
||||
files.forEach(file => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
@@ -140,4 +148,4 @@ export function useFileApi() {
|
||||
}
|
||||
|
||||
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, doAiImport, doAppImport}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Active": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
@@ -14,6 +16,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "",
|
||||
@@ -44,6 +52,7 @@
|
||||
"Color": "",
|
||||
"Coming_Soon": "",
|
||||
"Completed": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -51,6 +60,8 @@
|
||||
"CountMore": "",
|
||||
"Create": "",
|
||||
"Create Food": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
@@ -58,6 +69,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -102,10 +114,14 @@
|
||||
"FoodOnHand": "",
|
||||
"Food_Alias": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
@@ -121,6 +137,9 @@
|
||||
"IgnoredFood": "",
|
||||
"Image": "",
|
||||
"Import": "",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -146,8 +165,11 @@
|
||||
"Keyword": "",
|
||||
"Keyword_Alias": "",
|
||||
"Keywords": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Make_Header": "",
|
||||
@@ -165,6 +187,8 @@
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -193,6 +217,8 @@
|
||||
"NotInShopping": "",
|
||||
"Note": "",
|
||||
"Nutrition": "",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "",
|
||||
"Ok": "",
|
||||
"OnHand": "",
|
||||
@@ -256,6 +282,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Categories": "",
|
||||
"Shopping_Category": "",
|
||||
@@ -267,8 +294,13 @@
|
||||
"Show_as_header": "",
|
||||
"Single": "",
|
||||
"Size": "",
|
||||
"Skip": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Starting_Day": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -315,6 +347,8 @@
|
||||
"Website": "",
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Active": "",
|
||||
"Add": "Добави",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Добавете {food} към списъка си за пазаруване",
|
||||
@@ -14,6 +16,12 @@
|
||||
"Added_by": "Добавено от",
|
||||
"Added_on": "Добавено",
|
||||
"Advanced": "Разширено",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "Приложение",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "Сигурен ли си?",
|
||||
@@ -44,17 +52,21 @@
|
||||
"Color": "Цвят",
|
||||
"Coming_Soon": "Очаквайте скоро",
|
||||
"Completed": "Завършено",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Копиране",
|
||||
"Copy_template_reference": "Копирайте препратка към шаблона",
|
||||
"CountMore": "...+{count} още",
|
||||
"Create": "Създаване",
|
||||
"Create Food": "Създайте храна",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Създайте запис за план за хранене",
|
||||
"Create_New_Food": "Добавете нова храна",
|
||||
"Create_New_Keyword": "Добавяне на нова ключова дума",
|
||||
"Create_New_Meal_Type": "Добавете нов тип хранене",
|
||||
"Create_New_Shopping Category": "Създайте нова категория за пазаруване",
|
||||
"Create_New_Unit": "Добавяне на нова единица",
|
||||
"Credits": "",
|
||||
"Current_Period": "Текущ период",
|
||||
"Custom Filter": "Персонализиран филтър",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -99,10 +111,14 @@
|
||||
"FoodOnHand": "Имате {храна} под ръка.",
|
||||
"Food_Alias": "Псевдоним на храната",
|
||||
"Foods": "Храни",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Групирай по",
|
||||
"Hide_Food": "Скриване на храна",
|
||||
"Hide_Keyword": "Скриване на ключови думи",
|
||||
@@ -118,6 +134,9 @@
|
||||
"IgnoredFood": "{food} е настроен да игнорира пазаруването.",
|
||||
"Image": "Изображение",
|
||||
"Import": "Импортиране",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Възникна грешка по време на импортирането ви. Моля, разгънете подробностите в долната част на страницата, за да ги видите.",
|
||||
"Import_Not_Yet_Supported": "Импортирането все още не се поддържа",
|
||||
"Import_Result_Info": "Импортирани са {imported} от {total} рецепти",
|
||||
@@ -141,8 +160,11 @@
|
||||
"Keyword": "Ключова дума",
|
||||
"Keyword_Alias": "Псевдоним на ключова дума",
|
||||
"Keywords": "Ключови думи",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Връзка",
|
||||
"Load_More": "Зареди още",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Дневник на Готвене",
|
||||
"Log_Recipe_Cooking": "Дневник на Рецепта за готвене",
|
||||
"Make_Header": "Направете заглавие",
|
||||
@@ -159,6 +181,8 @@
|
||||
"Merge_Keyword": "Обединяване на ключова дума",
|
||||
"MissingProperties": "",
|
||||
"Month": "Месец",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Премести",
|
||||
"MoveCategory": "Премести към: ",
|
||||
"Move_Down": "Премести надолу",
|
||||
@@ -186,6 +210,8 @@
|
||||
"NotInShopping": "{food} не е в списъка ви за пазаруване.",
|
||||
"Note": "Бележка",
|
||||
"Nutrition": "Хранителни стойности",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Вие сте офлайн, списъкът за пазаруване може да не се синхронизира.",
|
||||
"Ok": "Отвори",
|
||||
"OnHand": "В момента под ръка",
|
||||
@@ -249,6 +275,7 @@
|
||||
"Selected": "Избрано",
|
||||
"Servings": "Порции",
|
||||
"Settings": "Настройки",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Споделяне",
|
||||
"Shopping_Categories": "Категории за пазаруване",
|
||||
"Shopping_Category": "Категория за пазаруване",
|
||||
@@ -260,8 +287,13 @@
|
||||
"Show_as_header": "Показване като заглавка",
|
||||
"Single": "Единичен",
|
||||
"Size": "Размер",
|
||||
"Skip": "",
|
||||
"Sort_by_new": "Сортиране по ново",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Starting_Day": "Начален ден от седмицата",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -306,6 +338,8 @@
|
||||
"Website": "уебсайт",
|
||||
"Week": "Седмица",
|
||||
"Week_Numbers": "Номера на седмиците",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Година",
|
||||
"Yes": "",
|
||||
"add_keyword": "Добавяне на ключова дума",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Compte",
|
||||
"Active": "",
|
||||
"Add": "Afegir",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Afegeix {food} a la llista de la compra",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Afegit per",
|
||||
"Added_on": "Afegit el",
|
||||
"Advanced": "Avançat",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alineació",
|
||||
"Amount": "Quantitat",
|
||||
"App": "Aplicació",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Mostrar comentaris",
|
||||
"Completed": "Completat",
|
||||
"Conversion": "Conversió",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copiar",
|
||||
"Copy Link": "Copiar Enllaç",
|
||||
"Copy Token": "Copiar Token",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Crear",
|
||||
"Create Food": "Crear aliment/ingredient",
|
||||
"Create Recipe": "Crear una recepta",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Crear una entrada de la planificació d'àpats",
|
||||
"Create_New_Food": "Afegir nou ingredient",
|
||||
"Create_New_Keyword": "Afegir nova Paraula Clau",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "Afegir nova Categoria de Compres",
|
||||
"Create_New_Unit": "Afegir nova unitat",
|
||||
"Created": "Creada",
|
||||
"Credits": "",
|
||||
"Current_Period": "Període Actual",
|
||||
"Custom Filter": "Filtre Personalitzat",
|
||||
"CustomImageHelp": "Carregar una imatge per mostrar a la vista general de l’espai.",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "Àlies per l'aliment",
|
||||
"Food_Replace": "Aliment equivalent",
|
||||
"Foods": "Aliments",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupat per",
|
||||
"Hide_Food": "Amagar Aliment",
|
||||
"Hide_Keyword": "Amaga les paraules clau",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "Imatge",
|
||||
"Import": "Importar",
|
||||
"Import Recipe": "Importar Recepta",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "S'ha produït un error durant la importació. Si us plau, amplia els detalls a la part inferior de la pàgina per veure'l.",
|
||||
"Import_Not_Yet_Supported": "Importació encara no suportada",
|
||||
"Import_Result_Info": "{imported} de {total} receptes s'han importat",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "Llenguatge",
|
||||
"Last_name": "Cognoms",
|
||||
"Learn_More": "Saber-me més",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Enllaç",
|
||||
"Load_More": "Carregueu-ne més",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registreu el que s'ha cuinat",
|
||||
"Log_Recipe_Cooking": "Registre de receptes",
|
||||
"Logo": "Logotip",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "Missatge",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mes",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Moure",
|
||||
"MoveCategory": "Moure a: ",
|
||||
"Move_Down": "Moveu avall",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "Nota",
|
||||
"Number of Objects": "Nombre d'Objectes",
|
||||
"Nutrition": "Valors nutricionals",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Estàs desconnectat, la llista de la compra no pot actualitzar-se.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Ja en tinc",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "Seleccionat",
|
||||
"Servings": "Racions",
|
||||
"Settings": "Opcions",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartir",
|
||||
"ShoppingBackgroundSyncWarning": "Error de la connexió, esperant per sincronitzar ...",
|
||||
"Shopping_Categories": "Categoria de compres",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "Mostreu com a títol",
|
||||
"Single": "Únic/a",
|
||||
"Size": "Mida",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Identificació amb Xarxes Socials",
|
||||
"Sort_by_new": "Ordenar a partir del més nou",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.",
|
||||
"Split_All_Steps": "Dividir totes les files en passos separats.",
|
||||
"StartDate": "Data d'inici",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "Setmana",
|
||||
"Week_Numbers": "Números de la setmana",
|
||||
"Welcome": "Benvingut/da",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Any",
|
||||
"Yes": "",
|
||||
"add_keyword": "Afegir Paraula Clau",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Účet",
|
||||
"Active": "",
|
||||
"Add": "Přidat",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Přidat {food} na váš nákupní seznam",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Přidáno uživatelem",
|
||||
"Added_on": "Přidáno v",
|
||||
"Advanced": "Rozšířené",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Zarovnání",
|
||||
"Amount": "Množství",
|
||||
"App": "Aplikace",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Zobrazit komentáře",
|
||||
"Completed": "Dokončeno",
|
||||
"Conversion": "Převod",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopírovat",
|
||||
"Copy Link": "Kopírovat odkaz",
|
||||
"Copy Token": "Kopírovat token",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Vytvořit",
|
||||
"Create Food": "Vytvořit potravinu",
|
||||
"Create Recipe": "Vytvořit recept",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Vytvořit položku v jídelníčku",
|
||||
"Create_New_Food": "Přidat novou potravinu",
|
||||
"Create_New_Keyword": "Přidat nový štítek",
|
||||
@@ -73,6 +84,7 @@
|
||||
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
|
||||
"Create_New_Shopping_Category": "Přidat novou nákupní kategorii",
|
||||
"Create_New_Unit": "Přidat novou jednotku",
|
||||
"Credits": "",
|
||||
"Current_Period": "Současné období",
|
||||
"Custom Filter": "Uživatelský filtr",
|
||||
"CustomImageHelp": "Nahrajte obrázek, který se zobrazí v přehledu prostoru.",
|
||||
@@ -142,10 +154,14 @@
|
||||
"Food_Alias": "Přezdívka potraviny",
|
||||
"Food_Replace": "Nahrazení v potravině",
|
||||
"Foods": "Potraviny",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Seskupit podle",
|
||||
"Hide_Food": "Skrýt potravinu",
|
||||
"Hide_Keyword": "Skrýt štítky",
|
||||
@@ -164,6 +180,9 @@
|
||||
"Image": "Obrázek",
|
||||
"Import": "Import",
|
||||
"Import Recipe": "Importovat recept",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Během importu došlo k chybě. Pro více informací rozbalte Detaily na konci stránky.",
|
||||
"Import_Not_Yet_Supported": "Import není zatím podporován",
|
||||
"Import_Result_Info": "{imported} z {total} receptů naimportováno",
|
||||
@@ -193,8 +212,11 @@
|
||||
"Language": "Jazyk",
|
||||
"Last_name": "Příjmení",
|
||||
"Learn_More": "Zjistit víc",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Odkaz",
|
||||
"Load_More": "Načíst další",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zaznamenat vaření",
|
||||
"Log_Recipe_Cooking": "Záznam vaření receptu",
|
||||
"Logo": "Logo",
|
||||
@@ -214,6 +236,8 @@
|
||||
"Message": "Zpráva",
|
||||
"MissingProperties": "",
|
||||
"Month": "Měsíc",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Přesunout",
|
||||
"MoveCategory": "Přesunout do: ",
|
||||
"Move_Down": "Dolů",
|
||||
@@ -249,6 +273,8 @@
|
||||
"Note": "Poznámka",
|
||||
"Number of Objects": "Počet Objektů",
|
||||
"Nutrition": "Výživové hodnoty",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Jste offline, nákupní seznam nemusí být synchronizován.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Momentálně k dispozici",
|
||||
@@ -325,6 +351,7 @@
|
||||
"Selected": "Vybrané",
|
||||
"Servings": "Porce",
|
||||
"Settings": "Nastavení",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Sdílet",
|
||||
"Shopping_Categories": "Kategorie nákupního seznamu",
|
||||
"Shopping_Category": "Kategorie nákupního seznamu",
|
||||
@@ -339,9 +366,14 @@
|
||||
"Show_as_header": "Nastav jako nadpis",
|
||||
"Single": "Jednoduchý",
|
||||
"Size": "Velikost",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
|
||||
"Sort_by_new": "Seřadit od nejnovějšího",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
|
||||
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
|
||||
"StartDate": "Počáteční datum",
|
||||
@@ -405,6 +437,8 @@
|
||||
"Week": "Týden",
|
||||
"Week_Numbers": "Číslo týdne",
|
||||
"Welcome": "Vítejte",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Rok",
|
||||
"Yes": "",
|
||||
"add_keyword": "Přidat štítek",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Bruger",
|
||||
"Active": "",
|
||||
"Add": "Tilføj",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Tilføj {food} til indkøbsliste",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Tilføjet af",
|
||||
"Added_on": "Tilføjet den",
|
||||
"Advanced": "Avanceret",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Justering",
|
||||
"Amount": "Mængde",
|
||||
"App": "App",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Vis kommentarer",
|
||||
"Completed": "Afsluttet",
|
||||
"Conversion": "Konversion",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopier",
|
||||
"Copy Link": "Kopier link",
|
||||
"Copy Token": "Kopier token",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Opret",
|
||||
"Create Food": "Opret mad",
|
||||
"Create Recipe": "Opret opskrift",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Indsæt punkt i madplan",
|
||||
"Create_New_Food": "Tilføj ny mad",
|
||||
"Create_New_Keyword": "Tilføj nyt nøgleord",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "Opret ny indkøbskategori",
|
||||
"Create_New_Unit": "Tilføj ny enhed",
|
||||
"Created": "Skabt",
|
||||
"Credits": "",
|
||||
"Current_Period": "Nuværende periode",
|
||||
"Custom Filter": "Tilpasset filter",
|
||||
"CustomImageHelp": "Upload et billede for at vise dets plade i område-oversigten.",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "Alternativt navn til mad",
|
||||
"Food_Replace": "Erstat ingrediens",
|
||||
"Foods": "Mad",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupper efter",
|
||||
"Hide_Food": "Skjul mad",
|
||||
"Hide_Keyword": "Skjul nøgleord",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "Billede",
|
||||
"Import": "Importer",
|
||||
"Import Recipe": "Importer opskrift",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Der opstod en fejl under din importering. Udvid detaljerne i bunden af siden for at se fejlen.",
|
||||
"Import_Not_Yet_Supported": "Import endnu ikke understøttet",
|
||||
"Import_Result_Info": "{imported} af {total} opskrifter blev importeret",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "Sprog",
|
||||
"Last_name": "Efternavn",
|
||||
"Learn_More": "Lær mere",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Indlæs mere",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Noter tilberedning",
|
||||
"Log_Recipe_Cooking": "Noter tilberedning af opskrift",
|
||||
"Logo": "Logo",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "Besked",
|
||||
"MissingProperties": "",
|
||||
"Month": "Måned",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Flyt",
|
||||
"MoveCategory": "Flyt til: ",
|
||||
"Move_Down": "Flyt ned",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "Note",
|
||||
"Number of Objects": "Antal objekter",
|
||||
"Nutrition": "Næring",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Du er offline, indkøbslisten er måske ikke synkroniseret.",
|
||||
"Ok": "Åben",
|
||||
"OnHand": "Til rådighed",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "Valgt",
|
||||
"Servings": "Serveringer",
|
||||
"Settings": "Indstillinger",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Del",
|
||||
"ShoppingBackgroundSyncWarning": "Dårligt netværk, afventer synkronisering ...",
|
||||
"Shopping_Categories": "Indkøbskategorier",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "Vis som rubrik",
|
||||
"Single": "Enkel",
|
||||
"Size": "Størrelse",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Social authenticering",
|
||||
"Sort_by_new": "Sorter efter nylige",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.",
|
||||
"Split_All_Steps": "Opdel rækker i separate trin.",
|
||||
"StartDate": "Startdato",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "Uge",
|
||||
"Week_Numbers": "Ugenumre",
|
||||
"Welcome": "Velkommen",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "År",
|
||||
"Yes": "",
|
||||
"add_keyword": "Tilføj nøgleord",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Verwende AI um Fotos von Rezepten zu importieren.",
|
||||
"AISettingsHostedHelp": "AI Verfügbarkeit und Credit Limits können über die Tarifverwaltung geändert werden. ",
|
||||
"API": "API",
|
||||
"APIKey": "API Schlüssel",
|
||||
"Summary": "Zusammenfassung",
|
||||
"Structured": "Strukturiert",
|
||||
"API_Browser": "API Browser",
|
||||
"API_Documentation": "API Dokumentation",
|
||||
"AccessTokenHelp": "Zugriffsschlüssel für die REST Schnittstelle.",
|
||||
"Access_Token": "Zugriffstoken",
|
||||
"Account": "Konto",
|
||||
"Actions": "Aktionen",
|
||||
"Active": "Aktiv",
|
||||
"Activity": "Aktivität",
|
||||
"Add": "Hinzufügen",
|
||||
"AddAll": "Alle Hinzufügen",
|
||||
@@ -31,6 +31,12 @@
|
||||
"Admin": "Admin",
|
||||
"Advanced": "Erweitert",
|
||||
"Advanced Search Settings": "Erweiterte Sucheinstellungen",
|
||||
"AiCreditsBalance": "Credit Guthaben",
|
||||
"AiLog": "AI Protokoll",
|
||||
"AiLogHelp": "Eine Übersicht der AI Anfragen.",
|
||||
"AiModelHelp": "Die Liste enthält Modelle die offiziell Unterstützt und getestet wurden. Weitere modelle können manuell eingetragen werden.",
|
||||
"AiProvider": "AI Anbieter",
|
||||
"AiProviderHelp": "Je nach Präferenz können verschiedene AI Anbieter angelegt werden. Diese können auch Space übergreifend sein.",
|
||||
"Alignment": "Ausrichtung",
|
||||
"AllRecipes": "Alle Rezepte",
|
||||
"Amount": "Menge",
|
||||
@@ -92,6 +98,7 @@
|
||||
"Continue": "Weiter",
|
||||
"Conversion": "Umrechnung",
|
||||
"ConversionsHelp": "Mit Umrechnungen kann die Menge eines Lebensmittels in verschiedenen Einheiten ausgerechnet werden. Aktuell wird dies nur zur berechnung von Eigenschaften verwendet, später jedoch sollen auch andere Funktionen von Tandoor davon profitieren. ",
|
||||
"ConvertUsingAI": "Mithilfe von AI Umwandeln",
|
||||
"CookLog": "Kochprotokoll",
|
||||
"CookLogHelp": "Einträge im Kochprotokoll für Rezepte. ",
|
||||
"Cooked": "Gekocht",
|
||||
@@ -105,6 +112,8 @@
|
||||
"Create": "Erstellen",
|
||||
"Create Food": "Zutat erstellen",
|
||||
"Create Recipe": "Rezept erstellen",
|
||||
"CreateFirstRecipe": "Erstelle dein erstes Rezept mit dem Rezepteditor.",
|
||||
"CreateInvitation": "Einladung erstellen",
|
||||
"Create_Meal_Plan_Entry": "Neuer Eintrag",
|
||||
"Create_New_Food": "Neues Lebensmittel hinzufügen",
|
||||
"Create_New_Keyword": "Neues Schlagwort hinzufügen",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Create_New_Unit": "Neue Einheit hinzufügen",
|
||||
"Created": "Erstellt",
|
||||
"CreatedBy": "Erstellt von",
|
||||
"Credits": "Credits",
|
||||
"Ctrl+K": "Strg+K",
|
||||
"Current_Period": "Aktueller Zeitraum",
|
||||
"Custom Filter": "Benutzerdefinierter Filter",
|
||||
@@ -207,11 +217,15 @@
|
||||
"Food_Replace": "Essen Ersetzen",
|
||||
"Foods": "Lebensmittel",
|
||||
"Friday": "Freitag",
|
||||
"FromBalance": "Guthaben verwendet",
|
||||
"Fulltext": "Volltext",
|
||||
"FulltextHelp": "Felder welche im Volltext durchsucht werden sollen. Tipp: Die Suchtypen 'web', 'raw' und 'phrase' funktionieren nur mit Volltext-Feldern.",
|
||||
"Fuzzy": "Unscharf",
|
||||
"FuzzySearchHelp": "Verwende unscharfe Suche um Einträge auch bei Unterschieden in der Schreibweise zu finden.",
|
||||
"GettingStarted": "Erste Schritte",
|
||||
"Global": "Global",
|
||||
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
|
||||
"Group": "Gruppe",
|
||||
"GroupBy": "Gruppieren nach",
|
||||
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
|
||||
"Headline": "Überschrift",
|
||||
@@ -237,7 +251,10 @@
|
||||
"Import": "Importieren",
|
||||
"Import Recipe": "Rezept importieren",
|
||||
"ImportAll": "Alle importieren",
|
||||
"ImportFirstRecipe": "Importiere dein erstes Rezept von einer von tausenden Websites oder nutze einen der anderen Importer um bestehende Sammlungen, Dokumente oder URL Listen zu importieren. ",
|
||||
"ImportIntoTandoor": "In Tandoor importieren",
|
||||
"ImportMealPlans": "Speisepläne importieren",
|
||||
"ImportShoppingList": "Einkaufslisten importieren",
|
||||
"Import_Error": "Es ist ein Fehler beim Importieren aufgetreten. Bitte sieh dir die ausgeklappten Details unten auf der Seite an.",
|
||||
"Import_Not_Yet_Supported": "Importieren wird noch nicht unterstützt",
|
||||
"Import_Result_Info": "{imported} von insgesamt {total} Rezepten wurden importiert",
|
||||
@@ -276,9 +293,12 @@
|
||||
"Last": "Letztes",
|
||||
"Last_name": "Nachname",
|
||||
"Learn_More": "Mehr erfahren",
|
||||
"LeaveSpace": "Space verlassen",
|
||||
"Link": "Link",
|
||||
"Load": "Laden",
|
||||
"Load_More": "Weitere laden",
|
||||
"LogCredits": "Credits Protokollieren",
|
||||
"LogCreditsHelp": "Protokolliere die Credit Kosten der AI Anfragen. Ohne diese Protokollierung können Nutzer unbgerenzt viele Anfragen stellen.",
|
||||
"Log_Cooking": "Kochen protokollieren",
|
||||
"Log_Recipe_Cooking": "Kochen protokollieren",
|
||||
"Logo": "Logo",
|
||||
@@ -308,6 +328,8 @@
|
||||
"ModelSelectResultsHelp": "Für mehr Ergebnisse suchen",
|
||||
"Monday": "Montag",
|
||||
"Month": "Monat",
|
||||
"MonthlyCredits": "Monatliche Credits",
|
||||
"MonthlyCreditsUsed": "Monatliche Credits verwendet",
|
||||
"More": "Mehr",
|
||||
"Move": "Verschieben",
|
||||
"MoveCategory": "Verschieben nach: ",
|
||||
@@ -349,6 +371,8 @@
|
||||
"Note": "Notiz",
|
||||
"Number of Objects": "Anzahl von Objekten",
|
||||
"Nutrition": "Nährwerte",
|
||||
"NutritionsPerServing": "Nährwerte pro Portion",
|
||||
"NutritionsPerServingHelp": "Manche Anwendungen spezifizieren nicht, ob Nährwerte pro Portion oder pro Rezept anzugeben sind. Standardmäßig werden Sie daher pro Rezept importiert. Wähle diese Option um Sie als pro Portion zu behandeln.",
|
||||
"OfflineAlert": "Du bist offline. Deine Einkaufsliste wird nicht synchronisiert.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Aktuell vorrätig",
|
||||
@@ -461,6 +485,7 @@
|
||||
"Servings": "Portionen",
|
||||
"ServingsText": "Portionstext",
|
||||
"Settings": "Einstellungen",
|
||||
"SettingsOnlySuperuser": "Einige Einstellungen können nur vom Server Administrator verändert werden.",
|
||||
"Share": "Teilen",
|
||||
"ShopLater": "Später kaufen",
|
||||
"ShopNow": "Jetzt kaufen",
|
||||
@@ -484,17 +509,21 @@
|
||||
"Show_as_header": "Als Überschrift",
|
||||
"Single": "Einzeln",
|
||||
"Size": "Größe",
|
||||
"Skip": "Überspringen",
|
||||
"Social_Authentication": "Login über Drittanbieter",
|
||||
"Sort_by_new": "Nach Neueste sortieren",
|
||||
"Source": "Quelle",
|
||||
"SourceImportHelp": "Importiere JSON im schema.org/recipe format oder eine HTML Seite mit json+ld Rezept bzw. microdata.",
|
||||
"SourceImportSubtitle": "Importiere JSON oder HTML manuell.",
|
||||
"Space": "Space",
|
||||
"SpaceHelp": "Alle deine Daten sind sicher in deinem Space gespeichert und können nur von dir und den anderen Mitgliedern genutzt werden.",
|
||||
"SpaceLimitExceeded": "Dein Space hat ein Limit überschritten, manche Funktionen wurden eingeschränkt.",
|
||||
"SpaceLimitReached": "Dieser Space hat ein Limit erreicht. Es können keine neuen Objekte von diesem Typ angelegt werden.",
|
||||
"SpaceMemberHelp": "Füge Benutzer hinzu indem du Einladungen erstellst und Sie an die gewünschte Person sendest.",
|
||||
"SpaceMembers": "Space Mitglieder",
|
||||
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. ",
|
||||
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. Füge weitere Nutzer mit Einladungslinks hinzu.",
|
||||
"SpaceName": "Space Name",
|
||||
"SpacePrivateObjectsHelp": "Einige Objekte sind Standardmäßig privat, können aber mit Mitgliedern deines Spaces geteilt werden.",
|
||||
"SpaceSettings": "Space Einstellungen",
|
||||
"Space_Cosmetic_Settings": "Kosmetische Einstellungen auf Space Ebene überschreiben die Einstellungen der einzelnen Nutzer.",
|
||||
"Split": "Aufteilen",
|
||||
@@ -606,6 +635,8 @@
|
||||
"Week": "Woche",
|
||||
"Week_Numbers": "Kalenderwochen",
|
||||
"Welcome": "Willkommen",
|
||||
"WelcomeSettingsHelp": "Bitte wähle die grundlegenden Einstellungen für deinen Space. Du kannst Sie später jederzeit in den Einstellungen ändern.",
|
||||
"WelcometoTandoor": "Willkommen bei Tandoor",
|
||||
"WorkingTime": "Arbeitszeit",
|
||||
"Year": "Jahr",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Λογαριασμός",
|
||||
"Active": "",
|
||||
"Add": "Προσθήκη",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Προσθήκη του φαγητού {food} στη λίστα αγορών σας",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Προστέθηκε από",
|
||||
"Added_on": "Προστέθηκε στις",
|
||||
"Advanced": "Για προχωρημένους",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Ευθυγράμμιση",
|
||||
"Amount": "Ποσότητα",
|
||||
"App": "Εφαρμογή",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Εμφάνιση σχολίων",
|
||||
"Completed": "Ολοκληρωμένο",
|
||||
"Conversion": "Μετατροπή",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Αντιγραφή",
|
||||
"Copy Link": "Αντιγραφή συνδέσμου",
|
||||
"Copy Token": "Αντιγραφή token",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Δημιουργία",
|
||||
"Create Food": "Δημιουργία φαγητού",
|
||||
"Create Recipe": "Δημιουργία συνταγής",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Δημιουργία εγγραφής στο πρόγραμμα γευμάτων",
|
||||
"Create_New_Food": "Προσθήκη νέου φαγητού",
|
||||
"Create_New_Keyword": "Προσθήκη νέας λέξης-κλειδί",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών",
|
||||
"Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης",
|
||||
"Created": "Δημιουργήθηκε",
|
||||
"Credits": "",
|
||||
"Current_Period": "Τρέχουσα περίοδος",
|
||||
"Custom Filter": "Προσαρμοσμένο φίλτρο",
|
||||
"CustomImageHelp": "Ανεβάστε μια εικόνα για να εμφανίζεται στην επισκόπηση χώρου",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "Ψευδώνυμο φαγητού",
|
||||
"Food_Replace": "Αντικατάσταση Φαγητού",
|
||||
"Foods": "Φαγητά",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Ομαδοποίηση κατά",
|
||||
"Hide_Food": "Απόκρυψη φαγητού",
|
||||
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "Εικόνα",
|
||||
"Import": "Εισαγωγή",
|
||||
"Import Recipe": "Εισαγωγή συνταγής",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Συνέβη ένα σφάλμα κατά την εισαγωγή. Για να το δείτε, εμφανίστε τις λεπτομέρειες στο κάτω μέρος της σελίδας.",
|
||||
"Import_Not_Yet_Supported": "Η εισαγωγή δεν υποστηρίζεται ακόμη",
|
||||
"Import_Result_Info": "Έγινε εισαγωγή {imported} από τις {total} συνταγές",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "Γλώσσα",
|
||||
"Last_name": "Επίθετο",
|
||||
"Learn_More": "Μάθετε περισσότερα",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Σύνδεσμος",
|
||||
"Load_More": "Φόρτωση περισσότερων",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Καταγραφή μαγειρέματος",
|
||||
"Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής",
|
||||
"Logo": "Λογότυπο",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "Μήνυμα",
|
||||
"MissingProperties": "",
|
||||
"Month": "Μήνας",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Μετακίνηση",
|
||||
"MoveCategory": "Μετακίνηση σε: ",
|
||||
"Move_Down": "Μετακίνηση κάτω",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "Σημείωση",
|
||||
"Number of Objects": "Αριθμός αντικειμένων",
|
||||
"Nutrition": "Διατροφική αξία",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Είστε εκτός σύνδεσης, η λίστα αγορών μπορεί να μην συγχρονιστεί.",
|
||||
"Ok": "ΟΚ",
|
||||
"OnHand": "Τώρα διαθέσιμα",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "Επιλεγμένο",
|
||||
"Servings": "Μερίδες",
|
||||
"Settings": "Ρυθμίσεις",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Κοινοποίηση",
|
||||
"ShoppingBackgroundSyncWarning": "Κακό δίκτυο, αναμονή συγχρονισμού...",
|
||||
"Shopping_Categories": "Κατηγορίες αγορών",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "Εμφάνιση ως κεφαλίδα",
|
||||
"Single": "Ενικός",
|
||||
"Size": "Μέγεθος",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων",
|
||||
"Sort_by_new": "Ταξινόμηση κατά νέο",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
|
||||
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
|
||||
"StartDate": "Ημερομηνία Έναρξης",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "Εβδομάδα",
|
||||
"Week_Numbers": "Αριθμοί εδομάδων",
|
||||
"Welcome": "Καλώς ήρθατε",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Έτος",
|
||||
"Yes": "",
|
||||
"add_keyword": "Προσθήκη λέξης-κλειδί",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Use AI to import images of recipes.",
|
||||
"AISettingsHostedHelp": "You can enable AI features or change available credits by managing your subscription.",
|
||||
"API": "API",
|
||||
"APIKey": "API key",
|
||||
"API_Browser": "API Browser",
|
||||
"API_Documentation": "API Docs",
|
||||
"AccessTokenHelp": "Access keys for the REST API.",
|
||||
"Access_Token": "Access Token",
|
||||
"Account": "Account",
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"Activity": "Activity",
|
||||
"Add": "Add",
|
||||
"AddAll": "Add all",
|
||||
@@ -26,6 +29,12 @@
|
||||
"Added_on": "Added On",
|
||||
"Admin": "Admin",
|
||||
"Advanced": "Advanced",
|
||||
"AiCreditsBalance": "Credit Balance",
|
||||
"AiLog": "AI Log",
|
||||
"AiLogHelp": "Overview of your spaces AI requests. ",
|
||||
"AiModelHelp": "The list contains model that are offically tested and supported. You can add additional models if you want.",
|
||||
"AiProvider": "AI Provider",
|
||||
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
|
||||
"Alignment": "Alignment",
|
||||
"AllRecipes": "All Recipes",
|
||||
"Amount": "Amount",
|
||||
@@ -87,6 +96,7 @@
|
||||
"Continue": "Continue",
|
||||
"Conversion": "Conversion",
|
||||
"ConversionsHelp": "With conversions you can calculate the amount of a food in different units. Currently this is only used for property calculation, later it might also be used in other parts of tandoor. ",
|
||||
"ConvertUsingAI": "Convert using AI",
|
||||
"CookLog": "Cook Log",
|
||||
"CookLogHelp": "Entries in the cook log for recipes. ",
|
||||
"Cooked": "Cooked",
|
||||
@@ -100,6 +110,8 @@
|
||||
"Create": "Create",
|
||||
"Create Food": "Create Food",
|
||||
"Create Recipe": "Create Recipe",
|
||||
"CreateFirstRecipe": "Create your first recipe using the recipe editor.",
|
||||
"CreateInvitation": "Create invitation",
|
||||
"Create_Meal_Plan_Entry": "Create meal plan entry",
|
||||
"Create_New_Food": "Add New Food",
|
||||
"Create_New_Keyword": "Add New Keyword",
|
||||
@@ -109,6 +121,7 @@
|
||||
"Create_New_Unit": "Add New Unit",
|
||||
"Created": "Created",
|
||||
"CreatedBy": "Created by",
|
||||
"Credits": "Credits",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Current Period",
|
||||
"Custom Filter": "Custom Filter",
|
||||
@@ -202,11 +215,15 @@
|
||||
"Food_Replace": "Food Replace",
|
||||
"Foods": "Foods",
|
||||
"Friday": "Friday",
|
||||
"FromBalance": "From Balance",
|
||||
"Fulltext": "Fulltext",
|
||||
"FulltextHelp": "Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields.",
|
||||
"Fuzzy": "Fuzzy",
|
||||
"FuzzySearchHelp": "Use fuzzy search to find entries even when there are differences in how the word is written.",
|
||||
"GettingStarted": "Getting Started",
|
||||
"Global": "Global",
|
||||
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
|
||||
"Group": "Group",
|
||||
"GroupBy": "Group By",
|
||||
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
|
||||
"Headline": "Headline",
|
||||
@@ -232,7 +249,10 @@
|
||||
"Import": "Import",
|
||||
"Import Recipe": "Import Recipe",
|
||||
"ImportAll": "Import all",
|
||||
"ImportFirstRecipe": "Import your first recipe from one of thousands of websites or use one of the other importers to import your existing collection, documents or URL lists.",
|
||||
"ImportIntoTandoor": "Import into Tandoor",
|
||||
"ImportMealPlans": "Import mealplans",
|
||||
"ImportShoppingList": "Import shoppinglists",
|
||||
"Import_Error": "An Error occurred during your import. Please expand the Details at the bottom of the page to view it.",
|
||||
"Import_Not_Yet_Supported": "Import not yet supported",
|
||||
"Import_Result_Info": "{imported} of {total} recipes were imported",
|
||||
@@ -271,9 +291,12 @@
|
||||
"Last": "Last",
|
||||
"Last_name": "Last Name",
|
||||
"Learn_More": "Learn More",
|
||||
"LeaveSpace": "Leave Space",
|
||||
"Link": "Link",
|
||||
"Load": "Load",
|
||||
"Load_More": "Load More",
|
||||
"LogCredits": "Log Credits.",
|
||||
"LogCreditsHelp": "Log credit cost of AI requests. Without this users can perform as many AI requests as they want. ",
|
||||
"Log_Cooking": "Log Cooking",
|
||||
"Log_Recipe_Cooking": "Log Recipe Cooking",
|
||||
"Logo": "Logo",
|
||||
@@ -303,6 +326,8 @@
|
||||
"ModelSelectResultsHelp": "Search for more results",
|
||||
"Monday": "Monday",
|
||||
"Month": "Month",
|
||||
"MonthlyCredits": "Monthly Credits",
|
||||
"MonthlyCreditsUsed": "Monthly credits used",
|
||||
"More": "More",
|
||||
"Move": "Move",
|
||||
"MoveCategory": "Move To: ",
|
||||
@@ -344,6 +369,8 @@
|
||||
"Note": "Note",
|
||||
"Number of Objects": "Number of Objects",
|
||||
"Nutrition": "Nutrition",
|
||||
"NutritionsPerServing": "Nutritions per Serving",
|
||||
"NutritionsPerServingHelp": "Some applications do not specify if nutritions are per recipe or per serving. By default Tandoor treats them as per recipe. Check this box to treat them as per serving. ",
|
||||
"OfflineAlert": "You are offline, shopping list may not syncronize.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Currently On Hand",
|
||||
@@ -456,6 +483,7 @@
|
||||
"Servings": "Servings",
|
||||
"ServingsText": "Servings Text",
|
||||
"Settings": "Settings",
|
||||
"SettingsOnlySuperuser": "Some Settings can only be changed by the Server Administrator.",
|
||||
"Share": "Share",
|
||||
"ShopLater": "Shop later",
|
||||
"ShopNow": "Shop now",
|
||||
@@ -479,17 +507,21 @@
|
||||
"Show_as_header": "Show as header",
|
||||
"Single": "Single",
|
||||
"Size": "Size",
|
||||
"Skip": "Skip",
|
||||
"Social_Authentication": "Social Authentication",
|
||||
"Sort_by_new": "Sort by new",
|
||||
"Source": "Source",
|
||||
"SourceImportHelp": "Import JSON in schema.org/recipe format or html pages with json+ld recipe or microdata.",
|
||||
"SourceImportSubtitle": "Import JSON or HTML manually.",
|
||||
"Space": "Space",
|
||||
"SpaceHelp": "All your data is part of your space and can only be acccessed by space members. ",
|
||||
"SpaceLimitExceeded": "Your space has surpassed one of its limits, some functions might be restricted.",
|
||||
"SpaceLimitReached": "This Space has reached a limit. No more objects of this type can be created.",
|
||||
"SpaceMemberHelp": "Add users to your space by creating an Invite Link and sending it to the person you want to add.",
|
||||
"SpaceMembers": "Space Members",
|
||||
"SpaceMembersHelp": "Users and their permissions in a space. ",
|
||||
"SpaceMembersHelp": "Users and their permissions in a space. Add additional users using invite links.",
|
||||
"SpaceName": "Space Name",
|
||||
"SpacePrivateObjectsHelp": " Some things are private by default an can be shared with members of your space.",
|
||||
"SpaceSettings": "Space Settings",
|
||||
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
|
||||
"Split": "Split",
|
||||
@@ -601,6 +633,8 @@
|
||||
"Week": "Week",
|
||||
"Week_Numbers": "Week numbers",
|
||||
"Welcome": "Welcome",
|
||||
"WelcomeSettingsHelp": "Please choose the basic settings for your Tandoor space. You can change all of these later trough the settings.",
|
||||
"WelcometoTandoor": "Welcome to Tandoor",
|
||||
"WorkingTime": "Working time",
|
||||
"Year": "Year",
|
||||
"Yes": "Yes",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Usar IA para importar imágenes de recetas.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Token de acceso",
|
||||
"Account": "Cuenta",
|
||||
"Actions": "Acciones",
|
||||
"Active": "",
|
||||
"Activity": "Actividad",
|
||||
"Add": "Añadir",
|
||||
"AddAll": "Agregar todo",
|
||||
@@ -26,6 +28,12 @@
|
||||
"Added_on": "Añadido el",
|
||||
"Admin": "Administrador",
|
||||
"Advanced": "Avanzado",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alineación",
|
||||
"AllRecipes": "Todas las recetas",
|
||||
"Amount": "Cantidad",
|
||||
@@ -85,6 +93,7 @@
|
||||
"Continue": "Continuar",
|
||||
"Conversion": "Conversión",
|
||||
"ConversionsHelp": "Con las conversiones puedes calcular la cantidad de un alimento en diferentes unidades. Actualmente esto solo se usa para el cálculo de propiedades, en un futuro podría ser usado en otras partes de Tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Historial de cocina",
|
||||
"CookLogHelp": "Entradas en el historial de cocina para recetas. ",
|
||||
"Cooked": "Cocinado",
|
||||
@@ -98,6 +107,8 @@
|
||||
"Create": "Crear",
|
||||
"Create Food": "Crear alimento",
|
||||
"Create Recipe": "Crear receta",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Crear entrada de plan de comidas",
|
||||
"Create_New_Food": "Añadir nuevo alimento",
|
||||
"Create_New_Keyword": "Añadir nueva palabra clave",
|
||||
@@ -107,6 +118,7 @@
|
||||
"Create_New_Unit": "Añadir nueva unidad",
|
||||
"Created": "Creada",
|
||||
"CreatedBy": "Creado por",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Periodo actual",
|
||||
"Custom Filter": "Filtro personalizado",
|
||||
@@ -200,7 +212,11 @@
|
||||
"Food_Replace": "Sustituir Alimento",
|
||||
"Foods": "Alimentos",
|
||||
"Friday": "Viernes",
|
||||
"FromBalance": "",
|
||||
"GettingStarted": "Primeros pasos",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupar por",
|
||||
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
|
||||
"Headline": "Encabezado",
|
||||
@@ -224,7 +240,10 @@
|
||||
"Import": "Importar",
|
||||
"Import Recipe": "Importar Receta",
|
||||
"ImportAll": "Importar todo",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Importar a Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Ocurrió un Error ocurrió durante la importación. Por favor, expanda los Detalles al final de la página para verlo.",
|
||||
"Import_Not_Yet_Supported": "Importación no soportada todavía",
|
||||
"Import_Result_Info": "{imported} de {total} recetas fueron importadas",
|
||||
@@ -263,9 +282,12 @@
|
||||
"Last": "Último",
|
||||
"Last_name": "Apellidos",
|
||||
"Learn_More": "Saber Más",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Enlace",
|
||||
"Load": "Cargar",
|
||||
"Load_More": "Cargar más",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registrar cocinada",
|
||||
"Log_Recipe_Cooking": "Registro de recetas",
|
||||
"Logo": "Logotipo",
|
||||
@@ -294,6 +316,8 @@
|
||||
"ModelSelectResultsHelp": "Buscar más resultados",
|
||||
"Monday": "Lunes",
|
||||
"Month": "Mes",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Más",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover a: ",
|
||||
@@ -335,6 +359,8 @@
|
||||
"Note": "Nota",
|
||||
"Number of Objects": "Número de Objetos",
|
||||
"Nutrition": "Nutrición",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Estas desconectado, la lista de la compra puede no sincronizarse.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Actualmente en Posesión",
|
||||
@@ -441,6 +467,7 @@
|
||||
"Servings": "Raciones",
|
||||
"ServingsText": "Texto de la porción",
|
||||
"Settings": "Opciones",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartir",
|
||||
"ShopLater": "Comprar después",
|
||||
"ShopNow": "Comprar ahora",
|
||||
@@ -463,16 +490,20 @@
|
||||
"Show_as_header": "Mostrar como encabezado",
|
||||
"Single": "Simple",
|
||||
"Size": "Tamaño",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Autenticación Social",
|
||||
"Sort_by_new": "Ordenar por novedades",
|
||||
"SourceImportHelp": "Importar JSON en formato schema.org/recipe o páginas HTML con recetas en formato JSON+LD o microdatos.",
|
||||
"SourceImportSubtitle": "Importar JSON o HTML manualmente.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Tu espacio ha sobrepasado uno de sus límites, algunas funciones podrían estar restringidas.",
|
||||
"SpaceLimitReached": "Este espacio ha alcanzado un límite. No se pueden crear más objetos de este tipo.",
|
||||
"SpaceMemberHelp": "Agrega usuarios a tu espacio creando un enlace de invitación y enviándolo a la persona que quieras agregar.",
|
||||
"SpaceMembers": "Miembros del espacio",
|
||||
"SpaceMembersHelp": "Usuarios y sus permisos en un espacio. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Ajustes del espacio",
|
||||
"Space_Cosmetic_Settings": "Algunos ajustes de apariencia pueden ser cambiados por los administradores del espacio y anularán los ajustes del cliente para ese espacio.",
|
||||
"Split": "Dividir",
|
||||
@@ -578,6 +609,8 @@
|
||||
"Week": "Semana",
|
||||
"Week_Numbers": "numero de semana",
|
||||
"Welcome": "Bienvenido/a",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Tiempo de trabajo",
|
||||
"Year": "Año",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Tili",
|
||||
"Active": "",
|
||||
"Add": "Lisää",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Lisää {food} ostoslistaan",
|
||||
@@ -17,6 +19,12 @@
|
||||
"Added_on": "Lisätty",
|
||||
"Advanced": "Edistynyt",
|
||||
"Advanced Search Settings": "Tarkennetun Haun Asetukset",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Tasaus",
|
||||
"Amount": "Määrä",
|
||||
"App": "Applikaatio",
|
||||
@@ -55,6 +63,7 @@
|
||||
"Comments_setting": "Näytä Kommentit",
|
||||
"Completed": "Valmis",
|
||||
"Conversion": "Muuntaminen",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopioi",
|
||||
"Copy Link": "Kopioi Linkki",
|
||||
"Copy Token": "Kopioi Token",
|
||||
@@ -63,6 +72,8 @@
|
||||
"CountMore": "...+{count} enemmän",
|
||||
"Create": "Luo",
|
||||
"Create Food": "Luo Ruoka",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Luo ateriasuunnitelma merkintä",
|
||||
"Create_New_Food": "Lisää Uusi Ruoka",
|
||||
"Create_New_Keyword": "Lisää Uusi Avainsana",
|
||||
@@ -71,6 +82,7 @@
|
||||
"Create_New_Shopping_Category": "Lisää uusi ostoskategoria",
|
||||
"Create_New_Unit": "Lisää Uusi Yksikkö",
|
||||
"Created": "Luotu",
|
||||
"Credits": "",
|
||||
"Current_Period": "Nykyinen Jakso",
|
||||
"Custom Filter": "Mukautettu Suodatin",
|
||||
"CustomImageHelp": "Lataa kuva näytettäväksi tilan yleiskatsauksessa.",
|
||||
@@ -140,10 +152,14 @@
|
||||
"Food_Alias": "Ruoan nimimerkki",
|
||||
"Food_Replace": "Korvaa Ruoka",
|
||||
"Foods": "Ruuat",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Ryhmittely peruste",
|
||||
"Hide_Food": "Piilota Ruoka",
|
||||
"Hide_Keyword": "Piilota avainsana",
|
||||
@@ -162,6 +178,9 @@
|
||||
"Image": "Kuva",
|
||||
"Import": "Tuo",
|
||||
"Import Recipe": "Tuo Resepti",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Not_Yet_Supported": "Tuontia ei vielä tueta",
|
||||
"Import_Supported": "Tuonti tuettu",
|
||||
"Import_finished": "Tuonti valmistui",
|
||||
@@ -189,8 +208,11 @@
|
||||
"Language": "Kieli",
|
||||
"Last_name": "Sukunimi",
|
||||
"Learn_More": "Lisätietoja",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Linkki",
|
||||
"Load_More": "Lataa Lisää",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Kirjaa kokkaus",
|
||||
"Log_Recipe_Cooking": "Kirjaa Reseptin valmistus",
|
||||
"Logo": "Logo",
|
||||
@@ -210,6 +232,8 @@
|
||||
"Message": "Viesti",
|
||||
"MissingProperties": "",
|
||||
"Month": "Kuukausi",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Siirry",
|
||||
"MoveCategory": "Siirrä paikkaan: ",
|
||||
"Move_Down": "Siirry alas",
|
||||
@@ -241,6 +265,8 @@
|
||||
"Note": "Lisätiedot",
|
||||
"Number of Objects": "Objektien määrä",
|
||||
"Nutrition": "Ravitsemus",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Olet offline-tilassa, ostoslista ei välttämättä synkronoidu.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Tällä hetkellä saatavilla",
|
||||
@@ -317,6 +343,7 @@
|
||||
"Selected": "Valittu",
|
||||
"Servings": "Annokset",
|
||||
"Settings": "Asetukset",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Jaa",
|
||||
"ShoppingBackgroundSyncWarning": "Huono verkkoyhteys, odotetaan synkronointia ...",
|
||||
"Shopping_Categories": "Ostoskategoriat",
|
||||
@@ -332,9 +359,14 @@
|
||||
"Show_as_header": "Näytä otsikkona",
|
||||
"Single": "Yksittäinen",
|
||||
"Size": "Koko",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Sosiaalinen Todennus",
|
||||
"Sort_by_new": "Lajittele uusien mukaan",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
|
||||
"StartDate": "Aloituspäivä",
|
||||
"Starting_Day": "Viikon aloituspäivä",
|
||||
@@ -393,6 +425,8 @@
|
||||
"Week": "Viikko",
|
||||
"Week_Numbers": "Viikkonumerot",
|
||||
"Welcome": "Tervetuloa",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Vuosi",
|
||||
"Yes": "",
|
||||
"add_keyword": "Lisää Avainsana",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Utiliser l'IA pour importer des images de recettes.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Jeton d'accès",
|
||||
"Account": "Compte",
|
||||
"Actions": "Actions",
|
||||
"Active": "",
|
||||
"Activity": "Activité",
|
||||
"Add": "Ajouter",
|
||||
"AddAll": "Tout ajouter",
|
||||
@@ -27,6 +29,12 @@
|
||||
"Admin": "Admin",
|
||||
"Advanced": "Avancé",
|
||||
"Advanced Search Settings": "Paramètres de recherche avancée",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alignement",
|
||||
"AllRecipes": "Toutes les recettes",
|
||||
"Amount": "Quantité",
|
||||
@@ -88,6 +96,7 @@
|
||||
"Continue": "Continuer",
|
||||
"Conversion": "Conversion",
|
||||
"ConversionsHelp": "Avec les conversions, vous pouvez calculer une quantité dans différentes unités. Actuellement, c'est utilisé uniquement pour le calcul des propriétés, mais ça pourrait être utilisé dans d'autres parties de Tandoor dans le futur. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Journal de cuisine",
|
||||
"CookLogHelp": "Entrées dans le journal de cuisine pour les recettes. ",
|
||||
"Cooked": "Cuit",
|
||||
@@ -101,6 +110,8 @@
|
||||
"Create": "Créer",
|
||||
"Create Food": "Créer un aliment",
|
||||
"Create Recipe": "Créer une recette",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Créer une entrée de menu",
|
||||
"Create_New_Food": "Ajouter un nouvel aliment",
|
||||
"Create_New_Keyword": "Ajouter un nouveau mot-clé",
|
||||
@@ -110,6 +121,7 @@
|
||||
"Create_New_Unit": "Ajouter une nouvelle unité",
|
||||
"Created": "Créé",
|
||||
"CreatedBy": "Créé par",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Période actuelle",
|
||||
"Custom Filter": "Filtre personnalisé",
|
||||
@@ -203,11 +215,15 @@
|
||||
"Food_Replace": "Remplacer l'aliment",
|
||||
"Foods": "Aliments",
|
||||
"Friday": "Vendredi",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Texte intégral",
|
||||
"FulltextHelp": "Champs de recherche en texte intégral. Remarque : les méthodes de recherche \"web\", \"phrase\" et \"raw\" ne fonctionnent qu'avec des champs en texte intégral.",
|
||||
"Fuzzy": "Approximatif",
|
||||
"FuzzySearchHelp": "Utilisez la recherche approximative pour trouver des entrées même lorsqu'il existe des différences dans la façon dont le mot est écrit.",
|
||||
"GettingStarted": "Commencer",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grouper par",
|
||||
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
|
||||
"Headline": "En-tête",
|
||||
@@ -233,7 +249,10 @@
|
||||
"Import": "Importer",
|
||||
"Import Recipe": "Importer une recette",
|
||||
"ImportAll": "Tout importer",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Importer dans Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Une erreur est survenue pendant votre importation. Veuillez développer les détails au bas de la page pour la consulter.",
|
||||
"Import_Not_Yet_Supported": "Importation pas encore prise en charge",
|
||||
"Import_Result_Info": "{imported} sur {total} recettes ont été importées",
|
||||
@@ -272,9 +291,12 @@
|
||||
"Last": "Dernier",
|
||||
"Last_name": "Nom",
|
||||
"Learn_More": "Apprenez-en plus",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Lien",
|
||||
"Load": "Chargement",
|
||||
"Load_More": "Charger plus",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Marquer comme cuisiné",
|
||||
"Log_Recipe_Cooking": "Marquer la recette comme cuisinée",
|
||||
"Logo": "Logo",
|
||||
@@ -301,6 +323,8 @@
|
||||
"ModelSelectResultsHelp": "Chercher plus de résultats",
|
||||
"Monday": "Lundi",
|
||||
"Month": "Mois",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Plus",
|
||||
"Move": "Déplacer",
|
||||
"MoveCategory": "Déplacer vers : ",
|
||||
@@ -342,6 +366,8 @@
|
||||
"Note": "Notes",
|
||||
"Number of Objects": "Nombre d'objets",
|
||||
"Nutrition": "Valeurs nutritionnelles",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Vous êtes déconnecté, votre liste de courses peut ne pas être synchronisée.",
|
||||
"Ok": "D'accord",
|
||||
"OnHand": "Disponible actuellement",
|
||||
@@ -454,6 +480,7 @@
|
||||
"Servings": "Portions",
|
||||
"ServingsText": "Texte des portions",
|
||||
"Settings": "Paramètres",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Partager",
|
||||
"ShopLater": "Acheter plus tard",
|
||||
"ShopNow": "Acheter maintenant",
|
||||
@@ -477,17 +504,21 @@
|
||||
"Show_as_header": "Montrer comme en-tête",
|
||||
"Single": "Unique",
|
||||
"Size": "Taille",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Authentification Sociale",
|
||||
"Sort_by_new": "Trier par nouveautés",
|
||||
"Source": "Source",
|
||||
"SourceImportHelp": "Importez du JSON au format schema.org/recipe ou des pages HTML avec une recette json+ld ou des microdonnées.",
|
||||
"SourceImportSubtitle": "Importez en JSON ou HTML manuellement.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Votre groupe a dépassé une de ses limites, certaines fonctions pourraient être restreintes.",
|
||||
"SpaceLimitReached": "Ce groupe a atteint sa limite. Aucun nouvel objet de ce type ne peut être créé.",
|
||||
"SpaceMemberHelp": "Ajoutez des utilisateurs à votre espace en créant un lien d'invitation et en l'envoyant à la personne que vous souhaitez ajouter.",
|
||||
"SpaceMembers": "Membres du groupe",
|
||||
"SpaceMembersHelp": "Utilisateurs et permissions dans un groupe. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Paramètres du groupe",
|
||||
"Space_Cosmetic_Settings": "Certains paramètres cosmétiques peuvent être modifiés par un administrateur de l'espace et seront prioritaires sur les paramètres des utilisateurs pour cet espace.",
|
||||
"Split": "Diviser",
|
||||
@@ -597,6 +628,8 @@
|
||||
"Week": "Semaine",
|
||||
"Week_Numbers": "Numéro de semaine",
|
||||
"Welcome": "Bienvenue",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Temps de préparation",
|
||||
"Year": "Année",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "חשבון",
|
||||
"Active": "",
|
||||
"Add": "הוספה",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "הוסף {מזון} לרשימת הקניות",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "נוסף ע\"י",
|
||||
"Added_on": "נוסף ב",
|
||||
"Advanced": "מתקדם",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "יישור",
|
||||
"Amount": "כמות",
|
||||
"App": "אפליקציה",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "הצג תגובות",
|
||||
"Completed": "הושלם",
|
||||
"Conversion": "עברית",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "העתקה",
|
||||
"Copy Link": "העתק קישור",
|
||||
"Copy Token": "העתק טוקן",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "יצירה",
|
||||
"Create Food": "צור מאכל",
|
||||
"Create Recipe": "צור מתכון",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "צור רשימת תכנון אוכל",
|
||||
"Create_New_Food": "הוסף אוכל חדש",
|
||||
"Create_New_Keyword": "הוסף מילת מפתח",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "הוסף קטגוריות קניות חדשה",
|
||||
"Create_New_Unit": "הוסף יחידה",
|
||||
"Created": "נוצר",
|
||||
"Credits": "",
|
||||
"Current_Period": "תקופה נוכחית",
|
||||
"Custom Filter": "פילטר מותאם",
|
||||
"CustomImageHelp": "העלאת תמונה שתראה באזור הסקירה.",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "שם כינוי לאוכל",
|
||||
"Food_Replace": "החלף אוכל",
|
||||
"Foods": "מאכלים",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "אסוף לפי",
|
||||
"Hide_Food": "הסתר אוכל",
|
||||
"Hide_Keyword": "הסתר מילות מפתח",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "תמונה",
|
||||
"Import": "ייבוא",
|
||||
"Import Recipe": "ייבא מתכון",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "שגיאה בעת ייבוא. הרחב את הפירוט בסוף עמוד זה לראות מידע נוסף.",
|
||||
"Import_Not_Yet_Supported": "ייבוא לא נתמך עדיין",
|
||||
"Import_Result_Info": "{imported} מתוך {total} מתכונים יובאו",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "שפה",
|
||||
"Last_name": "שם משפחה",
|
||||
"Learn_More": "למד עוד",
|
||||
"LeaveSpace": "",
|
||||
"Link": "קישור",
|
||||
"Load_More": "טען עוד",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "רשום הכנת מתכון",
|
||||
"Log_Recipe_Cooking": "רשום בישול מתכון",
|
||||
"Logo": "לוגו",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "הודעה",
|
||||
"MissingProperties": "",
|
||||
"Month": "חודש",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "העברה",
|
||||
"MoveCategory": "העבר אל: ",
|
||||
"Move_Down": "העברה למטה",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "הערה",
|
||||
"Number of Objects": "מספר אובייקטים",
|
||||
"Nutrition": "תזונה",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "אתה במצב מנותק, רשימת הקניות לא בהכרח מסונכרנת.",
|
||||
"Ok": "אישור",
|
||||
"OnHand": "כרגע נגיש",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "נבחר",
|
||||
"Servings": "מנות",
|
||||
"Settings": "הגדרות",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "שיתוף",
|
||||
"ShoppingBackgroundSyncWarning": "בעיית תקשורת, מחכה לסנכון...",
|
||||
"Shopping_Categories": "קטגוריות קניות",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "הצג בתור כותרת",
|
||||
"Single": "בודד",
|
||||
"Size": "גודל",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "אימות חברתי",
|
||||
"Sort_by_new": "סדר ע\"י חדש",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
|
||||
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
|
||||
"StartDate": "תאריך התחלה",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "שבוע",
|
||||
"Week_Numbers": "מספר השבוע",
|
||||
"Welcome": "ברוכים הבאים",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "שנה",
|
||||
"Yes": "",
|
||||
"add_keyword": "הוסף מילת מפתח",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Korisnički račun",
|
||||
"Active": "",
|
||||
"Add": "Dodaj",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Dodaj {food} na svoj popis za kupovinu",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Dodao",
|
||||
"Added_on": "Dodano",
|
||||
"Advanced": "Napredno",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Poravnanje",
|
||||
"Amount": "Količina",
|
||||
"App": "Aplikacija",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Prikaži komentare",
|
||||
"Completed": "Završeno",
|
||||
"Conversion": "Konverzija",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopiraj",
|
||||
"Copy Link": "Kopiraj vezu",
|
||||
"Copy Token": "Kopiraj token",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Stvori",
|
||||
"Create Food": "Kreiraj namirnicu",
|
||||
"Create Recipe": "Kreiraj recept",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Kreirajte unos plana obroka",
|
||||
"Create_New_Food": "Dodaj novu namirnicu",
|
||||
"Create_New_Keyword": "Dodaj novu ključnu riječ",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "Dodaj novu kategoriju za kupovinu",
|
||||
"Create_New_Unit": "Dodaj novu jedinicu",
|
||||
"Created": "Stvoreno",
|
||||
"Credits": "",
|
||||
"Current_Period": "Trenutno razdoblje",
|
||||
"Custom Filter": "Prilagođeni filtar",
|
||||
"CustomImageHelp": "Učitaj sliku za prikaz u pregledu prostora.",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "Nadimci namirnice",
|
||||
"Food_Replace": "Zamjena namirnica",
|
||||
"Foods": "Namirnice",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupiraj po",
|
||||
"Hide_Food": "Sakrij namirnicu",
|
||||
"Hide_Keyword": "Sakrij ključne riječi",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "Slika",
|
||||
"Import": "Uvoz",
|
||||
"Import Recipe": "Uvezi recept",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Došlo je do pogreške tijekom uvoza. Molimo proširite pojedinosti na dnu stranice kako bi vidjeli grešku.",
|
||||
"Import_Not_Yet_Supported": "Uvoz još nije podržan",
|
||||
"Import_Result_Info": "Uvezeno je {imported} od {total} recepata",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "Jezik",
|
||||
"Last_name": "Prezime",
|
||||
"Learn_More": "Saznajte više",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Poveznica",
|
||||
"Load_More": "Učitaj više",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zapis kuhanja",
|
||||
"Log_Recipe_Cooking": "Dnevnik recepata kuhanja",
|
||||
"Logo": "Logotip",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "Poruka",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mjesec",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Premjesti",
|
||||
"MoveCategory": "Premjesti u: ",
|
||||
"Move_Down": "Premjesti dolje",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "Bilješka",
|
||||
"Number of Objects": "Broj objekata",
|
||||
"Nutrition": "Nutritivna vrijednost",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Nisi na mreži, popis za kupnju se možda neće sinkronizirati.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Trenutno pri ruci",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "Odabrano",
|
||||
"Servings": "Porcije",
|
||||
"Settings": "Postavke",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Podijeli",
|
||||
"ShoppingBackgroundSyncWarning": "Loša mreža, čeka se sinkronizacija...",
|
||||
"Shopping_Categories": "Kategorije Kupovine",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "Prikaži kao zaglavlje",
|
||||
"Single": "Jedna",
|
||||
"Size": "Veličina",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Autentifikacija putem društvenih mreža",
|
||||
"Sort_by_new": "Poredaj po novom",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.",
|
||||
"Split_All_Steps": "Podijeli sve retke u zasebne korake.",
|
||||
"StartDate": "Početni datum",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "Tjedan",
|
||||
"Week_Numbers": "Brojevi tjedana",
|
||||
"Welcome": "Dobrodošli",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Godina",
|
||||
"Yes": "",
|
||||
"add_keyword": "Dodaj ključnu riječ",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Fiók",
|
||||
"Active": "",
|
||||
"Add": "Hozzáadás",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "{food} hozzáadása bevásárlólistához",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Hozzádta",
|
||||
"Added_on": "Hozzáadva",
|
||||
"Advanced": "Haladó",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Igazítás",
|
||||
"Amount": "Összeg",
|
||||
"App": "Applikáció",
|
||||
@@ -56,6 +64,7 @@
|
||||
"Comments_setting": "Hozzászólások megjelenítése",
|
||||
"Completed": "Kész",
|
||||
"Conversion": "Konverzió",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Másolás",
|
||||
"Copy Link": "Link másolása",
|
||||
"Copy Token": "Token másolása",
|
||||
@@ -64,6 +73,8 @@
|
||||
"Create": "Létrehozás",
|
||||
"Create Food": "Alapanyag létrehozása",
|
||||
"Create Recipe": "Recept létrehozása",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Menüterv bejegyzés létrehozása",
|
||||
"Create_New_Food": "Új alapanyag hozzáadása",
|
||||
"Create_New_Keyword": "Új kulcsszó hozzáadása",
|
||||
@@ -71,6 +82,7 @@
|
||||
"Create_New_Shopping Category": "Új vásárlási kategória létrehozása",
|
||||
"Create_New_Shopping_Category": "Új vásárlási kategória hozzáadása",
|
||||
"Create_New_Unit": "Új mértékegység hozzáadása",
|
||||
"Credits": "",
|
||||
"Current_Period": "Jelenlegi periódus",
|
||||
"Custom Filter": "Egyéni szűrő",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -126,10 +138,14 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "Étel cseréje",
|
||||
"Foods": "Alapanyagok",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Csoportosítva",
|
||||
"Hide_Food": "Alapanyag elrejtése",
|
||||
"Hide_Keyword": "Kulcsszavak elrejtése",
|
||||
@@ -148,6 +164,9 @@
|
||||
"Image": "Kép",
|
||||
"Import": "Import",
|
||||
"Import Recipe": "Recept importálása",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Hiba történt az importálás során. Kérjük, a megtekintéshez bontsa ki az oldal alján található Részletek menüpontot.",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "{total}-ból/ből {imported} recept importálva",
|
||||
@@ -177,8 +196,11 @@
|
||||
"Language": "Nyelv",
|
||||
"Last_name": "Vezetéknév",
|
||||
"Learn_More": "Tudjon meg többet",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Továbbiak betöltése",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Főzés naplózása",
|
||||
"Log_Recipe_Cooking": "Főzés naplózása",
|
||||
"Make_Header": "Átalakítás címsorra",
|
||||
@@ -197,6 +219,8 @@
|
||||
"Message": "Üzenet",
|
||||
"MissingProperties": "",
|
||||
"Month": "Hónap",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mozgatás",
|
||||
"MoveCategory": "Áthelyezés ide: ",
|
||||
"Move_Down": "Lefelé mozgatás",
|
||||
@@ -229,6 +253,8 @@
|
||||
"Note": "Megjegyzés",
|
||||
"Number of Objects": "Objektumok száma",
|
||||
"Nutrition": "Tápérték",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Ön éppen offline állapotban van, a bevásárlólista nem biztos, hogy szinkronizálódik.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Jelenleg készleten",
|
||||
@@ -301,6 +327,7 @@
|
||||
"Selected": "Kiválasztott",
|
||||
"Servings": "Adag",
|
||||
"Settings": "Beállítások",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Megosztás",
|
||||
"Shopping_Categories": "Vásárlási kategóriák",
|
||||
"Shopping_Category": "Vásárlási kategória",
|
||||
@@ -312,8 +339,13 @@
|
||||
"Show_as_header": "Megjelenítés címként",
|
||||
"Single": "Egyetlen",
|
||||
"Size": "Méret",
|
||||
"Skip": "",
|
||||
"Sort_by_new": "Rendezés legújabbak szerint",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.",
|
||||
"StartDate": "Kezdés dátuma",
|
||||
"Starting_Day": "A hét kezdőnapja",
|
||||
@@ -373,6 +405,8 @@
|
||||
"Week": "Hét",
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "Üdvözöljük",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Év",
|
||||
"Yes": "",
|
||||
"add_keyword": "Kulcsszó hozzáadása",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Active": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"Add_nutrition_recipe": "Ավելացնել սննդայնություն բաղադրատոմսին",
|
||||
@@ -8,6 +10,12 @@
|
||||
"Add_to_Plan": "Ավելացնել պլանին",
|
||||
"Add_to_Shopping": "Ավելացնել գնումներին",
|
||||
"Advanced Search Settings": "Ընդլայնված փնտրման կարգավորումներ",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Apply": "",
|
||||
"Automate": "Ավտոմատացնել",
|
||||
"BatchDeleteConfirm": "",
|
||||
@@ -22,11 +30,15 @@
|
||||
"Categories": "",
|
||||
"Category": "",
|
||||
"Close": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Create": "Ստեղծել",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
|
||||
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
|
||||
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
|
||||
"Credits": "",
|
||||
"DELETE_ERROR": "",
|
||||
"Date": "",
|
||||
"Delete": "",
|
||||
@@ -51,10 +63,14 @@
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Food": "Սննդամթերք",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"Hide_Food": "Թաքցնել սննդամթերքը",
|
||||
"Hide_Keywords": "Թաքցնել բանալի բառը",
|
||||
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
|
||||
@@ -63,12 +79,18 @@
|
||||
"IgnoreAccents": "",
|
||||
"IgnoreAccentsHelp": "",
|
||||
"Import": "Ներմուծել",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_finished": "Ներմուծումն ավարտված է",
|
||||
"Information": "Տեղեկություն",
|
||||
"Ingredients": "",
|
||||
"Keywords": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Գրանցել եփելը",
|
||||
"Log_Recipe_Cooking": "Գրանցել բաղադրատոմսի օգտագործում",
|
||||
"ManageSubscription": "",
|
||||
@@ -78,6 +100,8 @@
|
||||
"MergeAutomateHelp": "",
|
||||
"Merge_Keyword": "Միացնել բանալի բառը",
|
||||
"MissingProperties": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Տեղափոխել",
|
||||
"Move_Food": "Տեղափոխել սննդամթերքը",
|
||||
"Move_Keyword": "Տեղափոխել բանալի բառը",
|
||||
@@ -90,6 +114,8 @@
|
||||
"NoUnit": "",
|
||||
"No_Results": "Արդյունքներ չկան",
|
||||
"Nutrition": "",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"Ok": "",
|
||||
"Open": "",
|
||||
"Parent": "Ծնող",
|
||||
@@ -124,13 +150,19 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "Կարգավորումներ",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Category": "Գնումների կատեգորիա",
|
||||
"Shopping_list": "Գնումների ցուցակ",
|
||||
"Show_as_header": "Ցույց տալ որպես խորագիր",
|
||||
"Size": "",
|
||||
"Skip": "",
|
||||
"Sort_by_new": "Տեսակավորել ըստ նորերի",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
"Step": "",
|
||||
@@ -150,6 +182,8 @@
|
||||
"View_Recipes": "Դիտել բաղադրատոմսերը",
|
||||
"Visibility": "",
|
||||
"Waiting": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Yes": "",
|
||||
"all_fields_optional": "Բոլոր տողերը կամավոր են և կարող են մնալ դատարկ։",
|
||||
"and": "և",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Active": "",
|
||||
"Add": "Tambahkan",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"App": "",
|
||||
"Apply": "",
|
||||
"Are_You_Sure": "",
|
||||
@@ -48,6 +56,7 @@
|
||||
"Coming_Soon": "",
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Salin",
|
||||
"Copy Link": "Salin Tautan",
|
||||
"Copy Token": "Salin Token",
|
||||
@@ -56,6 +65,8 @@
|
||||
"CountMore": "",
|
||||
"Create": "Membuat",
|
||||
"Create Food": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
@@ -63,6 +74,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -114,10 +126,14 @@
|
||||
"FoodOnHand": "",
|
||||
"Food_Alias": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
@@ -135,6 +151,9 @@
|
||||
"IgnoredFood": "",
|
||||
"Image": "Gambar",
|
||||
"Import": "Impor",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -162,8 +181,11 @@
|
||||
"Keywords": "Kata Kunci",
|
||||
"Language": "",
|
||||
"Last_name": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Muat lebih banyak",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Log Memasak",
|
||||
"Log_Recipe_Cooking": "Log Resep Memasak",
|
||||
"Make_Header": "Buat Header",
|
||||
@@ -182,6 +204,8 @@
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Bergerak",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "Pindahkan kebawah",
|
||||
@@ -212,6 +236,8 @@
|
||||
"NotInShopping": "",
|
||||
"Note": "Catatan",
|
||||
"Nutrition": "Nutrisi",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "",
|
||||
"Ok": "Membuka",
|
||||
"OnHand": "",
|
||||
@@ -277,6 +303,7 @@
|
||||
"Selected": "Terpilih",
|
||||
"Servings": "Porsi",
|
||||
"Settings": "Pengaturan",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Bagikan",
|
||||
"Shopping_Categories": "Kategori Belanja",
|
||||
"Shopping_Category": "Kategori Belanja",
|
||||
@@ -288,9 +315,14 @@
|
||||
"Show_as_header": "Tampilkan sebagai tajuk",
|
||||
"Single": "",
|
||||
"Size": "Ukuran",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Urutkan berdasarkan baru",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Starting_Day": "",
|
||||
"StartsWith": "",
|
||||
"StartsWithHelp": "",
|
||||
@@ -340,6 +372,8 @@
|
||||
"Website": "",
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Active": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "",
|
||||
"App": "",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "",
|
||||
"Create Food": "",
|
||||
"Create Recipe": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Created": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"CustomImageHelp": "",
|
||||
@@ -142,10 +154,14 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
@@ -164,6 +180,9 @@
|
||||
"Image": "",
|
||||
"Import": "",
|
||||
"Import Recipe": "",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -194,8 +213,11 @@
|
||||
"Language": "",
|
||||
"Last_name": "",
|
||||
"Learn_More": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Logo": "",
|
||||
@@ -215,6 +237,8 @@
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -251,6 +275,8 @@
|
||||
"Note": "",
|
||||
"Number of Objects": "",
|
||||
"Nutrition": "",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "",
|
||||
"Ok": "",
|
||||
"OnHand": "",
|
||||
@@ -327,6 +353,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"ShoppingBackgroundSyncWarning": "",
|
||||
"Shopping_Categories": "",
|
||||
@@ -342,9 +369,14 @@
|
||||
"Show_as_header": "",
|
||||
"Single": "",
|
||||
"Size": "",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
@@ -411,6 +443,8 @@
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Utilizza IA per importare le immagini delle ricette.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Token di accesso",
|
||||
"Account": "Account",
|
||||
"Actions": "Azioni",
|
||||
"Active": "",
|
||||
"Activity": "Attività",
|
||||
"Add": "Aggiungi",
|
||||
"AddAll": "Aggiungi tutto",
|
||||
@@ -27,6 +29,12 @@
|
||||
"Admin": "Amministratore",
|
||||
"Advanced": "Avanzate",
|
||||
"Advanced Search Settings": "Impostazioni avanzate di ricerca",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Allineamento",
|
||||
"AllRecipes": "Tutte le ricette",
|
||||
"Amount": "Quantità",
|
||||
@@ -88,6 +96,7 @@
|
||||
"Continue": "Continua",
|
||||
"Conversion": "Conversione",
|
||||
"ConversionsHelp": "Con le conversioni è possibile calcolare la quantità di un alimento in diverse unità. Attualmente, questo metodo viene utilizzato solo per il calcolo delle proprietà, ma in futuro potrebbe essere utilizzato anche in altre parti del tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Registro di cucina",
|
||||
"CookLogHelp": "Le voci nel registro di cucina per le ricette. ",
|
||||
"Cooked": "Cucinati",
|
||||
@@ -101,6 +110,8 @@
|
||||
"Create": "Crea",
|
||||
"Create Food": "Crea alimento",
|
||||
"Create Recipe": "Crea ricetta",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Crea voce nel piano alimentare",
|
||||
"Create_New_Food": "Aggiungi nuovo alimento",
|
||||
"Create_New_Keyword": "Aggiungi nuova parola chiave",
|
||||
@@ -110,6 +121,7 @@
|
||||
"Create_New_Unit": "Aggiungi nuova unità",
|
||||
"Created": "Creata",
|
||||
"CreatedBy": "Creata da",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Periodo attuale",
|
||||
"Custom Filter": "Filtro personalizzato",
|
||||
@@ -203,11 +215,15 @@
|
||||
"Food_Replace": "Sostituisci alimento",
|
||||
"Foods": "Alimenti",
|
||||
"Friday": "Venerdì",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Fulltext",
|
||||
"FulltextHelp": "Campi per la ricerca full text. Nota: i metodi di ricerca 'web', 'phrase', e 'raw' funzionano solo con i campi fulltext.",
|
||||
"Fuzzy": "Fuzzy",
|
||||
"FuzzySearchHelp": "Utilizza la ricerca fuzzy per trovare voci anche quando ci sono differenze nel modo in cui la parola è scritta.",
|
||||
"GettingStarted": "Iniziamo",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Raggruppa per",
|
||||
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
|
||||
"Headline": "Intestazione",
|
||||
@@ -233,7 +249,10 @@
|
||||
"Import": "Importa",
|
||||
"Import Recipe": "Importa ricetta",
|
||||
"ImportAll": "Importa tutto",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Importa in Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Si è verificato un errore durante l'importazione. Per avere maggiori informazioni, espandi la sezione dettagli in fondo alla pagina.",
|
||||
"Import_Not_Yet_Supported": "Importazione non ancora supportata",
|
||||
"Import_Result_Info": "{imported} di {total} ricette sono state importate",
|
||||
@@ -272,9 +291,12 @@
|
||||
"Last": "Ultimo",
|
||||
"Last_name": "Cognome",
|
||||
"Learn_More": "Scopri altro",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Collegamento",
|
||||
"Load": "Carica",
|
||||
"Load_More": "Carica altro",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registro ricette cucinate",
|
||||
"Log_Recipe_Cooking": "Aggiungi a ricette cucinate",
|
||||
"Logo": "Logo",
|
||||
@@ -303,6 +325,8 @@
|
||||
"ModelSelectResultsHelp": "Cerca altri risultati",
|
||||
"Monday": "Lunedì",
|
||||
"Month": "Mese",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Altro",
|
||||
"Move": "Sposta",
|
||||
"MoveCategory": "Sposta in: ",
|
||||
@@ -344,6 +368,8 @@
|
||||
"Note": "Nota",
|
||||
"Number of Objects": "Numero di oggetti",
|
||||
"Nutrition": "Nutrienti",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Sei offline, le liste della spesa potrebbero non sincronizzarsi.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Attualmente disponibili",
|
||||
@@ -456,6 +482,7 @@
|
||||
"Servings": "Porzioni",
|
||||
"ServingsText": "Testo porzioni",
|
||||
"Settings": "Impostazioni",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Condividi",
|
||||
"ShopLater": "Compra dopo",
|
||||
"ShopNow": "Compra subito",
|
||||
@@ -479,17 +506,21 @@
|
||||
"Show_as_header": "Mostra come intestazione",
|
||||
"Single": "Singolo",
|
||||
"Size": "Dimensione",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Autenticazione social",
|
||||
"Sort_by_new": "Prima i nuovi",
|
||||
"Source": "Fonte",
|
||||
"SourceImportHelp": "Importa JSON nel formato schema.org/recipe o pagine HTML con ricetta json+ld o microdati.",
|
||||
"SourceImportSubtitle": "Importa manualmente JSON o HTML.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Il tuo spazio ha superato uno dei suoi limiti, alcune funzioni potrebbero essere limitate.",
|
||||
"SpaceLimitReached": "Questo spazio ha raggiunto il limite. Non è possibile creare altri oggetti di questo tipo.",
|
||||
"SpaceMemberHelp": "Aggiungi utenti al tuo spazio creando un collegamento di invito e inviandolo alla persona che desideri aggiungere.",
|
||||
"SpaceMembers": "Membri dello spazio",
|
||||
"SpaceMembersHelp": "Utenti e relativi permessi in uno spazio. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Impostazioni spazio",
|
||||
"Space_Cosmetic_Settings": "Alcune impostazioni cosmetiche possono essere modificate dagli amministratori dell'istanza e sovrascriveranno le impostazioni client per quell'istanza.",
|
||||
"Split": "Dividi",
|
||||
@@ -599,6 +630,8 @@
|
||||
"Week": "Settimana",
|
||||
"Week_Numbers": "Numeri della settimana",
|
||||
"Welcome": "Benvenuto",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Orario lavorativo",
|
||||
"Year": "Anno",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Active": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "Suma",
|
||||
"App": "",
|
||||
@@ -56,6 +64,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -65,6 +74,8 @@
|
||||
"Create": "",
|
||||
"Create Food": "",
|
||||
"Create Recipe": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
@@ -72,6 +83,7 @@
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -128,10 +140,14 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
@@ -150,6 +166,9 @@
|
||||
"Image": "",
|
||||
"Import": "",
|
||||
"Import Recipe": "",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -179,8 +198,11 @@
|
||||
"Language": "",
|
||||
"Last_name": "",
|
||||
"Learn_More": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "",
|
||||
"Load_More": "Įkelti daugiau",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Užregistruoti patiekalo gaminimą",
|
||||
"Log_Recipe_Cooking": "Užregistruoti recepto pagaminimą",
|
||||
"Make_Header": "Padaryti antraštę",
|
||||
@@ -199,6 +221,8 @@
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "Nuleisti žemyn",
|
||||
@@ -232,6 +256,8 @@
|
||||
"Note": "",
|
||||
"Number of Objects": "",
|
||||
"Nutrition": "",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "",
|
||||
"Ok": "",
|
||||
"OnHand": "",
|
||||
@@ -305,6 +331,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"Shopping_Categories": "",
|
||||
"Shopping_Category": "",
|
||||
@@ -316,9 +343,14 @@
|
||||
"Show_as_header": "Rodyti kaip antraštę",
|
||||
"Single": "",
|
||||
"Size": "",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Rūšiuoti pagal naujumą",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
"Starting_Day": "",
|
||||
@@ -381,6 +413,8 @@
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Active": "",
|
||||
"Add": "",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "",
|
||||
"Added_on": "",
|
||||
"Advanced": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "",
|
||||
"Amount": "",
|
||||
"App": "",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "",
|
||||
"Conversion": "",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "",
|
||||
"Copy Link": "",
|
||||
"Copy Token": "",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "",
|
||||
"Create Food": "",
|
||||
"Create Recipe": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "",
|
||||
"Create_New_Unit": "",
|
||||
"Created": "",
|
||||
"Credits": "",
|
||||
"Current_Period": "",
|
||||
"Custom Filter": "",
|
||||
"CustomImageHelp": "",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "",
|
||||
"Food_Replace": "",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "",
|
||||
"Hide_Food": "",
|
||||
"Hide_Keyword": "",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "",
|
||||
"Import": "",
|
||||
"Import Recipe": "",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "",
|
||||
"Last_name": "",
|
||||
"Learn_More": "",
|
||||
"LeaveSpace": "",
|
||||
"Link": "",
|
||||
"Load_More": "",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"Logo": "",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "",
|
||||
"MissingProperties": "",
|
||||
"Month": "",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "",
|
||||
"MoveCategory": "",
|
||||
"Move_Down": "",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "",
|
||||
"Number of Objects": "",
|
||||
"Nutrition": "",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "",
|
||||
"Ok": "",
|
||||
"OnHand": "",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "",
|
||||
"Servings": "",
|
||||
"Settings": "",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "",
|
||||
"ShoppingBackgroundSyncWarning": "",
|
||||
"Shopping_Categories": "",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "",
|
||||
"Single": "",
|
||||
"Size": "",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "",
|
||||
"Week_Numbers": "",
|
||||
"Welcome": "",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "",
|
||||
"Active": "",
|
||||
"Add": "Legg til",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Legg til {food] i handlelisten din",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Lagt til av",
|
||||
"Added_on": "Lagt til",
|
||||
"Advanced": "Avansert",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Justering",
|
||||
"Amount": "Mengde",
|
||||
"App": "App",
|
||||
@@ -55,6 +63,7 @@
|
||||
"Comments_setting": "",
|
||||
"Completed": "Fullført",
|
||||
"Conversion": "Omregn enhet",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopier",
|
||||
"Copy Link": "Kopier lenke",
|
||||
"Copy Token": "Kopier Token",
|
||||
@@ -64,6 +73,8 @@
|
||||
"Create": "Opprett",
|
||||
"Create Food": "",
|
||||
"Create Recipe": "",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Opprett måltidsplanoppføring",
|
||||
"Create_New_Food": "Opprett ny matrett",
|
||||
"Create_New_Keyword": "Opprett nytt nøkkelord",
|
||||
@@ -71,6 +82,7 @@
|
||||
"Create_New_Shopping Category": "Opprett ny handle kategori",
|
||||
"Create_New_Shopping_Category": "Opprett new handle kategori",
|
||||
"Create_New_Unit": "Opprett ny enhet",
|
||||
"Credits": "",
|
||||
"Current_Period": "Gjeldende periode",
|
||||
"Custom Filter": "Egendefinert Filter",
|
||||
"CustomImageHelp": "Last opp et bilde for å vise \"space\"-oversikten.",
|
||||
@@ -134,10 +146,14 @@
|
||||
"FoodOnHand": "Du har {food} på lager.",
|
||||
"Food_Alias": "Matrett Alias",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupér",
|
||||
"Hide_Food": "Skjul Matrett",
|
||||
"Hide_Keyword": "Skjul nøkkelord",
|
||||
@@ -156,6 +172,9 @@
|
||||
"Image": "Bilde",
|
||||
"Import": "Importer",
|
||||
"Import Recipe": "Importer oppskrift",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "",
|
||||
"Import_Not_Yet_Supported": "",
|
||||
"Import_Result_Info": "",
|
||||
@@ -186,8 +205,11 @@
|
||||
"Language": "Språk",
|
||||
"Last_name": "Etternavn",
|
||||
"Learn_More": "Lær mer",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Lenke",
|
||||
"Load_More": "Last inn flere",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Loggfør tilbereding",
|
||||
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
|
||||
"Make_Header": "Bruk som overskrift",
|
||||
@@ -206,6 +228,8 @@
|
||||
"Message": "Melding",
|
||||
"MissingProperties": "",
|
||||
"Month": "Måned",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Flytt",
|
||||
"MoveCategory": "Flytt til: ",
|
||||
"Move_Down": "Flytt ned",
|
||||
@@ -238,6 +262,8 @@
|
||||
"Note": "Merk",
|
||||
"Number of Objects": "Antall objekter",
|
||||
"Nutrition": "Næringsinnhold",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Du er ikke koblet til internett. Det kan hende handlelisten ikke synkroniserer.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "På lager",
|
||||
@@ -312,6 +338,7 @@
|
||||
"Selected": "Valgte",
|
||||
"Servings": "Porsjoner",
|
||||
"Settings": "Innstillinger",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Del",
|
||||
"ShoppingBackgroundSyncWarning": "Dårlig nettverkstilkobling, venter på synkronisering...",
|
||||
"Shopping_Categories": "Butikk Kategorier",
|
||||
@@ -326,9 +353,14 @@
|
||||
"Show_as_header": "Vis som overskrift",
|
||||
"Single": "",
|
||||
"Size": "Størrelse",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "",
|
||||
"Sort_by_new": "Sorter etter nyest",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Split_All_Steps": "",
|
||||
"StartDate": "Startdato",
|
||||
"Starting_Day": "Dag uken skal state på",
|
||||
@@ -390,6 +422,8 @@
|
||||
"Week": "Uke",
|
||||
"Week_Numbers": "Ukenummer",
|
||||
"Welcome": "Velkommen",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "År",
|
||||
"Yes": "",
|
||||
"add_keyword": "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Toegangstoken",
|
||||
"Account": "Account",
|
||||
"Actions": "Acties",
|
||||
"Active": "",
|
||||
"Activity": "Activiteit",
|
||||
"Add": "Voeg toe",
|
||||
"AddAll": "Voeg alles toe",
|
||||
@@ -28,6 +30,12 @@
|
||||
"Admin": "Beheer",
|
||||
"Advanced": "Geavanceerd",
|
||||
"Advanced Search Settings": "Geavanceerde zoekinstellingen",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Afstemming",
|
||||
"AllRecipes": "Alle recepten",
|
||||
"Amount": "Hoeveelheid",
|
||||
@@ -89,6 +97,7 @@
|
||||
"Continue": "Doorgaan",
|
||||
"Conversion": "Omrekening",
|
||||
"ConversionsHelp": "Met omrekeningen kun je de hoeveelheid van een ingrediënt in verschillende eenheden berekenen. Momenteel wordt dit alleen gebruikt voor het berekenen van eigenschappen, later kan het ook in andere onderdelen van Tandoor gebruikt worden. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Kooklogboek",
|
||||
"CookLogHelp": "Items in het kooklogboek voor recepten. ",
|
||||
"Cooked": "Gekookt",
|
||||
@@ -102,6 +111,8 @@
|
||||
"Create": "Aanmaken",
|
||||
"Create Food": "Maak voedingsmiddel",
|
||||
"Create Recipe": "Maak recept",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Maak maaltijdplan",
|
||||
"Create_New_Food": "Voeg nieuw voedingsmiddel toe",
|
||||
"Create_New_Keyword": "Voeg nieuw trefwoord toe",
|
||||
@@ -111,6 +122,7 @@
|
||||
"Create_New_Unit": "Voeg nieuwe eenheid toe",
|
||||
"Created": "Gemaakt",
|
||||
"CreatedBy": "Gemaakt door",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Huidige periode",
|
||||
"Custom Filter": "Aangepast filter",
|
||||
@@ -204,11 +216,15 @@
|
||||
"Food_Replace": "Voedingsmiddelen vervangen",
|
||||
"Foods": "Voedingsmiddelen",
|
||||
"Friday": "Vrijdag",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Volledige tekst",
|
||||
"FulltextHelp": "Velden voor volledige tekstzoekopdrachten. Opmerking: de zoekmethoden ‘web’, ‘zin’ en ‘ruw’ werken alleen met volledige tekstvelden.",
|
||||
"Fuzzy": "Fuzzy",
|
||||
"FuzzySearchHelp": "Gebruik fuzzy search om items te vinden, zelfs als het woord anders is gespeld.",
|
||||
"GettingStarted": "Aan de slag",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Groepeer per",
|
||||
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
|
||||
"Headline": "Koptekst",
|
||||
@@ -234,7 +250,10 @@
|
||||
"Import": "Importeer",
|
||||
"Import Recipe": "Recept importeren",
|
||||
"ImportAll": "Alles importeren",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Importeer in Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Er is een fout opgetreden tijdens je import. Breid de details aan de onderzijde van de pagina uit om ze te bekijken.",
|
||||
"Import_Not_Yet_Supported": "Import nog niet ondersteund",
|
||||
"Import_Result_Info": "{imported} van {total} recepten zijn geïmporteerd",
|
||||
@@ -273,9 +292,12 @@
|
||||
"Last": "Laatste",
|
||||
"Last_name": "Achternaam",
|
||||
"Learn_More": "Meer informatie",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load": "Laden",
|
||||
"Load_More": "Laad meer",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registreer bereiding",
|
||||
"Log_Recipe_Cooking": "Bereiding registreren",
|
||||
"Logo": "Logo",
|
||||
@@ -304,6 +326,8 @@
|
||||
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
|
||||
"Monday": "Maandag",
|
||||
"Month": "Maand",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Meer",
|
||||
"Move": "Verplaats",
|
||||
"MoveCategory": "Verplaats naar: ",
|
||||
@@ -345,6 +369,8 @@
|
||||
"Note": "Notitie",
|
||||
"Number of Objects": "Aantal objecten",
|
||||
"Nutrition": "Voedingswaarde",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Je bent offline, boodschappenlijst synchroniseert mogelijk niet.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Momenteel op voorraad",
|
||||
@@ -457,6 +483,7 @@
|
||||
"Servings": "Porties",
|
||||
"ServingsText": "Portie tekst",
|
||||
"Settings": "Instellingen",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Deel",
|
||||
"ShopLater": "Later boodschappen doen",
|
||||
"ShopNow": "Nu boodschappen doen",
|
||||
@@ -480,17 +507,21 @@
|
||||
"Show_as_header": "Toon als koptekst",
|
||||
"Single": "Enkele",
|
||||
"Size": "Grootte",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Authenticeren met sociale media-account",
|
||||
"Sort_by_new": "Sorteer op nieuw",
|
||||
"Source": "Bron",
|
||||
"SourceImportHelp": "Importeer JSON in schema.org/recipe-formaat of html-pagina’s met json+ld-recepten of microdata.",
|
||||
"SourceImportSubtitle": "Importeer handmatig JSON of HTML.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Je ruimte heeft een limiet overschreden, sommige functies zijn mogelijk beperkt.",
|
||||
"SpaceLimitReached": "Deze ruimte heeft een limiet bereikt. Er kunnen geen objecten van dit type meer worden aangemaakt.",
|
||||
"SpaceMemberHelp": "Voeg gebruikers toe aan je ruimte door een uitnodigingslink aan te maken en naar de persoon te sturen die je wilt toevoegen.",
|
||||
"SpaceMembers": "Gebruikers van de ruimte",
|
||||
"SpaceMembersHelp": "Gebruikers en hun rechten in een ruimte. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Ruimte-instellingen",
|
||||
"Space_Cosmetic_Settings": "Sommige weergave instellingen kunnen worden geforceerd door de administrator van de 'Ruimte' en zullen de persoonlijke instellingen voor die 'Ruimte' overschrijven.",
|
||||
"Split": "Splitsen",
|
||||
@@ -600,6 +631,8 @@
|
||||
"Week": "Week",
|
||||
"Week_Numbers": "Weeknummers",
|
||||
"Welcome": "Welkom",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Bereidingstijd",
|
||||
"Year": "Jaar",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -6,6 +7,7 @@
|
||||
"Access_Token": "Token Dostępu",
|
||||
"Account": "Konto",
|
||||
"Actions": "Akcje",
|
||||
"Active": "",
|
||||
"Activity": "Aktywność",
|
||||
"Add": "Dodaj",
|
||||
"AddAll": "Dodaj wszystkie",
|
||||
@@ -25,6 +27,12 @@
|
||||
"Admin": "Administator",
|
||||
"Advanced": "Zaawansowany",
|
||||
"Advanced Search Settings": "Ustawienia zaawansowanego wyszukiwania",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Wyrównanie",
|
||||
"AllRecipes": "Wszystkie przepisy",
|
||||
"Amount": "Ilość",
|
||||
@@ -83,6 +91,7 @@
|
||||
"Confirm": "Potwierdź",
|
||||
"Continue": "Kontynuuj",
|
||||
"Conversion": "Konwersja",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopiuj",
|
||||
"Copy Link": "Skopiuj link",
|
||||
"Copy Token": "Kopiuj Token",
|
||||
@@ -92,6 +101,8 @@
|
||||
"Create": "Stwórz",
|
||||
"Create Food": "Twórz jedzenie",
|
||||
"Create Recipe": "Utwórz przepis",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Utwórz wpis planu posiłków",
|
||||
"Create_New_Food": "Dodaj nową żywność",
|
||||
"Create_New_Keyword": "Dodaj nowe słowo kluczowe",
|
||||
@@ -100,6 +111,7 @@
|
||||
"Create_New_Shopping_Category": "Dodaj nową kategorię zakupów",
|
||||
"Create_New_Unit": "Dodaj nowa jednostkę",
|
||||
"Created": "Utworzony",
|
||||
"Credits": "",
|
||||
"Current_Period": "Bieżący okres",
|
||||
"Custom Filter": "Filtr niestandardowy",
|
||||
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
|
||||
@@ -169,10 +181,14 @@
|
||||
"Food_Alias": "Alias żywności",
|
||||
"Food_Replace": "Zastąp produkt",
|
||||
"Foods": "Żywność",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupuj według",
|
||||
"Hide_Food": "Ukryj żywność",
|
||||
"Hide_Keyword": "Ukryj słowa kluczowe",
|
||||
@@ -191,6 +207,9 @@
|
||||
"Image": "Obraz",
|
||||
"Import": "Importuj",
|
||||
"Import Recipe": "Importuj przepis",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Podczas importowania wystąpił błąd. Rozwiń Szczegóły na dole strony, aby go wyświetlić.",
|
||||
"Import_Not_Yet_Supported": "Importowanie jeszcze nie wspierane",
|
||||
"Import_Result_Info": "{imported} z {total} przepisów zostało zaimportowanych",
|
||||
@@ -221,8 +240,11 @@
|
||||
"Language": "Język",
|
||||
"Last_name": "Nazwisko",
|
||||
"Learn_More": "Dowiedz się więcej",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Załaduj więcej",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zanotuj ugotowanie",
|
||||
"Log_Recipe_Cooking": "Zaloguj gotowanie przepisu",
|
||||
"Logo": "Logo",
|
||||
@@ -242,6 +264,8 @@
|
||||
"Message": "Wiadomość",
|
||||
"MissingProperties": "",
|
||||
"Month": "Miesiąc",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Przenieś",
|
||||
"MoveCategory": "Przenieś do: ",
|
||||
"Move_Down": "Przesunąć w dół",
|
||||
@@ -278,6 +302,8 @@
|
||||
"Note": "Notatka",
|
||||
"Number of Objects": "Ilość obiektów",
|
||||
"Nutrition": "Odżywianie",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Jesteś offline, lista zakupów może nie być zsynchronizowana.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Obecnie posiadane",
|
||||
@@ -354,6 +380,7 @@
|
||||
"Selected": "Wybrane",
|
||||
"Servings": "Porcje",
|
||||
"Settings": "Ustawienia",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Udostępnij",
|
||||
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
|
||||
"Shopping_Categories": "Kategorie zakupów",
|
||||
@@ -370,9 +397,14 @@
|
||||
"Show_as_header": "Pokaż jako nagłówek",
|
||||
"Single": "Pojedynczy",
|
||||
"Size": "Rozmiar",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Uwierzytelnianie społecznościowe",
|
||||
"Sort_by_new": "Sortuj według nowych",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
|
||||
"Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.",
|
||||
"StartDate": "Data początkowa",
|
||||
@@ -439,6 +471,8 @@
|
||||
"Week": "Tydzień",
|
||||
"Week_Numbers": "Numery tygodni",
|
||||
"Welcome": "Witamy",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Rok",
|
||||
"Yes": "",
|
||||
"add_keyword": "Dodaj słowo kluczowe",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Active": "",
|
||||
"Add": "Adicionar",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Adicionar {food} à sua lista de compras",
|
||||
@@ -14,6 +16,12 @@
|
||||
"Added_by": "Adicionado por",
|
||||
"Added_on": "Adicionado a",
|
||||
"Advanced": "Avançado",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alinhamento",
|
||||
"Amount": "Quantidade",
|
||||
"Apply": "",
|
||||
@@ -46,6 +54,7 @@
|
||||
"Coming_Soon": "",
|
||||
"Completed": "Completo",
|
||||
"Conversion": "Conversão",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copiar",
|
||||
"Copy Link": "Copiar Ligação",
|
||||
"Copy Token": "Copiar Chave",
|
||||
@@ -53,6 +62,8 @@
|
||||
"CountMore": "...+{count} mais",
|
||||
"Create": "Criar",
|
||||
"Create Food": "Criar Comida",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Criar entrada para plano de refeições",
|
||||
"Create_New_Food": "Adicionar nova comida",
|
||||
"Create_New_Keyword": "Adicionar nova palavra-chave",
|
||||
@@ -60,6 +71,7 @@
|
||||
"Create_New_Shopping Category": "Criar nova categoria de Compras",
|
||||
"Create_New_Shopping_Category": "Adicionar nova categoria de compras",
|
||||
"Create_New_Unit": "Adicionar nova unidade",
|
||||
"Credits": "",
|
||||
"Current_Period": "Período atual",
|
||||
"Custom Filter": "",
|
||||
"CustomImageHelp": "Fazer upload de uma image para mostrar na visão geral do espaço.",
|
||||
@@ -114,10 +126,14 @@
|
||||
"FoodOnHand": "Tem {food} disponível.",
|
||||
"Food_Alias": "Alcunha da comida",
|
||||
"Foods": "",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupar por",
|
||||
"Hide_Food": "Esconder comida",
|
||||
"Hide_Keyword": "",
|
||||
@@ -133,6 +149,9 @@
|
||||
"IgnoredFood": "{food} está definida para ignorar compras.",
|
||||
"Image": "Image",
|
||||
"Import": "Importar",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_finished": "Importação terminada",
|
||||
"Information": "Informação",
|
||||
"Ingredient Editor": "Editor de Ingredientes",
|
||||
@@ -153,8 +172,11 @@
|
||||
"Keywords": "Palavras-chave",
|
||||
"Language": "Linguagem",
|
||||
"Learn_More": "Aprenda mais",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Ligação",
|
||||
"Load_More": "Carregar Mais",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registrar Culinária",
|
||||
"Log_Recipe_Cooking": "Registrar Receitas de Culinária",
|
||||
"Make_Header": "Tornar cabeçalho",
|
||||
@@ -171,6 +193,8 @@
|
||||
"Merge_Keyword": "Unir palavra-chave",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mês",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover para: ",
|
||||
"Move_Down": "Mover para baixo",
|
||||
@@ -198,6 +222,8 @@
|
||||
"Note": "Nota",
|
||||
"Number of Objects": "Número de objetos",
|
||||
"Nutrition": "Nutrição",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Está offline, lista das compras poderá não sincronizar.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Atualmente disponível",
|
||||
@@ -267,6 +293,7 @@
|
||||
"Selected": "Selecionado",
|
||||
"Servings": "Doses",
|
||||
"Settings": "Definições",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Partilhar",
|
||||
"Shopping_Categories": "Categorias de Compras",
|
||||
"Shopping_Category": "Categoria de Compras",
|
||||
@@ -278,8 +305,13 @@
|
||||
"Show_Week_Numbers": "Mostrar números das semanas?",
|
||||
"Show_as_header": "Mostrar como cabeçalho",
|
||||
"Size": "Tamanho",
|
||||
"Skip": "",
|
||||
"Sort_by_new": "Ordenar por mais recente",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"StartDate": "Data de início",
|
||||
"Starting_Day": "Dia de início da semana",
|
||||
"StartsWith": "",
|
||||
@@ -329,6 +361,8 @@
|
||||
"Week": "Semana",
|
||||
"Week_Numbers": "Números das semanas",
|
||||
"Welcome": "Bem-vindo",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Ano",
|
||||
"Yes": "",
|
||||
"add_keyword": "Adicionar Palavra Chave",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "IA",
|
||||
"AIImportSubtitle": "Use IA para importar imagens das receitas.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Token de acesso",
|
||||
"Account": "Conta",
|
||||
"Actions": "Ações",
|
||||
"Active": "",
|
||||
"Activity": "Atividade",
|
||||
"Add": "Adicionar",
|
||||
"AddAll": "Adicionar todos",
|
||||
@@ -26,6 +28,12 @@
|
||||
"Added_on": "Incluído Em",
|
||||
"Admin": "Administrador",
|
||||
"Advanced": "Avançado",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Alinhamento",
|
||||
"AllRecipes": "Todas Receitas",
|
||||
"Amount": "Quantidade",
|
||||
@@ -87,6 +95,7 @@
|
||||
"Continue": "Continuar",
|
||||
"Conversion": "Conversão",
|
||||
"ConversionsHelp": "Com conversões, você pode calcular a quantidade de um alimento em diferentes unidades. Atualmente, isso é usado apenas para cálculo de propriedades, posteriormente poderá ser usado em outras partes do Tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Registro de cozimento",
|
||||
"CookLogHelp": "Entradas no registro de cozimento para receitas. ",
|
||||
"Cooked": "Cozido",
|
||||
@@ -100,6 +109,8 @@
|
||||
"Create": "Criar",
|
||||
"Create Food": "Criar Alimento",
|
||||
"Create Recipe": "Criar Receita",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Criar Plano de Refeição",
|
||||
"Create_New_Food": "Incluir Novo Alimento",
|
||||
"Create_New_Keyword": "Incluir Nova Palavra-Chave",
|
||||
@@ -109,6 +120,7 @@
|
||||
"Create_New_Unit": "Incluir Nova Unidade",
|
||||
"Created": "Criado",
|
||||
"CreatedBy": "Criado por",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Período Atual",
|
||||
"Custom Filter": "Filtro Customizado",
|
||||
@@ -202,11 +214,15 @@
|
||||
"Food_Replace": "Substituir Alimento",
|
||||
"Foods": "Alimentos",
|
||||
"Friday": "Sexta-feira",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Texto completo",
|
||||
"FulltextHelp": "Campos para pesquisa textual completa. Observação: os métodos de pesquisa 'web', 'phrase' e 'raw' só funcionam com campos de pesquisa textual completa.",
|
||||
"Fuzzy": "Fuzzy",
|
||||
"FuzzySearchHelp": "Use pesquisa fuzzy para encontrar registros mesmo quando existem diferenças na grafia das palavras utilizadas.",
|
||||
"GettingStarted": "Começando",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Agrupar Por",
|
||||
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
|
||||
"Headline": "Título",
|
||||
@@ -232,7 +248,10 @@
|
||||
"Import": "Importar",
|
||||
"Import Recipe": "Importar Receita",
|
||||
"ImportAll": "Importar todos",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Importar para Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Ocorreu um erro durante a importação. Expanda os detalhes na parte inferior da página para visualizá-los.",
|
||||
"Import_Not_Yet_Supported": "Importação ainda não suportada",
|
||||
"Import_Result_Info": "{imported} de {total} receitas foram importadas",
|
||||
@@ -271,9 +290,12 @@
|
||||
"Last": "Último",
|
||||
"Last_name": "Último Nome",
|
||||
"Learn_More": "Aprender Mais",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load": "Carregar",
|
||||
"Load_More": "Carregar mais",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Registro de Cozinha",
|
||||
"Log_Recipe_Cooking": "Registrar receitas feitas",
|
||||
"Logo": "Logotipo",
|
||||
@@ -296,6 +318,8 @@
|
||||
"Message": "Mensagem",
|
||||
"MissingProperties": "",
|
||||
"Month": "Mês",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mover",
|
||||
"MoveCategory": "Mover Para: ",
|
||||
"Move_Down": "Mover para baixo",
|
||||
@@ -332,6 +356,8 @@
|
||||
"Note": "Nota",
|
||||
"Number of Objects": "Número de Objetos",
|
||||
"Nutrition": "Nutrição",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Você está offline, a lista de compras não pode ser sincronizada.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "Atualmente disponível",
|
||||
@@ -402,6 +428,7 @@
|
||||
"Selected": "Selecionado",
|
||||
"Servings": "Porções",
|
||||
"Settings": "Configurações",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Compartilhar",
|
||||
"ShoppingBackgroundSyncWarning": "Rede ruim, aguardando sincronização...",
|
||||
"Shopping_Categories": "Categorias de Mercado",
|
||||
@@ -418,9 +445,14 @@
|
||||
"Show_as_header": "Mostrar como título",
|
||||
"Single": "Simples",
|
||||
"Size": "Tamanho",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Autenticação social",
|
||||
"Sort_by_new": "Ordenar por novos",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Algumas configurações cosméticas podem ser alteradas pelos administradores do espaço e substituirão as configurações do cliente para esse espaço.",
|
||||
"Split_All_Steps": "Divida todas as linhas em etapas separadas.",
|
||||
"StartDate": "Data Início",
|
||||
@@ -483,6 +515,8 @@
|
||||
"Week": "Semana",
|
||||
"Week_Numbers": "Números da Semana",
|
||||
"Welcome": "Bem vindo",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Ano",
|
||||
"Yes": "",
|
||||
"add_keyword": "Incluir Palavra-Chave",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Cont",
|
||||
"Active": "",
|
||||
"Add": "Adaugă",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "Adăugă {food} în lista de cumpărături",
|
||||
@@ -17,6 +19,12 @@
|
||||
"Added_on": "Adăugat la",
|
||||
"Advanced": "Avansat",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Amount": "Cantitate",
|
||||
"App": "Aplicație",
|
||||
"Apply": "",
|
||||
@@ -53,6 +61,7 @@
|
||||
"Coming_Soon": "În curând",
|
||||
"Comments_setting": "Afișează comentarii",
|
||||
"Completed": "Completat",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Copie",
|
||||
"Copy Link": "Copiere link",
|
||||
"Copy Token": "Copiere token",
|
||||
@@ -62,6 +71,8 @@
|
||||
"Create": "Creează",
|
||||
"Create Food": "Creare mâncare",
|
||||
"Create Recipe": "Crearea rețetei",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Crearea înregistrării în planul de alimentare",
|
||||
"Create_New_Food": "Adaugă mâncare nouă",
|
||||
"Create_New_Keyword": "Adaugă cuvânt cheie nou",
|
||||
@@ -69,6 +80,7 @@
|
||||
"Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
|
||||
"Create_New_Shopping_Category": "Adaugă categorie de cumpărături nouă",
|
||||
"Create_New_Unit": "Adaugă unitate nouă",
|
||||
"Credits": "",
|
||||
"Current_Period": "Perioada curentă",
|
||||
"Custom Filter": "Filtru personalizat",
|
||||
"DELETE_ERROR": "",
|
||||
@@ -121,10 +133,14 @@
|
||||
"FoodOnHand": "Aveți {food} la îndemână.",
|
||||
"Food_Alias": "Pseudonim mâncare",
|
||||
"Foods": "Alimente",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Grupat de",
|
||||
"Hide_Food": "Ascunde mâncare",
|
||||
"Hide_Keyword": "Ascunde cuvintele cheie",
|
||||
@@ -143,6 +159,9 @@
|
||||
"Image": "Imagine",
|
||||
"Import": "Importă",
|
||||
"Import Recipe": "Importă rețeta",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "A apărut o eroare în timpul importului. Vă rugăm să extindeți detaliile din partea de jos a paginii pentru a le vizualiza.",
|
||||
"Import_Not_Yet_Supported": "Importul încă nu este compatibil",
|
||||
"Import_Result_Info": "{imported} din {total} rețete au fost importate",
|
||||
@@ -171,8 +190,11 @@
|
||||
"Keywords": "Cuvinte cheie",
|
||||
"Language": "Limba",
|
||||
"Last_name": "Nume de familie",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Link",
|
||||
"Load_More": "Încărcați mai mult",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Jurnal de pregătire",
|
||||
"Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
|
||||
"Make_Header": "Creare antet",
|
||||
@@ -191,6 +213,8 @@
|
||||
"Message": "Mesaj",
|
||||
"MissingProperties": "",
|
||||
"Month": "Lună",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Mută",
|
||||
"MoveCategory": "Mută la: ",
|
||||
"Move_Down": "Deplasați-vă în jos",
|
||||
@@ -221,6 +245,8 @@
|
||||
"NotInShopping": "{food} nu se află în lista de cumpărături.",
|
||||
"Note": "Notă",
|
||||
"Nutrition": "Nutriție",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Sunteți offline, este posibil ca lista de cumpărături să nu se sincronizeze.",
|
||||
"Ok": "Ok",
|
||||
"OnHand": "În prezent, la îndemână",
|
||||
@@ -289,6 +315,7 @@
|
||||
"Selected": "Selectat",
|
||||
"Servings": "Porții",
|
||||
"Settings": "Setări",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Împărtășire",
|
||||
"Shopping_Categories": "Categorii de cumpărături",
|
||||
"Shopping_Category": "Categorie de cumpărături",
|
||||
@@ -300,9 +327,14 @@
|
||||
"Show_as_header": "Afișare ca antet",
|
||||
"Single": "Singur",
|
||||
"Size": "Marime",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Autentificare socială",
|
||||
"Sort_by_new": "Sortare după nou",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
|
||||
"Starting_Day": "Ziua de început a săptămânii",
|
||||
"StartsWith": "",
|
||||
@@ -359,6 +391,8 @@
|
||||
"Website": "Site web",
|
||||
"Week": "Săptămână",
|
||||
"Week_Numbers": "Numerele săptămânii",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "An",
|
||||
"Yes": "",
|
||||
"add_keyword": "Adăugare cuvânt cheie",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "AI",
|
||||
"AIImportSubtitle": "Используй AI для импорта изображений рецептов.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Токен доступа",
|
||||
"Account": "Аккаунт",
|
||||
"Actions": "Действия",
|
||||
"Active": "",
|
||||
"Activity": "Активность",
|
||||
"Add": "Добавить",
|
||||
"AddAll": "Добавить все",
|
||||
@@ -27,6 +29,12 @@
|
||||
"Admin": "Админ",
|
||||
"Advanced": "Расширенный",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Выравнивание",
|
||||
"AllRecipes": "Все рецепты",
|
||||
"Amount": "Количество",
|
||||
@@ -88,6 +96,7 @@
|
||||
"Continue": "Продолжить",
|
||||
"Conversion": "Преобразование",
|
||||
"ConversionsHelp": "С помощью преобразований вы можете рассчитывать количество продукта в разных единицах измерения. В настоящее время это используется только для расчёта свойств, но в будущем может применяться и в других частях Tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Журнал приготовления",
|
||||
"CookLogHelp": "История приготовлений по рецептам. ",
|
||||
"Cooked": "Приготовлено",
|
||||
@@ -101,6 +110,8 @@
|
||||
"Create": "Создать",
|
||||
"Create Food": "Создать продукт",
|
||||
"Create Recipe": "Создать рецепт",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Создать плана питания",
|
||||
"Create_New_Food": "Добавить новую еду",
|
||||
"Create_New_Keyword": "Добавить ключевое слово",
|
||||
@@ -110,6 +121,7 @@
|
||||
"Create_New_Unit": "Добавить единицу измерения",
|
||||
"Created": "Создано",
|
||||
"CreatedBy": "Создано пользователем",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Текущий период",
|
||||
"Custom Filter": "Пользовательский фильтр",
|
||||
@@ -203,11 +215,15 @@
|
||||
"Food_Replace": "Замена продукта",
|
||||
"Foods": "Продукты",
|
||||
"Friday": "Пятница",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Полнотекстовый",
|
||||
"FulltextHelp": "Поля, используемые в полнотекстовом поиске. Важно: методы поиска web, phrase и raw применимы только к полнотекстовым полям.",
|
||||
"Fuzzy": "Нечёткий",
|
||||
"FuzzySearchHelp": "Нечёткий поиск позволяет находить записи, даже если в написании есть ошибки или отличия.",
|
||||
"GettingStarted": "Начало работы",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Сгруппировать по",
|
||||
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
|
||||
"Headline": "Заголовок",
|
||||
@@ -233,7 +249,10 @@
|
||||
"Import": "Импорт",
|
||||
"Import Recipe": "Импортировать рецепт",
|
||||
"ImportAll": "Импортировать всё",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Импорт в Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Во время импорта произошла ошибка. Для просмотра разверните \"Подробности\" в нижней части страницы.",
|
||||
"Import_Not_Yet_Supported": "Импорт пока не поддерживается",
|
||||
"Import_Result_Info": "{imported} из {total} рецептов были импортированы",
|
||||
@@ -272,9 +291,12 @@
|
||||
"Last": "Последний",
|
||||
"Last_name": "Фамилия",
|
||||
"Learn_More": "Узнать больше",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Гиперссылка",
|
||||
"Load": "Загрузить",
|
||||
"Load_More": "Загрузить еще",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Журнал приготовления",
|
||||
"Log_Recipe_Cooking": "Журнал приготовления",
|
||||
"Logo": "Логотип",
|
||||
@@ -302,6 +324,8 @@
|
||||
"ModelSelectResultsHelp": "Показать больше результатов",
|
||||
"Monday": "Понедельник",
|
||||
"Month": "Месяц",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Ещё",
|
||||
"Move": "Переместить",
|
||||
"MoveCategory": "Переместить в: ",
|
||||
@@ -342,6 +366,8 @@
|
||||
"Note": "Заметка",
|
||||
"Number of Objects": "Количество (шт.)",
|
||||
"Nutrition": "Питательность",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Вы находитесь вне сети, список покупок может не синхронизироваться.",
|
||||
"Ok": "Открыть",
|
||||
"OnHand": "В Наличии",
|
||||
@@ -454,6 +480,7 @@
|
||||
"Servings": "Порции",
|
||||
"ServingsText": "Описание порций",
|
||||
"Settings": "Настройки",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Поделиться",
|
||||
"ShopLater": "Купить позже",
|
||||
"ShopNow": "Купить сейчас",
|
||||
@@ -477,17 +504,21 @@
|
||||
"Show_as_header": "Показывать как заголовок",
|
||||
"Single": "Одиночный",
|
||||
"Size": "Размер",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Социальная аутентификация",
|
||||
"Sort_by_new": "Сортировка по новизне",
|
||||
"Source": "Источник",
|
||||
"SourceImportHelp": "Импортируйте JSON в формате schema.org/recipe или HTML-страницы с рецептами в формате JSON-LD или микроданных.",
|
||||
"SourceImportSubtitle": "Импортировать JSON или HTML вручную.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Ваше пространство превысило один из лимитов, некоторые функции могут быть ограничены.",
|
||||
"SpaceLimitReached": "В этом пространстве достигнут лимит. Новые объекты данного типа создавать нельзя.",
|
||||
"SpaceMemberHelp": "Для добавления пользователей создайте пригласительную ссылку и передайте её человеку, которого хотите пригласить.",
|
||||
"SpaceMembers": "Участники пространства",
|
||||
"SpaceMembersHelp": "Пользователи и их права доступа в пространстве. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Настройки пространства",
|
||||
"Space_Cosmetic_Settings": "Администраторы пространства могут менять некоторые визуальные настройки, которые будут переопределять настройки клиента для данного пространства.",
|
||||
"Split": "Разделить",
|
||||
@@ -597,6 +628,8 @@
|
||||
"Week": "Неделя",
|
||||
"Week_Numbers": "Номер недели",
|
||||
"Welcome": "Добро пожаловать",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Время работы",
|
||||
"Year": "Год",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"AI": "Umetna inteligenca",
|
||||
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Access_Token": "Dostopni žeton",
|
||||
"Account": "Račun",
|
||||
"Actions": "Dejanja",
|
||||
"Active": "",
|
||||
"Activity": "Aktivnost",
|
||||
"Add": "Dodaj",
|
||||
"AddAll": "Dodaj vse",
|
||||
@@ -27,6 +29,12 @@
|
||||
"Admin": "Skrbnik",
|
||||
"Advanced": "Napredno",
|
||||
"Advanced Search Settings": "",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Poravnava",
|
||||
"AllRecipes": "Vsi recepti",
|
||||
"Amount": "Količina",
|
||||
@@ -88,6 +96,7 @@
|
||||
"Continue": "Nadaljuj",
|
||||
"Conversion": "Pogovor",
|
||||
"ConversionsHelp": "S pretvorbami lahko izračunate količino živila v različnih enotah. Trenutno se to uporablja le za izračun lastnosti, kasneje pa se lahko uporabi tudi v drugih delih Tandoorja. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Kuharski dnevnik",
|
||||
"CookLogHelp": "Vnosi v dnevnik kuhanja za recepte. ",
|
||||
"Cooked": "Kuhano",
|
||||
@@ -101,6 +110,8 @@
|
||||
"Create": "Ustvari",
|
||||
"Create Food": "Ustvari živilo",
|
||||
"Create Recipe": "Ustvari recept",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
|
||||
"Create_New_Food": "Dodaj Novo Hrano",
|
||||
"Create_New_Keyword": "Dodaj novo ključno besedo",
|
||||
@@ -110,6 +121,7 @@
|
||||
"Create_New_Unit": "Dodaj novo enoto",
|
||||
"Created": "Ustvarjeno",
|
||||
"CreatedBy": "Ustvaril/a",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Trenutno obdobje",
|
||||
"Custom Filter": "Filter po meri",
|
||||
@@ -203,11 +215,15 @@
|
||||
"Food_Replace": "Zamenjava živila",
|
||||
"Foods": "Živila",
|
||||
"Friday": "Petek",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "Celotno besedilo",
|
||||
"FulltextHelp": "Polja za iskanje po celotnem besedilu. Opomba: metode iskanja »splet«, »fraza« in »surovo« delujejo samo s polji po celotnem besedilu.",
|
||||
"Fuzzy": "Nejasno",
|
||||
"FuzzySearchHelp": "Uporabite mehko iskanje za iskanje vnosov, tudi če obstajajo razlike v načinu pisanja besede.",
|
||||
"GettingStarted": "Začetek",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Združi po",
|
||||
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
|
||||
"Headline": "Glavni naslov",
|
||||
@@ -233,7 +249,10 @@
|
||||
"Import": "Uvozi",
|
||||
"Import Recipe": "Uvozi recept",
|
||||
"ImportAll": "Uvozi vse",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportIntoTandoor": "Uvozi v Tandoor",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.",
|
||||
"Import_Not_Yet_Supported": "Uvoz še ni podprt",
|
||||
"Import_Result_Info": "Uvoženih je bilo {imported} od {total} receptov",
|
||||
@@ -272,9 +291,12 @@
|
||||
"Last": "Zadnji",
|
||||
"Last_name": "Priimek",
|
||||
"Learn_More": "Preberite Več",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Hiperpovezava",
|
||||
"Load": "Naloži",
|
||||
"Load_More": "Naloži več",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Zgodovina kuhanja",
|
||||
"Log_Recipe_Cooking": "Beleži kuharski recept",
|
||||
"Logo": "Logotip",
|
||||
@@ -303,6 +325,8 @@
|
||||
"ModelSelectResultsHelp": "Išči več rezultatov",
|
||||
"Monday": "Ponedeljek",
|
||||
"Month": "Mesec",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"More": "Več",
|
||||
"Move": "Premakni",
|
||||
"MoveCategory": "Premakni v: ",
|
||||
@@ -344,6 +368,8 @@
|
||||
"Note": "Opomba",
|
||||
"Number of Objects": "Število predmetov",
|
||||
"Nutrition": "Prehrana",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Si v načinu brez povezave, nakupovalni listek se mogoče ne bo sinhroniziral.",
|
||||
"Ok": "V redu",
|
||||
"OnHand": "Trenutno imam v roki",
|
||||
@@ -456,6 +482,7 @@
|
||||
"Servings": "Porcije",
|
||||
"ServingsText": "Besedilo o porcijah",
|
||||
"Settings": "Nastavitve",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Deli",
|
||||
"ShopLater": "Nakupujte pozneje",
|
||||
"ShopNow": "Nakupujte zdaj",
|
||||
@@ -479,17 +506,21 @@
|
||||
"Show_as_header": "Prikaži kot glavo",
|
||||
"Single": "Ena",
|
||||
"Size": "Velikost",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Socialna avtentikacija",
|
||||
"Sort_by_new": "Razvrsti po novih",
|
||||
"Source": "Vir",
|
||||
"SourceImportHelp": "Uvozite JSON v formatu schema.org/recipe ali na straneh html z receptom json+ld ali mikropodatki.",
|
||||
"SourceImportSubtitle": "Ročno uvozite JSON ali HTML.",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceLimitExceeded": "Vaš prostor je presegel eno od svojih omejitev, nekatere funkcije so morda omejene.",
|
||||
"SpaceLimitReached": "Ta prostor je dosegel omejitev. Te vrste predmetov ni mogoče ustvariti več.",
|
||||
"SpaceMemberHelp": "Dodajte uporabnike v svoj prostor tako, da ustvarite povezavo za povabilo in jo pošljete osebi, ki jo želite dodati.",
|
||||
"SpaceMembers": "Člani prostora",
|
||||
"SpaceMembersHelp": "Uporabniki in njihova dovoljenja v prostoru. ",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"SpaceSettings": "Nastavitve prostora",
|
||||
"Space_Cosmetic_Settings": "Nekatere kozmetične nastavitve lahko spremenijo skrbniki prostora in bodo preglasile nastavitve odjemalca za ta prostor.",
|
||||
"Split": "Razdelitev",
|
||||
@@ -599,6 +630,8 @@
|
||||
"Week": "Teden",
|
||||
"Week_Numbers": "Števila tednov",
|
||||
"Welcome": "Dobrodošli",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"WorkingTime": "Delovni čas",
|
||||
"Year": "Leto",
|
||||
"Yes": "",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"AIImportSubtitle": "Använd AI för att importera bilder av recept.",
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
@@ -7,6 +8,7 @@
|
||||
"Access_Token": "Åtkomstnyckel",
|
||||
"Account": "Konto",
|
||||
"Actions": "Åtgärder",
|
||||
"Active": "",
|
||||
"Activity": "Aktivitet",
|
||||
"Add": "Lägg till",
|
||||
"AddAll": "Lägg till alla",
|
||||
@@ -26,6 +28,12 @@
|
||||
"Added_on": "Tillagd på",
|
||||
"Admin": "Administratör",
|
||||
"Advanced": "Avancerat",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Orientering",
|
||||
"AllRecipes": "Alla recept",
|
||||
"Amount": "Mängd",
|
||||
@@ -87,6 +95,7 @@
|
||||
"Continue": "Fortsätt",
|
||||
"Conversion": "Omvandling",
|
||||
"ConversionsHelp": "Med omvandlingar kan du beräkna mängden av ett livsmedel i olika enheter. För närvarande används detta endast för egenskapsberäkning, senare kan det även användas i andra delar av Tandoor. ",
|
||||
"ConvertUsingAI": "",
|
||||
"CookLog": "Tillagningslogg",
|
||||
"CookLogHelp": "Poster i tillagningsloggen för recept. ",
|
||||
"Cooked": "Tillagad",
|
||||
@@ -100,6 +109,8 @@
|
||||
"Create": "Skapa",
|
||||
"Create Food": "Skapa livsmedel",
|
||||
"Create Recipe": "Skapa recept",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Skapa en måltidsplan",
|
||||
"Create_New_Food": "Lägg till nytt livsmedel",
|
||||
"Create_New_Keyword": "Lägg till nytt nyckelord",
|
||||
@@ -109,6 +120,7 @@
|
||||
"Create_New_Unit": "Lägg till enhet",
|
||||
"Created": "Skapad",
|
||||
"CreatedBy": "Skapad av",
|
||||
"Credits": "",
|
||||
"Ctrl+K": "Ctrl+K",
|
||||
"Current_Period": "Nuvarande period",
|
||||
"Custom Filter": "Anpassat filter",
|
||||
@@ -180,10 +192,14 @@
|
||||
"Food_Alias": "Alias för livsmedel",
|
||||
"Food_Replace": "Ersätt ingrediens",
|
||||
"Foods": "Livsmedel",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Gruppera enligt",
|
||||
"Hide_Food": "Dölj livsmedel",
|
||||
"Hide_Keyword": "Dölj nyckelord",
|
||||
@@ -202,6 +218,9 @@
|
||||
"Image": "Bild",
|
||||
"Import": "Importera",
|
||||
"Import Recipe": "Importera recept",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "Ett fel uppstod under din import. Expandera informationen längst ner på sidan för att se den.",
|
||||
"Import_Not_Yet_Supported": "Import stöds inte ännu",
|
||||
"Import_Result_Info": "{imported} av totalt {total} recept blev importerat",
|
||||
@@ -232,8 +251,11 @@
|
||||
"Language": "Språk",
|
||||
"Last_name": "Efternamn",
|
||||
"Learn_More": "Läs mer",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Länk",
|
||||
"Load_More": "Ladda mer",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Logga tillagning",
|
||||
"Log_Recipe_Cooking": "Logga tillagningen av receptet",
|
||||
"Logo": "Logga",
|
||||
@@ -253,6 +275,8 @@
|
||||
"Message": "Meddelande",
|
||||
"MissingProperties": "",
|
||||
"Month": "Månad",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Flytta",
|
||||
"MoveCategory": "Flytta till: ",
|
||||
"Move_Down": "Flytta ned",
|
||||
@@ -289,6 +313,8 @@
|
||||
"Note": "Anteckning",
|
||||
"Number of Objects": "Antal objekt",
|
||||
"Nutrition": "Näringsinnehåll",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.",
|
||||
"Ok": "Öppna",
|
||||
"OnHand": "För närvarande till hands",
|
||||
@@ -365,6 +391,7 @@
|
||||
"Selected": "Vald",
|
||||
"Servings": "Portioner",
|
||||
"Settings": "Inställningar",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Dela",
|
||||
"ShoppingBackgroundSyncWarning": "Dålig uppkoppling, inväntar synkronisering...",
|
||||
"Shopping_Categories": "Shopping kategorier",
|
||||
@@ -381,9 +408,14 @@
|
||||
"Show_as_header": "Visa som rubrik",
|
||||
"Single": "Enstaka",
|
||||
"Size": "Storlek",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Social autentisering",
|
||||
"Sort_by_new": "Sortera efter ny",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Vissa kosmetiska inställningar kan ändras av hushålls-administratörer och skriver över klientinställningar för det hushållet.",
|
||||
"Split_All_Steps": "Dela upp alla rader i separata steg.",
|
||||
"StartDate": "Startdatum",
|
||||
@@ -450,6 +482,8 @@
|
||||
"Week": "Vecka",
|
||||
"Week_Numbers": "Veckonummer",
|
||||
"Welcome": "Välkommen",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "År",
|
||||
"Yes": "",
|
||||
"add_keyword": "Lägg till nyckelord",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"AISettingsHostedHelp": "",
|
||||
"API": "API",
|
||||
"API_Browser": "",
|
||||
"API_Documentation": "",
|
||||
"Account": "Hesap",
|
||||
"Active": "",
|
||||
"Add": "Ekle",
|
||||
"AddChild": "",
|
||||
"AddFoodToShopping": "{food}'ı alışveriş listenize ekleyin",
|
||||
@@ -16,6 +18,12 @@
|
||||
"Added_by": "Ekleyen",
|
||||
"Added_on": "Eklenme Zamanı",
|
||||
"Advanced": "Gelişmiş",
|
||||
"AiCreditsBalance": "",
|
||||
"AiLog": "",
|
||||
"AiLogHelp": "",
|
||||
"AiModelHelp": "",
|
||||
"AiProvider": "",
|
||||
"AiProviderHelp": "",
|
||||
"Alignment": "Hizalama",
|
||||
"Amount": "Miktar",
|
||||
"App": "Uygulama",
|
||||
@@ -57,6 +65,7 @@
|
||||
"Comments_setting": "Yorumları Göster",
|
||||
"Completed": "Tamamlandı",
|
||||
"Conversion": "Dönüşüm",
|
||||
"ConvertUsingAI": "",
|
||||
"Copy": "Kopyala",
|
||||
"Copy Link": "Bağlantıyı Kopyala",
|
||||
"Copy Token": "Anahtarı Kopyala",
|
||||
@@ -66,6 +75,8 @@
|
||||
"Create": "Oluştur",
|
||||
"Create Food": "Yiyecek Oluştur",
|
||||
"Create Recipe": "Tarif Oluştur",
|
||||
"CreateFirstRecipe": "",
|
||||
"CreateInvitation": "",
|
||||
"Create_Meal_Plan_Entry": "Yemek planı girişi oluştur",
|
||||
"Create_New_Food": "Yeni Yiyecek Ekle",
|
||||
"Create_New_Keyword": "Yeni Anahtar Kelime Ekle",
|
||||
@@ -74,6 +85,7 @@
|
||||
"Create_New_Shopping_Category": "Yeni Alışveriş Kategorisi Ekle",
|
||||
"Create_New_Unit": "Yeni Birim Ekle",
|
||||
"Created": "Oluşturuldu",
|
||||
"Credits": "",
|
||||
"Current_Period": "Mevcut Dönem",
|
||||
"Custom Filter": "Özel Filtre",
|
||||
"CustomImageHelp": "Alan genel bakışında gösterilecek bir resim yükleyin.",
|
||||
@@ -143,10 +155,14 @@
|
||||
"Food_Alias": "Yiyecek Takma Adı",
|
||||
"Food_Replace": "Yiyecek Değiştir",
|
||||
"Foods": "Yiyecekler",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
"FulltextHelp": "",
|
||||
"Fuzzy": "",
|
||||
"FuzzySearchHelp": "",
|
||||
"Global": "",
|
||||
"GlobalHelp": "",
|
||||
"Group": "",
|
||||
"GroupBy": "Gruplandırma Ölçütü",
|
||||
"Hide_Food": "Yiyeceği Gizle",
|
||||
"Hide_Keyword": "Anahtar kelimeleri gizle",
|
||||
@@ -165,6 +181,9 @@
|
||||
"Image": "Resim",
|
||||
"Import": "İçeriye Aktar",
|
||||
"Import Recipe": "Tarif İçe Aktar",
|
||||
"ImportFirstRecipe": "",
|
||||
"ImportMealPlans": "",
|
||||
"ImportShoppingList": "",
|
||||
"Import_Error": "İçeri aktarma sırasında bir hata oluştu. Görüntülemek için lütfen sayfanın altındaki Ayrıntıları genişletin.",
|
||||
"Import_Not_Yet_Supported": "İçe aktarma henüz desteklenmiyor",
|
||||
"Import_Result_Info": "{total} tariften {imported} tanesi içe aktarıldı",
|
||||
@@ -195,8 +214,11 @@
|
||||
"Language": "Dil",
|
||||
"Last_name": "Soyisim",
|
||||
"Learn_More": "Daha Fazla",
|
||||
"LeaveSpace": "",
|
||||
"Link": "Bağlantı",
|
||||
"Load_More": "Daha Fazla Yükle",
|
||||
"LogCredits": "",
|
||||
"LogCreditsHelp": "",
|
||||
"Log_Cooking": "Günlük Pişirme",
|
||||
"Log_Recipe_Cooking": "Günlük Tarif Pişirme",
|
||||
"Logo": "Logo",
|
||||
@@ -216,6 +238,8 @@
|
||||
"Message": "Mesaj",
|
||||
"MissingProperties": "",
|
||||
"Month": "Ay",
|
||||
"MonthlyCredits": "",
|
||||
"MonthlyCreditsUsed": "",
|
||||
"Move": "Taşı",
|
||||
"MoveCategory": "Taşı: ",
|
||||
"Move_Down": "Aşağıya Taşı",
|
||||
@@ -252,6 +276,8 @@
|
||||
"Note": "Not",
|
||||
"Number of Objects": "Nesne Sayısı",
|
||||
"Nutrition": "Besin Değeri",
|
||||
"NutritionsPerServing": "",
|
||||
"NutritionsPerServingHelp": "",
|
||||
"OfflineAlert": "Çevrimdışısınız, alışveriş listesi senkronize edilemeyebilir.",
|
||||
"Ok": "Tamam",
|
||||
"OnHand": "Şu anda Elinizde",
|
||||
@@ -328,6 +354,7 @@
|
||||
"Selected": "Seçilen",
|
||||
"Servings": "Servis Sayısı",
|
||||
"Settings": "Ayarlar",
|
||||
"SettingsOnlySuperuser": "",
|
||||
"Share": "Paylaş",
|
||||
"ShoppingBackgroundSyncWarning": "Kötü bağlantı, senkronizasyon bekleniyor...",
|
||||
"Shopping_Categories": "Alışveriş Kategorileri",
|
||||
@@ -344,9 +371,14 @@
|
||||
"Show_as_header": "Başlık olarak göster",
|
||||
"Single": "Tek",
|
||||
"Size": "Boyut",
|
||||
"Skip": "",
|
||||
"Social_Authentication": "Sosyal Kimlik Doğrulama",
|
||||
"Sort_by_new": "Yeniye göre sırala",
|
||||
"Space": "",
|
||||
"SpaceHelp": "",
|
||||
"SpaceMembersHelp": "",
|
||||
"SpaceName": "",
|
||||
"SpacePrivateObjectsHelp": "",
|
||||
"Space_Cosmetic_Settings": "Bazı kozmetik ayarlar alan yöneticileri tarafından değiştirilebilir ve o alanın istemci ayarlarını geçersiz kılar.",
|
||||
"Split_All_Steps": "Tüm satırları ayrı adımlara bölün.",
|
||||
"StartDate": "Başlangıç Tarihi",
|
||||
@@ -413,6 +445,8 @@
|
||||
"Week": "Hafta",
|
||||
"Week_Numbers": "Hafta numaraları",
|
||||
"Welcome": "Hoşgeldiniz",
|
||||
"WelcomeSettingsHelp": "",
|
||||
"WelcometoTandoor": "",
|
||||
"Year": "Yıl",
|
||||
"Yes": "",
|
||||
"add_keyword": "Anahtar Kelime Ekle",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user