Compare commits

..

76 Commits

Author SHA1 Message Date
Brian Choromanski
c63932e8b3 fix: Updated pwa orientation to any (#6298) 2025-10-01 20:51:15 -05:00
renovate[bot]
3ba2227bc7 chore(deps): update dependency mkdocs-material to v9.6.21 (#6293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 04:45:41 +00:00
renovate[bot]
67af391c6b chore(deps): update dependency pillow-heif to v1.1.1 (#6291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 23:33:14 -05:00
renovate[bot]
70ae0dac25 chore(deps): update node.js to d367fd3 (#6292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 03:19:44 +00:00
renovate[bot]
e15a9c3c9f chore(deps): update dependency apprise to v1.9.5 (#6290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 22:07:31 -05:00
renovate[bot]
9d40d60b3b fix(deps): update dependency openai to v2 (#6294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 21:40:14 -05:00
renovate[bot]
e2760f7247 chore(deps): update dependency tzdata to v2025 (#6287) 2025-09-29 17:18:19 -05:00
Michael Genson
83bf21b947 fix: Restore recipe meta for non-logged-in users (#6286) 2025-09-29 10:33:18 -05:00
Michael Genson
d1824affff fix: Default to "0" qty when creating ingredients everywhere (#6285) 2025-09-29 10:19:37 -05:00
renovate[bot]
4827e1092f chore(deps): update dependency beautifulsoup4 to v4.14.2 (#6283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:49:54 -05:00
github-actions[bot]
7db767b075 chore(auto): Update pre-commit hooks (#6282)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2025-09-29 14:36:01 +00:00
renovate[bot]
afdd0b15dc fix(deps): update dependency fastapi to ^0.118.0 (#6281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:24:37 -05:00
Michael Genson
37c9166a77 docs: Update TOKEN_TIME docs to include max (#6279) 2025-09-28 22:05:15 -05:00
github-actions[bot]
ba0b9d4cd9 docs(auto): Update image tag, for release v3.3.0 (#6267)
Co-authored-by: michael-genson <71845777+michael-genson@users.noreply.github.com>
2025-09-28 01:13:15 +00:00
renovate[bot]
9fd99a86b8 chore(deps): update dependency beautifulsoup4 to v4.14.0 (#6260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 20:02:31 -05:00
Michael Genson
824603a578 fix: Stores Not Populating Sometimes (#6266) 2025-09-27 19:17:08 -05:00
Hayden
e3f120c680 chore(l10n): New Crowdin updates (#6264) 2025-09-27 16:23:01 -05:00
Michael Genson
d16a10440d chore: Add Stricter Frontend Formatting (#6262) 2025-09-27 13:57:53 -05:00
Michael Genson
ecdf7de386 chore: Upgrade Node and Nuxt (#6240) 2025-09-27 12:26:02 -05:00
Hayden
0e10ed8461 chore(l10n): New Crowdin updates (#6257) 2025-09-27 15:50:58 +02:00
Michael Genson
1684169e7b fix: Check for non-hid properties when injetcing SPA meta (#6256) 2025-09-26 16:07:13 -05:00
Hayden
3d9f2bef82 chore(l10n): New Crowdin updates (#6254) 2025-09-26 17:21:43 +00:00
Michael Genson
a722b05fb5 fix: Make Ingredient Parser Dialog Use Full Space (#6253) 2025-09-26 11:45:41 -05:00
Michael Genson
187e83eeb5 fix: Misc Issues with Ingredient Parser (#6250) 2025-09-26 11:25:15 -05:00
renovate[bot]
f3cc51190c fix(deps): update dependency bcrypt to v5 (#6246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 15:05:42 +00:00
renovate[bot]
33aedd6904 chore(deps): update dependency pyyaml to v6.0.3 (#6245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 09:52:20 -05:00
renovate[bot]
ea9a25a891 chore(deps): update dependency pydantic-settings to v2.11.0 (#6233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 09:30:37 -05:00
Hayden
3a237258a1 chore(l10n): New Crowdin updates (#6241) 2025-09-26 06:00:25 +00:00
Michael Genson
d29de8e679 feat: Simplify Default Layout Logic and Add Household.name To Cookbooks API (#6243) 2025-09-25 18:01:10 -05:00
Michael Genson
79367872ac fix: Remove Double Cookie Refresh (#6242) 2025-09-25 14:55:07 -05:00
renovate[bot]
f058dec27b chore(deps): update dependency lxml to v6.0.2 (#6219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 16:14:49 +00:00
renovate[bot]
c87acf54db chore(deps): update dependency coverage to v7.10.7 (#6216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 16:02:08 +00:00
renovate[bot]
84c144e40f fix(deps): update dependency fastapi to ^0.117.0 (#6205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 15:49:22 +00:00
renovate[bot]
474cf299cd fix(deps): update dependency uvicorn to ^0.37.0 (#6200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 15:32:35 +00:00
renovate[bot]
1cababc5a5 chore(deps): update dependency ruff to v0.13.2 (#6239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 15:14:28 +00:00
renovate[bot]
8705bcf195 chore(deps): update dependency openai to v1.109.1 (#6196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 14:59:21 +00:00
Hayden
bdb511c1c8 chore(l10n): New Crowdin updates (#6237) 2025-09-25 09:44:03 -05:00
Carter
c9f3f65f36 fix: Remove constraint on unhashed password being 'LDAP' (#6236) 2025-09-24 23:32:28 -05:00
Hayden
3ec55f0e48 chore(l10n): New Crowdin updates (#6234) 2025-09-24 16:33:34 +00:00
Patrick Lehner (he/him)
7d43c7c7a2 docs: Improve formatting in 'Automating Backups with n8n' community guide (#6221)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-09-24 15:16:56 +00:00
Michael Genson
c710e9d3f5 fix: Enabled Using Mealie In iframe If Served Over HTTPS (#6128) 2025-09-24 09:58:17 -05:00
Hayden
0313e6b3b8 chore(l10n): New Crowdin updates (#6231) 2025-09-24 08:38:03 +02:00
Michael Genson
24b890136d fix: Workflow Issues with Deleting Ingredient In Parser (#6230) 2025-09-23 17:36:49 -05:00
Patrick Lehner (he/him)
4b67554b36 docs: Update navigation instructions for (admin) settings pages (#6220) 2025-09-23 22:14:19 +00:00
Michael Genson
679a42a7cc feat: Ingredient Parser Enhancements (#6228) 2025-09-23 17:03:35 -05:00
Hayden
4dfc32a314 chore(l10n): New Crowdin updates (#6225) 2025-09-23 21:06:34 +00:00
Michael Genson
96acc6fc4b fix: Remove explicit timeout from OpenAI image API Call (#6227) 2025-09-23 12:39:23 -05:00
Hayden
249c9e8f23 chore(l10n): New Crowdin updates (#6224) 2025-09-23 03:18:17 +00:00
Hayden
7413185300 chore(l10n): New Crowdin updates (#6218) 2025-09-22 15:35:33 +00:00
github-actions[bot]
6168ea0150 chore(auto): Update pre-commit hooks (#6222)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2025-09-22 15:21:36 +00:00
Patrick Lehner (he/him)
f7ba7862d4 docs: Fix formatting in some community guides (#6223) 2025-09-22 10:11:15 -05:00
Michael Genson
cec6d2c5ec fix: Actually Fix Token Time (#6215) 2025-09-21 19:51:19 -05:00
renovate[bot]
b27977fbdf chore(deps): update dependency mypy to v1.18.2 (#6193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 16:14:29 +00:00
github-actions[bot]
2a60b330ac chore(l10n): Crowdin locale sync (#6206)
Co-authored-by: GitHub Action <action@github.com>
2025-09-21 16:03:16 +00:00
Hayden
72ec5bd13e chore(l10n): New Crowdin updates (#6213) 2025-09-21 10:53:10 -05:00
Hayden
bb45cbb0a2 chore(l10n): New Crowdin updates (#6176) 2025-09-21 05:12:32 +00:00
Michael Genson
c929a03b57 feat: Upgraded Ingredient Parsing Workflow (#6151) 2025-09-21 04:37:14 +00:00
Michael Genson
9e5a54477f docs: Add Info Regarding Theme Settings Config (#6198) 2025-09-20 12:58:59 -05:00
Arsène Reymond
078b4563b3 fix: multiple RecipeRating backend calls (#6194)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-09-20 15:27:17 +00:00
Michael Genson
a9090bc2bd feat: Manually calculate OpenAI Parsing Confidence (#6141) 2025-09-19 23:09:34 -05:00
renovate[bot]
cb8c1423c5 chore(deps): update dependency ruff to v0.13.1 (#6191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 16:35:33 -05:00
Arsène Reymond
f6a1b5f4eb fix: ingredient linker and instructions titles (#6146)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-09-19 17:38:29 +00:00
Michael Genson
7623b72c4c fix: Print Button Does Nothing (#6178) 2025-09-18 12:48:36 -05:00
renovate[bot]
17d40e34df fix(deps): update dependency openai to v1.108.0 (#6185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 22:14:45 -05:00
renovate[bot]
bade6968a3 fix(deps): update dependency authlib to v1.6.4 (#6182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 14:50:42 -05:00
renovate[bot]
92a142125f fix(deps): update dependency fastapi to v0.116.2 (#6181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 00:19:17 +00:00
renovate[bot]
d39c2a2874 chore(deps): update dependency mkdocs-material to v9.6.20 (#6179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 00:07:10 +00:00
renovate[bot]
324de7fb10 chore(deps): update dependency pytest-asyncio to v1.2.0 (#6162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 23:55:05 +00:00
renovate[bot]
c4544ea042 chore(deps): update dependency mypy to v1.18.1 (#6161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 18:41:31 -05:00
renovate[bot]
a5dda74812 fix(deps): update dependency pydantic to v2.11.9 (#6159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 23:15:24 +00:00
renovate[bot]
fd7e58e40c fix(deps): update dependency openai to v1.107.3 (#6147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 23:01:40 +00:00
renovate[bot]
5e42841a7d chore(deps): update node.js to abcf9c9 (#6138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 22:45:31 +00:00
github-actions[bot]
ae9306b8c2 chore(auto): Update pre-commit hooks (#6174)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2025-09-16 22:34:13 +00:00
renovate[bot]
7f0c5cbcc4 chore(deps): update dependency ruff to ^0.13.0 (#6148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 17:23:32 -05:00
github-actions[bot]
a7d8bcc6ba docs(auto): Update image tag, for release v3.2.1 (#6172)
Co-authored-by: michael-genson <71845777+michael-genson@users.noreply.github.com>
2025-09-15 19:45:07 +00:00
Hayden
b94ef78a12 chore(l10n): New Crowdin updates (#6145) 2025-09-15 04:19:55 +00:00
196 changed files with 12008 additions and 11233 deletions

View File

@@ -11,7 +11,7 @@
// Use -bullseye variants on local on arm64/Apple Silicon. // Use -bullseye variants on local on arm64/Apple Silicon.
"VARIANT": "3.12-bullseye", "VARIANT": "3.12-bullseye",
// Options // Options
"NODE_VERSION": "20" "NODE_VERSION": "22"
} }
}, },
"mounts": [ "mounts": [

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup node env 🏗 - name: Setup node env 🏗
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.0
with: with:
node-version: 20 node-version: 22
check-latest: true check-latest: true
- name: Get yarn cache directory path 🛠 - name: Get yarn cache directory path 🛠

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: 'yarn' cache: 'yarn'
cache-dependency-path: ./tests/e2e/yarn.lock cache-dependency-path: ./tests/e2e/yarn.lock
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -14,7 +14,7 @@ jobs:
- name: Setup node env 🏗 - name: Setup node env 🏗
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.0
with: with:
node-version: 20 node-version: 22
check-latest: true check-latest: true
- name: Get yarn cache directory path 🛠 - name: Get yarn cache directory path 🛠

View File

@@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/ exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.12.12 rev: v0.13.2
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format

View File

@@ -59,8 +59,11 @@
"netlify.toml": "runtime.txt", "netlify.toml": "runtime.txt",
"README.md": "LICENSE, SECURITY.md" "README.md": "LICENSE, SECURITY.md"
}, },
"[typescript]": {
"editor.formatOnSave": true
},
"[vue]": { "[vue]": {
"editor.formatOnSave": false "editor.formatOnSave": true
}, },
"[python]": { "[python]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,

View File

@@ -1,3 +1,4 @@
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -105,12 +106,16 @@ def main():
# Flatten list of lists # Flatten list of lists
all_children = [item for sublist in all_children for item in sublist] all_children = [item for sublist in all_children for item in sublist]
out_path = GENERATED / "__init__.py"
render_python_template( render_python_template(
TEMPLATE, TEMPLATE,
GENERATED / "__init__.py", out_path,
{"children": all_children}, {"children": all_children},
) )
subprocess.run(["poetry", "run", "ruff", "check", str(out_path), "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", str(out_path)])
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,5 +1,6 @@
import pathlib import pathlib
import re import re
import subprocess
from dataclasses import dataclass, field from dataclasses import dataclass, field
from utils import PROJECT_DIR, log, render_python_template from utils import PROJECT_DIR, log, render_python_template
@@ -84,16 +85,23 @@ def find_modules(root: pathlib.Path) -> list[Modules]:
return modules return modules
def main(): def main() -> None:
modules = find_modules(SCHEMA_PATH) modules = find_modules(SCHEMA_PATH)
template_paths: list[pathlib.Path] = []
for module in modules: for module in modules:
log.debug(f"Module: {module.directory.name}") log.debug(f"Module: {module.directory.name}")
for file in module.files: for file in module.files:
log.debug(f" File: {file.import_path}") log.debug(f" File: {file.import_path}")
log.debug(f" Classes: [{', '.join(file.classes)}]") log.debug(f" Classes: [{', '.join(file.classes)}]")
render_python_template(template, module.directory / "__init__.py", {"module": module}) template_path = module.directory / "__init__.py"
template_paths.append(template_path)
render_python_template(template, template_path, {"module": module})
path_args = (str(p) for p in template_paths)
subprocess.run(["poetry", "run", "ruff", "check", *path_args, "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", *path_args])
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,4 +1,5 @@
import re import re
import subprocess
from pathlib import Path from pathlib import Path
from jinja2 import Template from jinja2 import Template
@@ -189,6 +190,7 @@ def generate_typescript_types() -> None: # noqa: C901
skipped_dirs: list[Path] = [] skipped_dirs: list[Path] = []
failed_modules: list[Path] = [] failed_modules: list[Path] = []
out_paths: list[Path] = []
for module in schema_path.iterdir(): for module in schema_path.iterdir():
if module.is_dir() and module.stem in ignore_dirs: if module.is_dir() and module.stem in ignore_dirs:
skipped_dirs.append(module) skipped_dirs.append(module)
@@ -205,10 +207,18 @@ def generate_typescript_types() -> None: # noqa: C901
path_as_module = path_to_module(module) path_as_module = path_to_module(module)
generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore
clean_output_file(out_path) clean_output_file(out_path)
out_paths.append(out_path)
except Exception: except Exception:
failed_modules.append(module) failed_modules.append(module)
log.exception(f"Module Error: {module}") log.exception(f"Module Error: {module}")
# Run ESLint --fix on the files to clean up any formatting issues
subprocess.run(
["yarn", "lint", "--fix", *(str(path) for path in out_paths)],
check=True,
cwd=PROJECT_DIR / "frontend",
)
log.debug("\n📁 Skipped Directories:") log.debug("\n📁 Skipped Directories:")
for skipped_dir in skipped_dirs: for skipped_dir in skipped_dirs:
log.debug(f" 📁 {skipped_dir.name}") log.debug(f" 📁 {skipped_dir.name}")

View File

@@ -1,5 +1,4 @@
import logging import logging
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -23,11 +22,6 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict):
dest.write_text(text) dest.write_text(text)
# lint/format file with Ruff
log.info(f"Formatting {dest}")
subprocess.run(["poetry", "run", "ruff", "check", str(dest), "--fix"])
subprocess.run(["poetry", "run", "ruff", "format", str(dest)])
@dataclass @dataclass
class CodeSlicer: class CodeSlicer:
@@ -37,7 +31,7 @@ class CodeSlicer:
indentation: str | None indentation: str | None
text: list[str] text: list[str]
_next_line = None _next_line: int | None = None
def purge_lines(self) -> None: def purge_lines(self) -> None:
start = self.start + 1 start = self.start + 1

View File

@@ -1,7 +1,7 @@
############################################### ###############################################
# Frontend Build # Frontend Build
############################################### ###############################################
FROM node:20@sha256:f3e50c7689a1b6982fab45b1b23ba5adf1fd725e233dc640918fb59f7a57b174 \ FROM node:22@sha256:d367fd3fce932a9d3bc3816c23f85313d9b44e58e1fed49ef90a10c4b99409e8 \
AS frontend-builder AS frontend-builder
WORKDIR /frontend WORKDIR /frontend

View File

@@ -34,7 +34,7 @@ Make sure the VSCode Dev Containers extension is installed, then select "Dev Con
- [Python 3.12](https://www.python.org/downloads/) - [Python 3.12](https://www.python.org/downloads/)
- [Poetry](https://python-poetry.org/docs/#installation) - [Poetry](https://python-poetry.org/docs/#installation)
- [Node v16.x](https://nodejs.org/en/) - [Node](https://nodejs.org/en/)
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable) - [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)
- [task](https://taskfile.dev/#/installation) - [task](https://taskfile.dev/#/installation)

View File

@@ -1,5 +1,5 @@
!!! info !!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
Mealie supports adding the ingredients of a recipe to your [Bring](https://www.getbring.com/) shopping list, as you can Mealie supports adding the ingredients of a recipe to your [Bring](https://www.getbring.com/) shopping list, as you can
see [here](https://docs.mealie.io/documentation/getting-started/features/#recipe-actions). see [here](https://docs.mealie.io/documentation/getting-started/features/#recipe-actions).

View File

@@ -1,21 +1,21 @@
!!! info !!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard. In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard.
### Display Today's Meal in Lovelace ## Display Today's Meal in Lovelace
You can use the Mealie API to get access to meal plans in Home Assistant like in the image below. You can use the Mealie API to get access to meal plans in Home Assistant like in the image below.
![api-extras-gif](../../assets/img/home-assistant-card.png) ![api-extras-gif](../../assets/img/home-assistant-card.png)
Steps: ## Steps:
#### 1. Get your API Token ### 1. Get your API Token
Create an API token from Mealie's User Settings page (https://docs.mealie.io/documentation/getting-started/api-usage/#getting-a-token) Create an API token from Mealie's User Settings page (see [this page](https://docs.mealie.io/documentation/getting-started/api-usage/#getting-a-token) to learn how).
#### 2. Create Home Assistant Sensors ### 2. Create Home Assistant Sensors
Create REST sensors in home assistant to get the details of today's meal. Create REST sensors in home assistant to get the details of today's meal.
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal. We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
@@ -40,7 +40,7 @@ rest:
unique_id: mealie_todays_meal_id unique_id: mealie_todays_meal_id
``` ```
#### 3. Create a Camera Entity ### 3. Create a Camera Entity
We will create a camera entity to display the image of today's meal in Lovelace. We will create a camera entity to display the image of today's meal in Lovelace.
@@ -52,7 +52,7 @@ In the still image url field put in:
Under the entity page for the new camera, rename it. Under the entity page for the new camera, rename it.
e.g. `camera.mealie_todays_meal_image` e.g. `camera.mealie_todays_meal_image`
#### 4. Create a Lovelace Card ### 4. Create a Lovelace Card
Create a picture entity card and set the entity to `mealie_todays_meal` and the camera entity to `camera.mealie_todays_meal_image` or set in the yaml directly. Create a picture entity card and set the entity to `mealie_todays_meal` and the camera entity to `camera.mealie_todays_meal_image` or set in the yaml directly.
@@ -76,4 +76,4 @@ card_mod:
``` ```
!!! tip !!! tip
Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This requires an [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS. Due to how Home Assistant works with images, I had to include the additional styling to get the images to not appear distorted. This requires an [additional installation](https://github.com/thomasloven/lovelace-card-mod) from HACS.

View File

@@ -12,12 +12,10 @@ var url = document.URL.endsWith('/') ?
document.URL; document.URL;
var mealie = "http://localhost:8080"; var mealie = "http://localhost:8080";
var group_slug = "home" // Change this to your group slug. You can obtain this from your URL after logging-in to Mealie var group_slug = "home" // Change this to your group slug. You can obtain this from your URL after logging-in to Mealie
var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that
var edity = "&edit=1" // Optional - keep in edit mode - update to "" if you don't want that
if (mealie.slice(-1) === "/") { if (mealie.slice(-1) === "/") {
mealie = mealie.slice(0, -1) mealie = mealie.slice(0, -1)
} }
var dest = mealie + "/g/" + group_slug + "/r/create/url?recipe_import_url=" + url + use_keywords + edity; var dest = mealie + "/g/" + group_slug + "/r/create/url?recipe_import_url=" + url;
window.open(dest, "_blank"); window.open(dest, "_blank");
``` ```

View File

@@ -1,9 +1,10 @@
!!! info !!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
An easy way to add recipes to Mealie from an Apple device is via an Apple Shortcut. This is a short guide to install an configure a shortcut able to add recipes via a link or image(s). An easy way to add recipes to Mealie from an Apple device is via an Apple Shortcut. This is a short guide to install an configure a shortcut able to add recipes via a link or image(s).
*Note: if adding via images make sure to enable [Mealie's openai integration](https://docs.mealie.io/documentation/getting-started/installation/open-ai/)* !!! note
If adding via images make sure to enable [Mealie's OpenAI Integration](https://docs.mealie.io/documentation/getting-started/installation/open-ai/)
## Javascript can only be run via Shortcuts on the Safari browser on MacOS and iOS. If you do not use Safari you may skip this section ## Javascript can only be run via Shortcuts on the Safari browser on MacOS and iOS. If you do not use Safari you may skip this section
Some sites have begun blocking AI scraping bots, inadvertently blocking the recipe scraping library Mealie uses as well. To circumvent this, the shortcut uses javascript to capture the raw html loaded in the browser and sends that to mealie when possible. Some sites have begun blocking AI scraping bots, inadvertently blocking the recipe scraping library Mealie uses as well. To circumvent this, the shortcut uses javascript to capture the raw html loaded in the browser and sends that to mealie when possible.
@@ -16,12 +17,13 @@ Settings app -> apps -> Shortcuts -> Advanced -> Allow Running Scripts
Shortcuts app -> Settings (CMD ,) -> Advanced -> Allow Running Scripts Shortcuts app -> Settings (CMD ,) -> Advanced -> Allow Running Scripts
## Initial setup ## Initial Setup
An API key is needed to authenticate with mealie. To create an api key for a user, navigate to http://YOUR_MEALIE_URL/user/profile/api-tokens. Alternatively you can create a key via the mealie home page by clicking the user's profile pic in the top left -> Api Tokens An API key is needed to authenticate with mealie. To create an api key for a user, navigate to http://YOUR_MEALIE_URL/user/profile/api-tokens. Alternatively you can create a key via the mealie home page by clicking the user's profile pic in the top left -> Api Tokens
The shortcut can be installed via **[This link](https://www.icloud.com/shortcuts/52834724050b42aebe0f2efd8d067360)**. Upon install, replace "MEALIE_API_KEY" with the API key generated previously and "MEALIE_URI" with the full URL used to access your mealie instance e.g. "http://10.0.0.5:9000" or "https://mealie.domain.com". The shortcut can be installed via **[This link](https://www.icloud.com/shortcuts/52834724050b42aebe0f2efd8d067360)**. Upon install, replace "MEALIE_API_KEY" with the API key generated previously and "MEALIE_URI" with the full URL used to access your mealie instance e.g. "http://10.0.0.5:9000" or "https://mealie.domain.com".
## Using the shortcut ## Using the Shortcut
Once installed, the shortcut will automatically appear as an option when sharing an image or webpage. It can also be useful to add the shortcut to the home screen of your device. If selected from the home screen or shortcuts app, a menu will appear with prompts to import via **taking photo(s)**, **selecting photo(s)**, **scanning a URL**, or **pasting a URL**. Once installed, the shortcut will automatically appear as an option when sharing an image or webpage. It can also be useful to add the shortcut to the home screen of your device. If selected from the home screen or shortcuts app, a menu will appear with prompts to import via **taking photo(s)**, **selecting photo(s)**, **scanning a URL**, or **pasting a URL**.
*Note: despite the mealie API being able to accept multiple recipe images for import it is currently impossible to send multiple files in 1 web request via Shortcuts. Instead, the shortcut combines the images into a singular, vertically-concatenated image to send to mealie. This can result in slightly less-accurate text recognition.* !!! note
Despite the Mealie API being able to accept multiple recipe images for import it is currently impossible to send multiple files in 1 web request via Shortcuts. Instead, the shortcut combines the images into a singular, vertically-concatenated image to send to mealie. This can result in slightly less-accurate text recognition.

View File

@@ -1,71 +1,77 @@
# Automating Backups with n8n # Automating Backups with n8n
!!! info !!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
> [n8n](https://github.com/n8n-io/n8n) is a free and source-available fair-code licensed workflow automation tool. Alternative to Zapier or Make, allowing you to use a UI to create automated workflows. [n8n](https://github.com/n8n-io/n8n) is a free and source-available fair-code licensed workflow automation tool. It's an alternative to tools like Zapier or Make, allowing you to use a UI to create automated workflows.
This example workflow: This example workflow:
1. Backups Mealie every morning via an API call 1. Creates a Mealie backup every morning via an API call
2. Deletes all but the last 7 backups 2. Keeps the last 7 backups, deleting older ones
> [!CAUTION] !!! warning "Important"
> This only automates the backup function, this does not backup your data to anywhere except your local instance. Please make sure you are backing up your data to an external source. This only automates the backup function, this does not backup your data to anywhere except your local instance. Please make sure you are backing up your data to an external source.
---
![screenshot](../../assets/img/n8n/n8n-mealie-backup.png) ![screenshot](../../assets/img/n8n/n8n-mealie-backup.png)
# Setup ## Setup
## Deploying n8n ### Deploying n8n
Follow the relevant guide in the [n8n Documentation](https://docs.n8n.io/) Follow the relevant guide in the [n8n Documentation](https://docs.n8n.io/)
## Importing n8n workflow ### Importing n8n workflow
1. In n8n, add a new workflow 1. In n8n, add a new workflow
2. In the top right hit the 3 dot menu and select 'Import from URL...' 2. In the top right hit the 3 dot menu and select 'Import from URL...'
![screenshot](../../assets/img/n8n/n8n-workflow-import.png) ![screenshot](../../assets/img/n8n/n8n-workflow-import.png)
3. Paste `https://github.com/mealie-recipes/mealie/blob/mealie-next/docs/docs/assets/other/n8n/n8n-mealie-backup.json` and click Import 3. Paste `https://github.com/mealie-recipes/mealie/blob/mealie-next/docs/docs/assets/other/n8n/n8n-mealie-backup.json` and click 'Import'
4. Click through the nodes and update the URLs for your environment 4. Click through the nodes and update the URLs for your environment
## API Credentials ### API Credentials
#### Generate Mealie API Token #### Generate Mealie API Token
1. Head to https://mealie.example.com/user/profile/api-tokens 1. Head to `<YOUR MEALIE INSTANCE>/user/profile/api-tokens`
> If you dont see this screen make sure that "Show advanced features" is checked under https://mealie.example.com/user/profile/edit
2. Under token name, enter the name of the token i.e. 'n8n' and hit Generate !!! tip
If you dont see this screen make sure that "Show advanced features" is checked under `<YOUR MEALIE INSTANCE>/user/profile/edit`
2. Under token name, enter the name of the token (for example, 'n8n') and hit 'Generate'
3. Copy and keep this API Token somewhere safe, this is like your password! 3. Copy and keep this API Token somewhere safe, this is like your password!
> You can use your normal user for this, but assuming you're an admin you could also choose to create a user named n8n and generate the API key against that user. !!! tip
You can use your normal user for this, but assuming you're an admin you could also choose to create a user named n8n and generate the API key against that user.
#### Setup Credentials in n8n #### Setup Credentials in n8n
> [n8n Docs](https://docs.n8n.io/credentials/add-edit-credentials/) See also [n8n Docs](https://docs.n8n.io/credentials/add-edit-credentials/).
1. Create a new "Header Auth" Credential 1. Create a new "Header Auth" Credential
![screenshot](../../assets/img/n8n/n8n-cred-app.png) ![screenshot](../../assets/img/n8n/n8n-cred-app.png)
2. In the connection screen set - Name as `Authorization` - Value as `Bearer {INSERT MEALIE API KEY}` 2. In the connection screen set - Name as `Authorization` - Value as `Bearer {INSERT MEALIE API KEY}`
![screenshot](../../assets/img/n8n/n8n-cred-connection.png) ![screenshot](../../assets/img/n8n/n8n-cred-connection.png)
3. In the workflow you created, for the "Run Backup", "Get All backups", and "Delete Oldies" nodes, update: 3. In the workflow you created, for the "Run Backup", "Get All backups", and "Delete Oldies" nodes, update:
- Authentication to `Generic Credential Type`
- Generic Auth Type to `Header Auth`
- Header Auth to `Mealie API` or whatever you named your credentials
![screenshot](../../assets/img/n8n/n8n-workflow-auth.png) - Authentication to `Generic Credential Type`
- Generic Auth Type to `Header Auth`
- Header Auth to `Mealie API` or whatever you named your credentials
## Notification Node ![screenshot](../../assets/img/n8n/n8n-workflow-auth.png)
> Please use error notifications of some kind. It's very easy to set and forget an automation, then have the worst happen and lose data. ### Notification Node
!!! warning "Important"
Please use error notifications of some kind. It's very easy to set and forget an automation, then have the worst happen and lose data.
[ntfy](https://github.com/binwiederhier/ntfy) is a great open source, self-hostable tool for sending notifications. [ntfy](https://github.com/binwiederhier/ntfy) is a great open source, self-hostable tool for sending notifications.

View File

@@ -1,11 +1,10 @@
# Using SWAG as Reverse Proxy # Using SWAG as Reverse Proxy
!!! info !!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag).
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag)
SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention. SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.
## Step 1: Get a domain ## Step 1: Get a domain

View File

@@ -111,7 +111,7 @@
You can change the theme by settings the environment variables. You can change the theme by settings the environment variables.
- [Backend Config - Themeing](./installation/backend-config.md#themeing) - [Backend Config - Theming](./installation/backend-config.md#theming)
??? question "How can I change the login session timeout?" ??? question "How can I change the login session timeout?"

View File

@@ -11,7 +11,7 @@
| DEFAULT_GROUP | Home | The default group for users | | DEFAULT_GROUP | Home | The default group for users |
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group | | DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
| BASE_URL | http://localhost:8080 | Used for Notifications | | BASE_URL | http://localhost:8080 | Used for Notifications |
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid | | TOKEN_TIME | 48 | The time in hours that a login/auth token is valid. Must be <= 87600 (10 years, in hours). |
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** | | API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally | | API_DOCS | True | Turns on/off access to the API documentation locally |
| TZ | UTC | Must be set to get correct date/time on the server | | TZ | UTC | Must be set to get correct date/time on the server |
@@ -138,6 +138,13 @@ For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x. Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
!!! info
If you're setting these variables but not seeing these changes persist, try removing the `#` character. Also, depending on which syntax you're using, double-check you're using quotes correctly.
If using YAML mapping syntax, be sure to include quotes around these values, otherwise they will be treated as comments in your YAML file:<br>`THEME_LIGHT_PRIMARY: '#E58325'` or `THEME_LIGHT_PRIMARY: 'E58325'`
If using YAML sequence syntax, don't include any quotes:<br>`THEME_LIGHT_PRIMARY=#E58325` or `THEME_LIGHT_PRIMARY=E58325`
| Variables | Default | Description | | Variables | Default | Description |
| --------------------- | :-----: | --------------------------- | | --------------------- | :-----: | --------------------------- |
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable | | THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do: We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case! 1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.2.0` 2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.3.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access. 3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container 4. Restart the container

View File

@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml ```yaml
services: services:
mealie: mealie:
image: ghcr.io/mealie-recipes/mealie:v3.2.0 # (3) image: ghcr.io/mealie-recipes/mealie:v3.3.0 # (3)
container_name: mealie container_name: mealie
restart: always restart: always
ports: ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml ```yaml
services: services:
mealie: mealie:
image: ghcr.io/mealie-recipes/mealie:v3.2.0 # (3) image: ghcr.io/mealie-recipes/mealie:v3.3.0 # (3)
container_name: mealie container_name: mealie
restart: always restart: always
ports: ports:

View File

@@ -2,7 +2,7 @@
Mealie provides an integrated mechanic for doing full installation backups of the database. Mealie provides an integrated mechanic for doing full installation backups of the database.
Navigate to Settings > Backups or manually by adding `/admin/backups` to your instance URL. Navigate to Settings > Admin Settings > Backups or manually by adding `/admin/backups` to your instance URL.
From this page, you will be able to: From this page, you will be able to:
@@ -39,7 +39,7 @@ Restoring the Database when using Postgres requires Mealie to be configured with
```sql ```sql
ALTER USER mealie WITH SUPERUSER; ALTER USER mealie WITH SUPERUSER;
# Run restore from Mealie -- Run restore from Mealie
ALTER USER mealie WITH NOSUPERUSER; ALTER USER mealie WITH NOSUPERUSER;
``` ```

View File

@@ -1,6 +1,7 @@
# Permissions and Public Access # Permissions and Public Access
Mealie provides various levels of user access and permissions. This includes: Mealie provides various levels of user access and permissions. This includes:
- Authentication and registration ([LDAP](../authentication/ldap.md) and [OpenID Connect](../authentication/oidc.md) are both supported) - Authentication and registration ([LDAP](../authentication/ldap.md) and [OpenID Connect](../authentication/oidc.md) are both supported)
- Customizable user permissions - Customizable user permissions
- Fine-tuned public access for non-users - Fine-tuned public access for non-users
@@ -8,12 +9,12 @@ Mealie provides various levels of user access and permissions. This includes:
## Customizable User Permissions ## Customizable User Permissions
Each user can be configured to have varying levels of access. Some of these permissions include: Each user can be configured to have varying levels of access. Some of these permissions include:
- Access to Administrator tools - Access to Administrator tools
- Access to inviting other users - Access to inviting other users
- Access to manage their group and group data - Access to manage their group and group data
Administrators can navigate to the Settings page and access the User Management page to configure these settings. Administrators can configure these settings on the User Management page (navigate to Settings > Admin Settings > Users or append `/admin/manage/users` to your instance URL).
[User Management Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary } [User Management Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
@@ -22,8 +23,8 @@ Administrators can navigate to the Settings page and access the User Management
By default, groups and households are set to private, meaning only logged-in users may access the group/household. In order for a recipe to be viewable by public (not logged-in) users, three criteria must be met: By default, groups and households are set to private, meaning only logged-in users may access the group/household. In order for a recipe to be viewable by public (not logged-in) users, three criteria must be met:
1. The group must not be private 1. The group must not be private
2. The household must not be private, *and* the household setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Household Settings page 2. The household must not be private, _and_ the household setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Household Management page (navigate to Settings > Admin Settings > Households or append `/admin/manage/households` to your instance URL)
2. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page 3. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page
Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)). Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)).

File diff suppressed because one or more lines are too long

View File

@@ -86,7 +86,7 @@ nav:
- Community Guides: - Community Guides:
- Bring API without internet exposure: "documentation/community-guide/bring-api.md" - Bring API without internet exposure: "documentation/community-guide/bring-api.md"
- Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md" - Automating Backups with n8n: "documentation/community-guide/n8n-backup-automation.md"
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md" - Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
- Home Assistant: "documentation/community-guide/home-assistant.md" - Home Assistant: "documentation/community-guide/home-assistant.md"
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md" - Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"

View File

@@ -37,7 +37,7 @@
} }
.handle { .handle {
cursor: grab; cursor: grab !important;
} }
.hidden { .hidden {

View File

@@ -32,9 +32,9 @@
> >
<div class="d-flex align-center w-100 mb-2"> <div class="d-flex align-center w-100 mb-2">
<v-toolbar-title class="headline mb-0"> <v-toolbar-title class="headline mb-0">
<v-icon size="large" class="mr-3"> <v-icon size="large" class="mr-3">
{{ $globals.icons.pages }} {{ $globals.icons.pages }}
</v-icon> </v-icon>
{{ book.name }} {{ book.name }}
</v-toolbar-title> </v-toolbar-title>
<BaseButton <BaseButton

View File

@@ -1,44 +1,44 @@
<template> <template>
<div v-if="preferences"> <div v-if="preferences">
<BaseCardSectionTitle :title="$t('household.household-preferences')" /> <BaseCardSectionTitle :title="$t('household.household-preferences')" />
<div class="mb-6"> <div class="mb-6">
<v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" /> <v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
<div class="ml-8"> <div class="ml-8">
<p class="text-subtitle-2 my-0 py-0"> <p class="text-subtitle-2 my-0 py-0">
{{ $t("household.private-household-description") }} {{ $t("household.private-household-description") }}
</p> </p>
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" /> <DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
</div> </div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" /> <v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
<div class="ml-8"> <div class="ml-8">
<p class="text-subtitle-2 my-0 py-0"> <p class="text-subtitle-2 my-0 py-0">
{{ $t("household.lock-recipe-edits-from-other-households-description") }} {{ $t("household.lock-recipe-edits-from-other-households-description") }}
</p> </p>
</div> </div>
</div> </div>
<v-select <v-select
v-model="preferences.firstDayOfWeek" v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin" :prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays" :items="allDays"
item-title="name" item-title="name"
item-value="value" item-value="value"
:label="$t('settings.first-day-of-week')" :label="$t('settings.first-day-of-week')"
variant="underlined" variant="underlined"
flat flat
/> />
<BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" /> <BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" />
<div class="preference-container"> <div class="preference-container">
<div v-for="p in recipePreferences" :key="p.key"> <div v-for="p in recipePreferences" :key="p.key">
<v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" /> <v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" />
<p class="ml-8 text-subtitle-2 my-0 py-0"> <p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }} {{ p.description }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -432,9 +432,9 @@ function removeField(index: number) {
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => { const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
/* newFields.forEach((field, index) => { /* newFields.forEach((field, index) => {
const updatedField = getFieldFromFieldDef(field); const updatedField = getFieldFromFieldDef(field);
fields.value[index] = updatedField; // recursive!!! fields.value[index] = updatedField; // recursive!!!
}); */ }); */
const qf = buildQueryFilterString(fields.value, state.showAdvanced); const qf = buildQueryFilterString(fields.value, state.showAdvanced);
if (qf) { if (qf) {

View File

@@ -5,8 +5,14 @@
density="compact" density="compact"
elevation="0" elevation="0"
> >
<BaseDialog v-model="deleteDialog" :title="$t('recipe.delete-recipe')" color="error" <BaseDialog
:icon="$globals.icons.alertCircle" can-confirm @confirm="emitDelete()"> v-model="deleteDialog"
:title="$t('recipe.delete-recipe')"
color="error"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="emitDelete()"
>
<v-card-text> <v-card-text>
{{ $t("recipe.delete-confirmation") }} {{ $t("recipe.delete-confirmation") }}
</v-card-text> </v-card-text>
@@ -15,7 +21,14 @@
<v-spacer /> <v-spacer />
<div v-if="!open" class="custom-btn-group ma-1"> <div v-if="!open" class="custom-btn-group ma-1">
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always /> <RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" /> <RecipeTimelineBadge
v-if="loggedIn"
class="ml-1"
color="info"
button-style
:slug="recipe.slug"
:recipe-name="recipe.name!"
/>
<div v-if="loggedIn"> <div v-if="loggedIn">
<v-tooltip v-if="canEdit" location="bottom" color="info"> <v-tooltip v-if="canEdit" location="bottom" color="info">
<template #activator="{ props: tooltipProps }"> <template #activator="{ props: tooltipProps }">

View File

@@ -1,101 +1,101 @@
<template> <template>
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition --> <!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
<div> <div>
<v-hover <v-hover
v-slot="{ isHovering, props: hoverProps }" v-slot="{ isHovering, props: hoverProps }"
:open-delay="50" :open-delay="50"
>
<v-card
v-bind="hoverProps"
:class="{ 'on-hover': isHovering }"
:style="{ cursor }"
:elevation="isHovering ? 12 : 2"
:to="recipeRoute"
:min-height="imageHeight + 75"
@click.self="$emit('click')"
> >
<v-card <RecipeCardImage
v-bind="hoverProps" :icon-size="imageHeight"
:class="{ 'on-hover': isHovering }" :height="imageHeight"
:style="{ cursor }" :slug="slug"
:elevation="isHovering ? 12 : 2" :recipe-id="recipeId"
:to="recipeRoute" size="small"
:min-height="imageHeight + 75" :image-version="image"
@click.self="$emit('click')"
> >
<RecipeCardImage <v-expand-transition v-if="description">
:icon-size="imageHeight" <div
:height="imageHeight" v-if="isHovering"
:slug="slug" class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
:recipe-id="recipeId" style="height: 100%"
size="small"
:image-version="image"
>
<v-expand-transition v-if="description">
<div
v-if="isHovering"
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
style="height: 100%"
>
<v-card-text class="v-card--text-show white--text">
<div class="descriptionWrapper">
<SafeMarkdown :source="description" />
</div>
</v-card-text>
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="mb-n3 px-4">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
<slot name="actions">
<v-card-actions
v-if="showRecipeContent"
class="px-1"
> >
<RecipeFavoriteBadge <v-card-text class="v-card--text-show white--text">
v-if="isOwnGroup" <div class="descriptionWrapper">
class="absolute" <SafeMarkdown :source="description" />
:recipe-id="recipeId" </div>
show-always </v-card-text>
/> </div>
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent --> </v-expand-transition>
</RecipeCardImage>
<v-card-title class="mb-n3 px-4">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
<RecipeCardRating <slot name="actions">
:model-value="rating" <v-card-actions
:recipe-id="recipeId" v-if="showRecipeContent"
/> class="px-1"
<v-spacer /> >
<RecipeChips <RecipeFavoriteBadge
:truncate="true" v-if="isOwnGroup"
:items="tags" class="absolute"
:title="false" :recipe-id="recipeId"
:limit="2" show-always
small />
url-prefix="tags" <div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
v-bind="$attrs"
/>
<!-- If we're not logged-in, no items display, so we hide this menu --> <RecipeCardRating
<RecipeContextMenu :model-value="rating"
v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId"
color="grey-darken-2" />
:slug="slug" <v-spacer />
:menu-icon="$globals.icons.dotsVertical" <RecipeChips
:name="name" :truncate="true"
:recipe-id="recipeId" :items="tags"
:use-items="{ :title="false"
delete: false, :limit="2"
edit: false, small
download: true, url-prefix="tags"
mealplanner: true, v-bind="$attrs"
shoppingList: true, />
print: false,
printPreferences: false, <!-- If we're not logged-in, no items display, so we hide this menu -->
share: true, <RecipeContextMenu
}" v-if="isOwnGroup && showRecipeContent"
@deleted="$emit('delete', slug)" color="grey-darken-2"
/> :slug="slug"
</v-card-actions> :menu-icon="$globals.icons.dotsVertical"
</slot> :name="name"
<slot /> :recipe-id="recipeId"
</v-card> :use-items="{
</v-hover> delete: false,
</div> edit: false,
download: true,
mealplanner: true,
shoppingList: true,
print: false,
printPreferences: false,
share: true,
}"
@deleted="$emit('delete', slug)"
/>
</v-card-actions>
</slot>
<slot />
</v-card>
</v-hover>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -116,7 +116,7 @@
@deleted="$emit('delete', slug)" @deleted="$emit('delete', slug)"
/> />
</v-card-actions> </v-card-actions>
</slot> </slot>
</v-list-item> </v-list-item>
<slot /> <slot />
</v-card> </v-card>

View File

@@ -38,6 +38,7 @@
<RecipeContextMenuContent <RecipeContextMenuContent
v-if="isMenuContentLoaded" v-if="isMenuContentLoaded"
v-bind="contentProps" v-bind="contentProps"
@print="$emit('print')"
@deleted="$emit('deleted', $event)" @deleted="$emit('deleted', $event)"
/> />
</v-menu> </v-menu>
@@ -108,6 +109,7 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits<{ defineEmits<{
[key: string]: any; [key: string]: any;
print: [];
deleted: [slug: string]; deleted: [slug: string];
}>(); }>();

View File

@@ -67,7 +67,7 @@
hide-header hide-header
:first-day-of-week="firstDayOfWeek" :first-day-of-week="firstDayOfWeek"
:local="$i18n.locale" :local="$i18n.locale"
@update:model-value="pickerMenu = false" @update:model-value="pickerMenu = false"
/> />
</v-menu> </v-menu>
<v-select <v-select

View File

@@ -1,8 +1,15 @@
<template> <template>
<div> <div>
<BaseDialog v-model="dialog" :title="$t('data-pages.manage-aliases')" :icon="$globals.icons.edit" <BaseDialog
:submit-icon="$globals.icons.check" :submit-text="$t('general.confirm')" can-submit @submit="saveAliases" v-model="dialog"
@cancel="$emit('cancel')"> :title="$t('data-pages.manage-aliases')"
:icon="$globals.icons.edit"
:submit-icon="$globals.icons.check"
:submit-text="$t('general.confirm')"
can-submit
@submit="saveAliases"
@cancel="$emit('cancel')"
>
<v-card-text> <v-card-text>
<v-container> <v-container>
<v-row v-for="alias, i in aliases" :key="i"> <v-row v-for="alias, i in aliases" :key="i">
@@ -10,13 +17,16 @@
<v-text-field v-model="alias.name" :label="$t('general.name')" :rules="[validators.required]" /> <v-text-field v-model="alias.name" :label="$t('general.name')" :rules="[validators.required]" />
</v-col> </v-col>
<v-col cols="2"> <v-col cols="2">
<BaseButtonGroup :buttons="[ <BaseButtonGroup
{ :buttons="[
icon: $globals.icons.delete, {
text: $t('general.delete'), icon: $globals.icons.delete,
event: 'delete', text: $t('general.delete'),
}, event: 'delete',
]" @delete="deleteAlias(i)" /> },
]"
@delete="deleteAlias(i)"
/>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>

View File

@@ -31,7 +31,7 @@
:placeholder="$t('recipe.quantity')" :placeholder="$t('recipe.quantity')"
@keypress="quantityFilter" @keypress="quantityFilter"
> >
<template #prepend> <template v-if="enableDragHandle" #prepend>
<v-icon <v-icon
class="mr-n1 handle" class="mr-n1 handle"
> >
@@ -59,6 +59,7 @@
class="mx-1" class="mx-1"
:placeholder="$t('recipe.choose-unit')" :placeholder="$t('recipe.choose-unit')"
clearable clearable
:menu-props="{ attach: props.menuAttachTarget, maxHeight: '250px' }"
@keyup.enter="handleUnitEnter" @keyup.enter="handleUnitEnter"
> >
<template #prepend> <template #prepend>
@@ -115,6 +116,7 @@
class="mx-1 py-0" class="mx-1 py-0"
:placeholder="$t('recipe.choose-food')" :placeholder="$t('recipe.choose-food')"
clearable clearable
:menu-props="{ attach: props.menuAttachTarget, maxHeight: '250px' }"
@keyup.enter="handleFoodEnter" @keyup.enter="handleFoodEnter"
> >
<template #prepend> <template #prepend>
@@ -165,12 +167,12 @@
@click="$emit('clickIngredientField', 'note')" @click="$emit('clickIngredientField', 'note')"
/> />
<BaseButtonGroup <BaseButtonGroup
v-if="enableContextMenu"
hover hover
:large="false" :large="false"
class="my-auto d-flex" class="my-auto d-flex"
:buttons="btns" :buttons="btns"
@toggle-section="toggleTitle" @toggle-section="toggleTitle"
@toggle-original="toggleOriginalText"
@insert-above="$emit('insert-above')" @insert-above="$emit('insert-above')"
@insert-below="$emit('insert-below')" @insert-below="$emit('insert-below')"
@delete="$emit('delete')" @delete="$emit('delete')"
@@ -178,13 +180,7 @@
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
<p <slot name="before-divider" />
v-if="showOriginalText"
class="text-caption"
>
{{ $t("recipe.original-text-with-value", { originalText: model.originalText }) }}
</p>
<v-divider <v-divider
v-if="!mdAndUp" v-if="!mdAndUp"
class="my-4" class="my-4"
@@ -203,7 +199,11 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
// defineModel replaces modelValue prop // defineModel replaces modelValue prop
const model = defineModel<RecipeIngredient>({ required: true }); const model = defineModel<RecipeIngredient>({ required: true });
defineProps({ const props = defineProps({
menuAttachTarget: {
type: String,
default: "body",
},
unitError: { unitError: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -220,6 +220,18 @@ defineProps({
type: String, type: String,
default: "", default: "",
}, },
enableContextMenu: {
type: Boolean,
default: false,
},
enableDragHandle: {
type: Boolean,
default: false,
},
deleteDisabled: {
type: Boolean,
default: false,
},
}); });
defineEmits([ defineEmits([
@@ -235,7 +247,6 @@ const { $globals } = useNuxtApp();
const state = reactive({ const state = reactive({
showTitle: false, showTitle: false,
showOriginalText: false,
}); });
const contextMenuOptions = computed(() => { const contextMenuOptions = computed(() => {
@@ -254,13 +265,6 @@ const contextMenuOptions = computed(() => {
}, },
]; ];
if (model.value.originalText) {
options.push({
text: i18n.t("recipe.see-original-text"),
event: "toggle-original",
});
}
return options; return options;
}); });
@@ -281,8 +285,8 @@ const btns = computed(() => {
text: i18n.t("general.delete"), text: i18n.t("general.delete"),
event: "delete", event: "delete",
children: undefined, children: undefined,
disabled: props.deleteDisabled,
}); });
return out; return out;
}); });
@@ -319,10 +323,6 @@ function toggleTitle() {
state.showTitle = !state.showTitle; state.showTitle = !state.showTitle;
} }
function toggleOriginalText() {
state.showOriginalText = !state.showOriginalText;
}
function handleUnitEnter() { function handleUnitEnter() {
if ( if (
model.value.unit === undefined model.value.unit === undefined
@@ -349,7 +349,7 @@ function quantityFilter(e: KeyboardEvent) {
} }
} }
const { showTitle, showOriginalText } = toRefs(state); const { showTitle } = toRefs(state);
const foods = foodStore.store; const foods = foodStore.store;
const units = unitStore.store; const units = unitStore.store;

View File

@@ -1,15 +1,44 @@
<template> <template>
<!-- eslint-disable-next-line vue/no-v-html --> <div class="ingredient-link-label links-disabled">
<div v-html="safeMarkup" /> <SafeMarkdown v-if="baseText" :source="baseText" />
<SafeMarkdown
v-if="ingredient?.note"
class="d-inline"
:source="` ${ingredient.note}`"
/>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients"; import { computed } from "vue";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { useParsedIngredientText } from "~/composables/recipes";
interface Props { interface Props {
markup: string; ingredient?: RecipeIngredient;
scale?: number;
} }
const props = defineProps<Props>();
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup)); const { ingredient, scale = 1 } = defineProps<Props>();
const baseText = computed(() => {
if (!ingredient) return "";
const parsed = useParsedIngredientText(ingredient, scale);
return [parsed.quantity, parsed.unit, parsed.name].filter(Boolean).join(" ").trim();
});
</script> </script>
<style scoped>
.ingredient-link-label {
display: block;
line-height: 1.25;
word-break: break-word;
font-size: 0.95rem;
}
.links-disabled :deep(a) {
pointer-events: none;
cursor: default;
color: var(--v-theme-primary);
text-decoration: none;
}
</style>

View File

@@ -1,5 +1,12 @@
<template> <template>
<div> <div>
<RecipePageParseDialog
:model-value="isParsing"
:ingredients="recipe.recipeIngredient"
:width="$vuetify.display.smAndDown ? '100%' : '80%'"
@update:model-value="toggleIsParsing"
@save="saveParsedIngredients"
/>
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown }"> <v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown }">
<v-card :flat="$vuetify.display.smAndDown" class="d-print-none"> <v-card :flat="$vuetify.display.smAndDown" class="d-print-none">
<RecipePageHeader <RecipePageHeader
@@ -106,9 +113,13 @@
/> />
<v-divider /> <v-divider />
</v-col> </v-col>
<v-col class="overflow-y-auto" <v-col
:class="$vuetify.display.smAndDown ? 'py-2': 'py-6'" class="overflow-y-auto"
style="height: 100%" cols="12" sm="7"> :class="$vuetify.display.smAndDown ? 'py-2': 'py-6'"
style="height: 100%"
cols="12"
sm="7"
>
<h2 class="text-h5 px-4 font-weight-medium opacity-80"> <h2 class="text-h5 px-4 font-weight-medium opacity-80">
{{ $t('recipe.instructions') }} {{ $t('recipe.instructions') }}
</h2> </h2>
@@ -168,6 +179,7 @@ import RecipePageIngredientEditor from "./RecipePageParts/RecipePageIngredientEd
import RecipePageIngredientToolsView from "./RecipePageParts/RecipePageIngredientToolsView.vue"; import RecipePageIngredientToolsView from "./RecipePageParts/RecipePageIngredientToolsView.vue";
import RecipePageInstructions from "./RecipePageParts/RecipePageInstructions.vue"; import RecipePageInstructions from "./RecipePageParts/RecipePageInstructions.vue";
import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue"; import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue";
import RecipePageParseDialog from "./RecipePageParts/RecipePageParseDialog.vue";
import RecipePageScale from "./RecipePageParts/RecipePageScale.vue"; import RecipePageScale from "./RecipePageParts/RecipePageScale.vue";
import RecipePageInfoEditor from "./RecipePageParts/RecipePageInfoEditor.vue"; import RecipePageInfoEditor from "./RecipePageParts/RecipePageInfoEditor.vue";
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue"; import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
@@ -178,12 +190,13 @@ import {
usePageState, usePageState,
} from "~/composables/recipe-page/shared-state"; } from "~/composables/recipe-page/shared-state";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; import type { Recipe, RecipeCategory, RecipeIngredient, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
import { useRouteQuery } from "~/composables/use-router"; import { useRouteQuery } from "~/composables/use-router";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { uuid4, deepCopy } from "~/composables/use-utils"; import { uuid4, deepCopy } from "~/composables/use-utils";
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue"; import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue"; import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useNavigationWarning } from "~/composables/use-navigation-warning"; import { useNavigationWarning } from "~/composables/use-navigation-warning";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true }); const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
@@ -192,12 +205,13 @@ const display = useDisplay();
const i18n = useI18n(); const i18n = useI18n();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const route = useRoute(); const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || ""); const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
const router = useRouter(); const router = useRouter();
const api = useUserApi(); const api = useUserApi();
const { setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode } const { setMode, isEditForm, isEditJSON, isCookMode, isEditMode, isParsing, toggleCookMode, toggleIsParsing }
= usePageState(recipe.value.slug); = usePageState(recipe.value.slug);
const { deactivateNavigationWarning } = useNavigationWarning(); const { deactivateNavigationWarning } = useNavigationWarning();
const notLinkedIngredients = computed(() => { const notLinkedIngredients = computed(() => {
@@ -246,12 +260,29 @@ const hasLinkedIngredients = computed(() => {
type BooleanString = "true" | "false" | ""; type BooleanString = "true" | "false" | "";
const edit = useRouteQuery<BooleanString>("edit", ""); const paramsEdit = useRouteQuery<BooleanString>("edit", "");
const paramsParse = useRouteQuery<BooleanString>("parse", "");
onMounted(() => { onMounted(() => {
if (edit.value === "true") { if (paramsEdit.value === "true" && isOwnGroup.value) {
setMode(PageMode.EDIT); setMode(PageMode.EDIT);
} }
if (paramsParse.value === "true" && isOwnGroup.value) {
toggleIsParsing(true);
}
});
watch(isEditMode, (newVal) => {
if (!newVal) {
paramsEdit.value = undefined;
}
});
watch(isParsing, () => {
if (!isParsing.value) {
paramsParse.value = undefined;
}
}); });
/** ============================================================= /** =============================================================
@@ -266,6 +297,12 @@ async function saveRecipe() {
} }
} }
async function saveParsedIngredients(ingredients: NoUndefinedField<RecipeIngredient[]>) {
recipe.value.recipeIngredient = ingredients;
await saveRecipe();
toggleIsParsing(false);
}
async function deleteRecipe() { async function deleteRecipe() {
const { data } = await api.recipes.deleteOne(recipe.value.slug); const { data } = await api.recipes.deleteOne(recipe.value.slug);
if (data?.slug) { if (data?.slug) {
@@ -302,7 +339,7 @@ function addStep(steps: Array<string> | null = null) {
if (steps) { if (steps) {
const cleanedSteps = steps.map((step) => { const cleanedSteps = steps.map((step) => {
return { id: uuid4(), text: step, title: "", ingredientReferences: [] }; return { id: uuid4(), text: step, title: "", summary: "", ingredientReferences: [] };
}); });
recipe.value.recipeInstructions.push(...cleanedSteps); recipe.value.recipeInstructions.push(...cleanedSteps);

View File

@@ -13,25 +13,25 @@
@upload="uploadImage" @upload="uploadImage"
/> />
<v-spacer /> <v-spacer />
<v-select <v-select
v-model="recipe.userId" v-model="recipe.userId"
class="my-2" class="my-2"
max-width="300" max-width="300"
:items="allUsers" :items="allUsers"
:item-props="itemsProps" :item-props="itemsProps"
:label="$t('general.owner')" :label="$t('general.owner')"
:disabled="!canEditOwner" :disabled="!canEditOwner"
variant="outlined" variant="outlined"
density="compact" density="compact"
> >
<template #prepend> <template #prepend>
<UserAvatar <UserAvatar
:user-id="recipe.userId" :user-id="recipe.userId"
:tooltip="false" :tooltip="false"
/> />
</template> </template>
</v-select> </v-select>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -29,8 +29,8 @@
class="mb-2 mx-n2" class="mb-2 mx-n2"
> >
<v-card-title class="text-h5 font-weight-medium opacity-80"> <v-card-title class="text-h5 font-weight-medium opacity-80">
{{ $t('recipe.api-extras') }} {{ $t('recipe.api-extras') }}
</v-card-title> </v-card-title>
<v-divider class="ml-4" /> <v-divider class="ml-4" />
<v-card-text> <v-card-text>
{{ $t('recipe.api-extras-description') }} {{ $t('recipe.api-extras-description') }}

View File

@@ -12,10 +12,10 @@
> >
<v-card-text class="w-100"> <v-card-text class="w-100">
<div class="d-flex flex-column align-center"> <div class="d-flex flex-column align-center">
<v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80"> <v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80">
{{ recipe.name }} {{ recipe.name }}
</v-card-title> </v-card-title>
<RecipeRating <RecipeRating
:key="recipe.slug" :key="recipe.slug"
:value="recipe.rating" :value="recipe.rating"
:recipe-id="recipe.id" :recipe-id="recipe.id"

View File

@@ -1,4 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<div> <div>
<div class="mb-4"> <div class="mb-4">
@@ -31,6 +30,8 @@
v-for="(ingredient, index) in recipe.recipeIngredient" v-for="(ingredient, index) in recipe.recipeIngredient"
:key="ingredient.referenceId" :key="ingredient.referenceId"
v-model="recipe.recipeIngredient[index]" v-model="recipe.recipeIngredient[index]"
enable-drag-handle
enable-context-menu
class="list-group-item" class="list-group-item"
@delete="recipe.recipeIngredient.splice(index, 1)" @delete="recipe.recipeIngredient.splice(index, 1)"
@insert-above="insertNewIngredient(index)" @insert-above="insertNewIngredient(index)"
@@ -55,8 +56,8 @@
class="mb-1" class="mb-1"
:disabled="hasFoodOrUnit" :disabled="hasFoodOrUnit"
color="accent" color="accent"
:to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`"
v-bind="props" v-bind="props"
@click="toggleIsParsing(true)"
> >
<template #icon> <template #icon>
{{ $globals.icons.foods }} {{ $globals.icons.foods }}
@@ -87,16 +88,14 @@ import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue"; import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue"; import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
import { usePageState } from "~/composables/recipe-page/shared-state";
import { uuid4 } from "~/composables/use-utils"; import { uuid4 } from "~/composables/use-utils";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true }); const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
const i18n = useI18n(); const i18n = useI18n();
const $auth = useMealieAuth();
const drag = ref(false); const drag = ref(false);
const { toggleIsParsing } = usePageState(recipe.value.slug);
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const hasFoodOrUnit = computed(() => { const hasFoodOrUnit = computed(() => {
if (!recipe.value) { if (!recipe.value) {
@@ -128,7 +127,7 @@ function addIngredient(ingredients: Array<string> | null = null) {
note: x, note: x,
unit: undefined, unit: undefined,
food: undefined, food: undefined,
quantity: 1, quantity: 0,
}; };
}); });
@@ -146,7 +145,7 @@ function addIngredient(ingredients: Array<string> | null = null) {
unit: undefined, unit: undefined,
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set // @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
food: undefined, food: undefined,
quantity: 1, quantity: 0,
}); });
} }
} }
@@ -160,7 +159,7 @@ function insertNewIngredient(dest: number) {
unit: undefined, unit: undefined,
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set // @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
food: undefined, food: undefined,
quantity: 1, quantity: 0,
}); });
} }
</script> </script>

View File

@@ -34,21 +34,21 @@
{{ $t("recipe.unlinked") }} {{ $t("recipe.unlinked") }}
</h4> </h4>
<template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title"> <template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title">
<h4 v-if="title" class="py-3 ml-1 pl-4"> <h4 v-if="title" class="py-3 ml-1 pl-4">
{{ title }} {{ title }}
</h4> </h4>
<v-checkbox-btn <v-checkbox-btn
v-for="ing in ingredients" v-for="ing in ingredients"
:key="ing.referenceId" :key="ing.referenceId"
v-model="activeRefs" v-model="activeRefs"
:value="ing.referenceId" :value="ing.referenceId"
class="ml-4" class="ml-4"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing)" /> <RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
</template> </template>
<template v-if="Object.keys(groupedUsedIngredients).length > 0"> <template v-if="Object.keys(groupedUsedIngredients).length > 0">
@@ -67,7 +67,7 @@
class="ml-4" class="ml-4"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing)" /> <RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template> </template>
</v-checkbox-btn> </v-checkbox-btn>
</template> </template>
@@ -184,17 +184,17 @@
<v-hover v-slot="{ isHovering }"> <v-hover v-slot="{ isHovering }">
<v-card <v-card
class="my-3" class="my-3"
:class="[{ 'on-hover': isHovering }, isChecked(index)]" :class="[{ 'on-hover': isHovering }, { 'cursor-default': isEditForm }, isChecked(index)]"
:elevation="isHovering ? 12 : 2" :elevation="isHovering ? 12 : 2"
:ripple="false" :ripple="false"
@click="toggleDisabled(index)" @click="toggleDisabled(index)"
> >
<v-card-title :class="{ 'pb-0': !isChecked(index) }"> <v-card-title class="recipe-step-title pt-3" :class="!isChecked(index) ? 'pb-0' : 'pb-3'">
<div class="d-flex align-center"> <div class="d-flex align-center w-100">
<v-text-field <v-text-field
v-if="isEditForm" v-if="isEditForm"
v-model="step.summary" v-model="step.summary"
class="headline handle" class="headline"
hide-details hide-details
density="compact" density="compact"
variant="solo" variant="solo"
@@ -202,14 +202,27 @@
:placeholder="$t('recipe.step-index', { step: index + 1 })" :placeholder="$t('recipe.step-index', { step: index + 1 })"
> >
<template #prepend> <template #prepend>
<v-icon size="26"> <v-icon size="26" class="handle">
{{ $globals.icons.arrowUpDown }} {{ $globals.icons.arrowUpDown }}
</v-icon> </v-icon>
</template> </template>
</v-text-field> </v-text-field>
<span v-else> <div
{{ step.summary ? step.summary : $t("recipe.step-index", { step: index + 1 }) }} v-else
</span> class="summary-wrapper"
>
<template v-if="step.summary">
<SafeMarkdown
class="pr-2"
:source="step.summary"
/>
</template>
<template v-else>
<span>
{{ $t('recipe.step-index', { step: index + 1 }) }}
</span>
</template>
</div>
<template v-if="isEditForm"> <template v-if="isEditForm">
<div class="ml-auto"> <div class="ml-auto">
<BaseButtonGroup <BaseButtonGroup
@@ -314,11 +327,22 @@
persistentHint: true, persistentHint: true,
}" }"
/> />
<RecipeIngredientHtml <div
v-for="ing in step.ingredientReferences" v-if="step.ingredientReferences && step.ingredientReferences.length"
:key="ing.referenceId!" class="linked-ingredients-editor"
:markup="getIngredientByRefId(ing.referenceId!)" >
/> <div
v-for="(linkRef, i) in step.ingredientReferences"
:key="linkRef.referenceId ?? i"
class="mb-1"
>
<RecipeIngredientHtml
v-if="linkRef.referenceId && ingredientLookup[linkRef.referenceId]"
:ingredient="ingredientLookup[linkRef.referenceId]"
:scale="scale"
/>
</div>
</div>
</v-card-text> </v-card-text>
</DropZone> </DropZone>
<v-expand-transition> <v-expand-transition>
@@ -373,9 +397,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { VueDraggable } from "vue-draggable-plus"; import { VueDraggable } from "vue-draggable-plus";
import { computed, nextTick, onMounted, ref, watch } from "vue"; import { computed, nextTick, onMounted, ref, watch } from "vue";
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe"; import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
import { parseIngredientText } from "~/composables/recipes";
import { uuid4 } from "~/composables/use-utils"; import { uuid4 } from "~/composables/use-utils";
import { useUserApi, useStaticRoutes } from "~/composables/api"; import { useUserApi, useStaticRoutes } from "~/composables/api";
import { usePageState } from "~/composables/recipe-page/shared-state"; import { usePageState } from "~/composables/recipe-page/shared-state";
@@ -383,6 +405,7 @@ import { useExtractIngredientReferences } from "~/composables/recipe-page/use-ex
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import DropZone from "~/components/global/DropZone.vue"; import DropZone from "~/components/global/DropZone.vue";
import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue"; import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue";
import RecipeIngredientHtml from "~/components/Domain/Recipe/RecipeIngredientHtml.vue";
interface MergerHistory { interface MergerHistory {
target: number; target: number;
@@ -500,10 +523,9 @@ function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
instructionList.value[idx].ingredientReferences = []; instructionList.value[idx].ingredientReferences = [];
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[]; refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
} }
setUsedIngredients();
activeText.value = text;
activeIndex.value = idx; activeIndex.value = idx;
activeText.value = text;
setUsedIngredients();
dialog.value = true; dialog.value = true;
activeRefs.value = refs.map(ref => ref.referenceId ?? ""); activeRefs.value = refs.map(ref => ref.referenceId ?? "");
} }
@@ -544,29 +566,26 @@ function saveAndOpenNextLinkIngredients() {
function setUsedIngredients() { function setUsedIngredients() {
const usedRefs: { [key: string]: boolean } = {}; const usedRefs: { [key: string]: boolean } = {};
instructionList.value.forEach((element) => { instructionList.value.forEach((element, idx) => {
if (idx === activeIndex.value) return;
element.ingredientReferences?.forEach((ref) => { element.ingredientReferences?.forEach((ref) => {
if (ref.referenceId !== undefined) { if (ref.referenceId) usedRefs[ref.referenceId] = true;
usedRefs[ref.referenceId!] = true;
}
}); });
}); });
usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => { usedIngredients.value = props.recipe.recipeIngredient.filter(ing => !!ing.referenceId && ing.referenceId in usedRefs);
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
});
unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => { unusedIngredients.value = props.recipe.recipeIngredient.filter(ing => !!ing.referenceId && !(ing.referenceId in usedRefs));
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
});
} }
watch(activeRefs, () => setUsedIngredients());
function autoSetReferences() { function autoSetReferences() {
useExtractIngredientReferences( useExtractIngredientReferences(
props.recipe.recipeIngredient, props.recipe.recipeIngredient,
activeRefs.value, activeRefs.value,
activeText.value, activeText.value,
).forEach((ingredient: string) => activeRefs.value.push(ingredient)); ).forEach(ingredient => activeRefs.value.push(ingredient));
} }
const ingredientLookup = computed(() => { const ingredientLookup = computed(() => {
@@ -603,8 +622,8 @@ const ingredientSectionTitles = computed(() => {
return titleMap; return titleMap;
}); });
const groupedUnusedIngredients = computed(() => { const groupedUnusedIngredients = computed((): Record<string, RecipeIngredient[]> => {
const groups: { [key: string]: RecipeIngredient[] } = {}; const groups: Record<string, RecipeIngredient[]> = {};
// Group ingredients by section title // Group ingredients by section title
unusedIngredients.value.forEach((ingredient) => { unusedIngredients.value.forEach((ingredient) => {
@@ -614,20 +633,14 @@ const groupedUnusedIngredients = computed(() => {
// Use the section title from the mapping, or fallback to the ingredient's own title // Use the section title from the mapping, or fallback to the ingredient's own title
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || ""; const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
(groups[title] ||= []).push(ingredient);
if (!groups[title]) {
groups[title] = [];
}
groups[title].push(ingredient);
}); });
return groups; return groups;
}); });
const groupedUsedIngredients = computed(() => { const groupedUsedIngredients = computed((): Record<string, RecipeIngredient[]> => {
const groups: { [key: string]: RecipeIngredient[] } = {}; const groups: Record<string, RecipeIngredient[]> = {};
// Group ingredients by section title
usedIngredients.value.forEach((ingredient) => { usedIngredients.value.forEach((ingredient) => {
if (ingredient.referenceId === undefined) { if (ingredient.referenceId === undefined) {
return; return;
@@ -635,26 +648,12 @@ const groupedUsedIngredients = computed(() => {
// Use the section title from the mapping, or fallback to the ingredient's own title // Use the section title from the mapping, or fallback to the ingredient's own title
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || ""; const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
(groups[title] ||= []).push(ingredient);
if (!groups[title]) {
groups[title] = [];
}
groups[title].push(ingredient);
}); });
return groups; return groups;
}); });
function getIngredientByRefId(refId: string | undefined) {
if (refId === undefined) {
return "";
}
const ing = ingredientLookup.value[refId];
if (!ing) return "";
return parseIngredientText(ing, props.scale);
}
// =============================================================== // ===============================================================
// Instruction Merger // Instruction Merger
const mergeHistory = ref<MergerHistory[]>([]); const mergeHistory = ref<MergerHistory[]>([]);
@@ -847,7 +846,21 @@ function openImageUpload(index: number) {
z-index: 1; z-index: 1;
} }
.v-text-field >>> input { .v-text-field :deep(input) {
font-size: 1.5rem; font-size: 1.5rem;
} }
.recipe-step-title {
/* Multiline display */
white-space: normal;
line-height: 1.25;
word-break: break-word;
}
.summary-wrapper {
flex: 1 1 auto;
min-width: 0; /* wrapping in flex container */
white-space: normal;
overflow-wrap: anywhere;
cursor: pointer;
}
</style> </style>

View File

@@ -0,0 +1,538 @@
<template>
<BaseDialog
:model-value="modelValue"
:title="$t('recipe.parse-ingredients')"
:icon="$globals.icons.fileSign"
disable-submit-on-enter
@update:model-value="emit('update:modelValue', $event)"
>
<v-container fluid class="pa-2 ma-0" style="background-color: rgb(var(--v-theme-background));">
<div v-if="state.loading.parser" class="my-6">
<AppLoader waiting-text="" class="my-6" />
</div>
<div v-else>
<BaseCardSectionTitle :title="$t('recipe.parser.ingredient-parser')">
<div v-if="!state.allReviewed" class="mb-4">
<p>{{ $t("recipe.parser.ingredient-parser-description") }}</p>
<p>{{ $t("recipe.parser.ingredient-parser-final-review-description") }}</p>
</div>
<div class="d-flex flex-wrap align-center">
<div class="text-body-2 mr-2">
{{ $t("recipe.parser.select-parser") }}
</div>
<div class="d-flex align-center">
<BaseOverflowButton
v-model="parser"
:disabled="state.loading.parser"
btn-class="mx-2"
:items="availableParsers"
/>
<v-btn
icon
size="40"
color="info"
:disabled="state.loading.parser"
@click="parseIngredients"
>
<v-icon>{{ $globals.icons.refresh }}</v-icon>
</v-btn>
</div>
</div>
</BaseCardSectionTitle>
<v-card v-if="!state.allReviewed && currentIng">
<v-card-text class="pb-0 mb-0">
<div class="text-center px-8 py-4 mb-6">
<p class="text-h5 font-italic">
{{ currentIng.input }}
</p>
</div>
<div class="d-flex align-center pa-0 ma-0">
<v-icon
:color="(currentIng.confidence?.average || 0) < confidenceThreshold ? 'error' : 'success'"
>
{{ (currentIng.confidence?.average || 0) < confidenceThreshold ? $globals.icons.alert : $globals.icons.check }}
</v-icon>
<span
class="ml-2"
:color="currentIngHasError ? 'error-text' : 'success-text'"
>
{{ $t("recipe.parser.confidence-score") }}: {{ currentIng.confidence ? asPercentage(currentIng.confidence?.average!) : "" }}
</span>
</div>
<RecipeIngredientEditor
v-model="currentIng.ingredient"
:unit-error="!!currentMissingUnit"
:unit-error-tooltip="$t('recipe.parser.this-unit-could-not-be-parsed-automatically')"
:food-error="!!currentMissingFood"
:food-error-tooltip="$t('recipe.parser.this-food-could-not-be-parsed-automatically')"
/>
<v-card-actions>
<v-spacer />
<BaseButton
v-if="currentMissingUnit && !currentIng.ingredient.unit?.id"
color="warning"
size="small"
@click="createMissingUnit"
>
{{ i18n.t("recipe.parser.missing-unit", { unit: currentMissingUnit }) }}
</BaseButton>
<BaseButton
v-if="
currentMissingUnit
&& currentIng.ingredient.unit?.id
&& currentMissingUnit.toLowerCase() != currentIng.ingredient.unit?.name.toLowerCase()
"
color="warning"
size="small"
@click="addMissingUnitAsAlias"
>
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingUnit, item: currentIng.ingredient.unit.name }) }}
</BaseButton>
<BaseButton
v-if="currentMissingFood && !currentIng.ingredient.food?.id"
color="warning"
size="small"
@click="createMissingFood"
>
{{ i18n.t("recipe.parser.missing-food", { food: currentMissingFood }) }}
</BaseButton>
<BaseButton
v-if="
currentMissingFood
&& currentIng.ingredient.food?.id
&& currentMissingFood.toLowerCase() != currentIng.ingredient.food?.name.toLowerCase()
"
color="warning"
size="small"
@click="addMissingFoodAsAlias"
>
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingFood, item: currentIng.ingredient.food.name }) }}
</BaseButton>
</v-card-actions>
</v-card-text>
</v-card>
<div v-else>
<v-card-title class="text-center pt-0 pb-8">
{{ $t("recipe.parser.review-parsed-ingredients") }}
</v-card-title>
<v-card-text style="max-height: 60vh; overflow-y: auto;">
<VueDraggable
v-model="parsedIngs"
handle=".handle"
:delay="250"
:delay-on-touch-only="true"
v-bind="{
animation: 200,
group: 'recipe-ingredients',
disabled: false,
ghostClass: 'ghost',
}"
class="px-6"
@start="drag = true"
@end="drag = false"
>
<TransitionGroup
type="transition"
>
<v-lazy v-for="(ingredient, index) in parsedIngs" :key="index">
<RecipeIngredientEditor
v-model="ingredient.ingredient"
enable-drag-handle
enable-context-menu
class="list-group-item pb-8"
:delete-disabled="parsedIngs.length <= 1"
@delete="parsedIngs.splice(index, 1)"
@insert-above="insertNewIngredient(index)"
@insert-below="insertNewIngredient(index + 1)"
>
<template #before-divider>
<p v-if="ingredient.input" class="py-0 my-0 text-caption">
{{ $t("recipe.original-text-with-value", { originalText: ingredient.input }) }}
</p>
</template>
</RecipeIngredientEditor>
</v-lazy>
</TransitionGroup>
</VueDraggable>
</v-card-text>
</div>
</div>
</v-container>
<template v-if="!state.loading.parser" #custom-card-action>
<!-- Parse -->
<div v-if="!state.allReviewed" class="d-flex justify-space-between align-center">
<v-checkbox
v-model="currentIngShouldDelete"
color="error"
hide-details
density="compact"
:label="i18n.t('recipe.parser.delete-item')"
class="mr-4"
/>
<BaseButton
:color="currentIngShouldDelete ? 'error' : 'info'"
:icon="currentIngShouldDelete ? $globals.icons.delete : $globals.icons.arrowRightBold"
:icon-right="!currentIngShouldDelete"
:text="$t(currentIngShouldDelete ? 'recipe.parser.delete-item' : 'general.next')"
@click="nextIngredient"
/>
</div>
<!-- Review -->
<div v-else>
<BaseButton
create
:text="$t('general.save')"
:icon="$globals.icons.save"
:loading="state.loading.save"
@click="saveIngs"
/>
</div>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
import { VueDraggable } from "vue-draggable-plus";
import type { IngredientFood, IngredientUnit, ParsedIngredient, RecipeIngredient } from "~/lib/api/types/recipe";
import type { Parser } from "~/lib/api/user/recipes/recipe";
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import { useAppInfo, useUserApi } from "~/composables/api";
import { parseIngredientText } from "~/composables/recipes";
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
import { useGlobalI18n } from "~/composables/use-global-i18n";
import { alert } from "~/composables/use-toast";
import { useParsingPreferences } from "~/composables/use-users/preferences";
const props = defineProps<{
modelValue: boolean;
ingredients: NoUndefinedField<RecipeIngredient[]>;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
}>();
const i18n = useGlobalI18n();
const api = useUserApi();
const appInfo = useAppInfo();
const drag = ref(false);
const unitStore = useUnitStore();
const unitData = useUnitData();
const foodStore = useFoodStore();
const foodData = useFoodData();
const parserPreferences = useParsingPreferences();
const parser = ref<Parser>(parserPreferences.value.parser || "nlp");
const availableParsers = computed(() => {
return [
{
text: i18n.t("recipe.parser.natural-language-processor"),
value: "nlp",
},
{
text: i18n.t("recipe.parser.brute-parser"),
value: "brute",
},
{
text: i18n.t("recipe.parser.openai-parser"),
value: "openai",
hide: !appInfo.value?.enableOpenai,
},
];
});
/**
* If confidence of parsing is below this threshold,
* we will prompt the user to review the parsed ingredient.
*/
const confidenceThreshold = 0.85;
const parsedIngs = ref<ParsedIngredient[]>([]);
const currentIng = ref<ParsedIngredient | null>(null);
const currentMissingUnit = ref("");
const currentMissingFood = ref("");
const currentIngHasError = computed(() => currentMissingUnit.value || currentMissingFood.value);
const currentIngShouldDelete = ref(false);
const state = reactive({
currentParsedIndex: -1,
allReviewed: false,
loading: {
parser: false,
save: false,
},
});
function shouldReview(ing: ParsedIngredient): boolean {
console.debug(`Checking if ingredient needs review (input="${ing.input})":`, ing);
if ((ing.confidence?.average || 0) < confidenceThreshold) {
console.debug("Needs review due to low confidence:", ing.confidence?.average);
return true;
}
const food = ing.ingredient.food;
if (food && !food.id) {
console.debug("Needs review due to missing food ID:", food);
return true;
}
const unit = ing.ingredient.unit;
if (unit && !unit.id) {
console.debug("Needs review due to missing unit ID:", unit);
return true;
}
console.debug("No review needed");
return false;
}
function checkUnit(ing: ParsedIngredient) {
const unit = ing.ingredient.unit?.name;
if (!unit || ing.ingredient.unit?.id) {
currentMissingUnit.value = "";
return;
}
const potentialMatch = createdUnits.get(unit.toLowerCase());
if (potentialMatch) {
ing.ingredient.unit = potentialMatch;
currentMissingUnit.value = "";
return;
}
currentMissingUnit.value = unit;
ing.ingredient.unit = undefined;
}
function checkFood(ing: ParsedIngredient) {
const food = ing.ingredient.food?.name;
if (!food || ing.ingredient.food?.id) {
currentMissingFood.value = "";
return;
}
const potentialMatch = createdFoods.get(food.toLowerCase());
if (potentialMatch) {
ing.ingredient.food = potentialMatch;
currentMissingFood.value = "";
return;
}
currentMissingFood.value = food;
ing.ingredient.food = undefined;
}
function nextIngredient() {
let nextIndex = state.currentParsedIndex;
if (currentIngShouldDelete.value) {
parsedIngs.value.splice(state.currentParsedIndex, 1);
currentIngShouldDelete.value = false;
}
else {
nextIndex += 1;
}
while (nextIndex < parsedIngs.value.length) {
const current = parsedIngs.value[nextIndex];
if (shouldReview(current)) {
state.currentParsedIndex = nextIndex;
currentIng.value = current;
currentIngShouldDelete.value = false;
checkUnit(current);
checkFood(current);
return;
}
nextIndex += 1;
}
// No more to review
state.allReviewed = true;
}
async function parseIngredients() {
if (state.loading.parser) {
return;
}
if (!props.ingredients || props.ingredients.length === 0) {
state.loading.parser = false;
return;
}
state.loading.parser = true;
try {
const ingsAsString = props.ingredients.map(ing => parseIngredientText(ing, 1, false) ?? "");
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
if (error || !data) {
throw new Error("Failed to parse ingredients");
}
parsedIngs.value = data;
state.currentParsedIndex = -1;
state.allReviewed = false;
createdUnits.clear();
createdFoods.clear();
currentIngShouldDelete.value = false;
nextIngredient();
}
catch (error) {
console.error("Error parsing ingredients:", error);
alert.error(i18n.t("events.something-went-wrong"));
}
finally {
state.loading.parser = false;
}
}
/** Cache of lowercased created units to avoid duplicate creations */
const createdUnits = new Map<string, IngredientUnit>();
/** Cache of lowercased created foods to avoid duplicate creations */
const createdFoods = new Map<string, IngredientFood>();
async function createMissingUnit() {
if (!currentMissingUnit.value) {
return;
}
unitData.reset();
unitData.data.name = currentMissingUnit.value;
let newUnit: IngredientUnit | null = null;
if (createdUnits.has(unitData.data.name)) {
newUnit = createdUnits.get(unitData.data.name)!;
}
else {
newUnit = await unitStore.actions.createOne(unitData.data);
}
if (!newUnit) {
alert.error(i18n.t("events.something-went-wrong"));
return;
}
currentIng.value!.ingredient.unit = newUnit;
createdUnits.set(newUnit.name.toLowerCase(), newUnit);
currentMissingUnit.value = "";
}
async function createMissingFood() {
if (!currentMissingFood.value) {
return;
}
foodData.reset();
foodData.data.name = currentMissingFood.value;
let newFood: IngredientFood | null = null;
if (createdFoods.has(foodData.data.name)) {
newFood = createdFoods.get(foodData.data.name)!;
}
else {
newFood = await foodStore.actions.createOne(foodData.data);
}
if (!newFood) {
alert.error(i18n.t("events.something-went-wrong"));
return;
}
currentIng.value!.ingredient.food = newFood;
createdFoods.set(newFood.name.toLowerCase(), newFood);
currentMissingFood.value = "";
}
async function addMissingUnitAsAlias() {
const unit = currentIng.value?.ingredient.unit as IngredientUnit | undefined;
if (!currentMissingUnit.value || !unit?.id) {
return;
}
unit.aliases = unit.aliases || [];
if (unit.aliases.map(a => a.name).includes(currentMissingUnit.value)) {
return;
}
unit.aliases.push({ name: currentMissingUnit.value });
const updated = await unitStore.actions.updateOne(unit);
if (!updated) {
alert.error(i18n.t("events.something-went-wrong"));
return;
}
currentIng.value!.ingredient.unit = updated;
currentMissingUnit.value = "";
}
async function addMissingFoodAsAlias() {
const food = currentIng.value?.ingredient.food as IngredientFood | undefined;
if (!currentMissingFood.value || !food?.id) {
return;
}
food.aliases = food.aliases || [];
if (food.aliases.map(a => a.name).includes(currentMissingFood.value)) {
return;
}
food.aliases.push({ name: currentMissingFood.value });
const updated = await foodStore.actions.updateOne(food);
if (!updated) {
alert.error(i18n.t("events.something-went-wrong"));
return;
}
currentIng.value!.ingredient.food = updated;
currentMissingFood.value = "";
}
watch(() => props.modelValue, () => {
if (!props.modelValue) {
return;
}
parseIngredients();
});
watch(parser, () => {
parserPreferences.value.parser = parser.value;
parseIngredients();
});
watch([parsedIngs, () => state.allReviewed], () => {
if (!state.allReviewed) {
return;
}
if (!parsedIngs.value.length) {
insertNewIngredient(0);
}
}, { immediate: true, deep: true });
function asPercentage(num: number | undefined): string {
if (!num) {
return "0%";
}
return Math.round(num * 100).toFixed(2) + "%";
}
function insertNewIngredient(index: number) {
const ing = {
input: "",
confidence: {},
ingredient: {
quantity: 0,
referenceId: uuid4(),
},
} as ParsedIngredient;
parsedIngs.value.splice(index, 0, ing);
}
function saveIngs() {
emit("save", parsedIngs.value.map(x => x.ingredient as NoUndefinedField<RecipeIngredient>));
state.loading.save = true;
}
</script>

View File

@@ -4,20 +4,23 @@
<section> <section>
<v-container class="ma-0 pa-0"> <v-container class="ma-0 pa-0">
<v-row> <v-row>
<v-col v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden" <v-col
:order="preferences.imagePosition == ImagePosition.left ? -1 : 1" v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden"
cols="4" :order="preferences.imagePosition == ImagePosition.left ? -1 : 1"
align-self="center" cols="4"
align-self="center"
> >
<img :key="imageKey" <img
:src="recipeImageUrl" :key="imageKey"
style="min-height: 50; max-width: 100%;" :src="recipeImageUrl"
style="min-height: 50; max-width: 100%;"
> >
</v-col> </v-col>
<v-col order="0"> <v-col order="0">
<v-card-title class="headline pl-0"> <v-card-title class="headline pl-0">
<v-icon start <v-icon
color="primary" start
color="primary"
> >
{{ $globals.icons.primary }} {{ $globals.icons.primary }}
</v-icon> </v-icon>
@@ -36,17 +39,19 @@
</div> </div>
</div> </div>
<v-row class="d-flex justify-start"> <v-row class="d-flex justify-start">
<RecipeTimeCard :prep-time="recipe.prepTime" <RecipeTimeCard
:total-time="recipe.totalTime" :prep-time="recipe.prepTime"
:perform-time="recipe.performTime" :total-time="recipe.totalTime"
small :perform-time="recipe.performTime"
color="white" small
class="ml-4" color="white"
class="ml-4"
/> />
</v-row> </v-row>
<v-card-text v-if="preferences.showDescription" <v-card-text
class="px-0" v-if="preferences.showDescription"
class="px-0"
> >
<SafeMarkdown :source="recipe.description" /> <SafeMarkdown :source="recipe.description" />
</v-card-text> </v-card-text>
@@ -60,24 +65,29 @@
<v-card-title class="headline pl-0"> <v-card-title class="headline pl-0">
{{ $t("recipe.ingredients") }} {{ $t("recipe.ingredients") }}
</v-card-title> </v-card-title>
<div v-for="(ingredientSection, sectionIndex) in ingredientSections" <div
:key="`ingredient-section-${sectionIndex}`" v-for="(ingredientSection, sectionIndex) in ingredientSections"
class="print-section" :key="`ingredient-section-${sectionIndex}`"
class="print-section"
> >
<h4 v-if="ingredientSection.ingredients[0].title" <h4
class="ingredient-title mt-2" v-if="ingredientSection.ingredients[0].title"
class="ingredient-title mt-2"
> >
{{ ingredientSection.ingredients[0].title }} {{ ingredientSection.ingredients[0].title }}
</h4> </h4>
<div class="ingredient-grid" <div
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }" class="ingredient-grid"
:style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
> >
<template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients" <template
:key="`ingredient-${ingredientIndex}`" v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients"
:key="`ingredient-${ingredientIndex}`"
> >
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<p class="ingredient-body" <p
v-html="parseText(ingredient)" class="ingredient-body"
v-html="parseText(ingredient)"
/> />
</template> </template>
</div> </div>
@@ -86,22 +96,26 @@
<!-- Instructions --> <!-- Instructions -->
<section> <section>
<div v-for="(instructionSection, sectionIndex) in instructionSections" <div
:key="`instruction-section-${sectionIndex}`" v-for="(instructionSection, sectionIndex) in instructionSections"
:class="{ 'print-section': instructionSection.sectionName }" :key="`instruction-section-${sectionIndex}`"
:class="{ 'print-section': instructionSection.sectionName }"
> >
<v-card-title v-if="!sectionIndex" <v-card-title
class="headline pl-0" v-if="!sectionIndex"
class="headline pl-0"
> >
{{ $t("recipe.instructions") }} {{ $t("recipe.instructions") }}
</v-card-title> </v-card-title>
<div v-for="(step, stepIndex) in instructionSection.instructions" <div
:key="`instruction-${stepIndex}`" v-for="(step, stepIndex) in instructionSection.instructions"
:key="`instruction-${stepIndex}`"
> >
<div class="print-section"> <div class="print-section">
<h4 v-if="step.title" <h4
:key="`instruction-title-${stepIndex}`" v-if="step.title"
class="instruction-title mb-2" :key="`instruction-title-${stepIndex}`"
class="instruction-title mb-2"
> >
{{ step.title }} {{ step.title }}
</h4> </h4>
@@ -112,8 +126,9 @@
+ 1, + 1,
}) }} }) }}
</h5> </h5>
<SafeMarkdown :source="step.text" <SafeMarkdown
class="recipe-step-body" :source="step.text"
class="recipe-step-body"
/> />
</div> </div>
</div> </div>
@@ -122,18 +137,21 @@
<!-- Notes --> <!-- Notes -->
<div v-if="preferences.showNotes"> <div v-if="preferences.showNotes">
<v-divider v-if="hasNotes" <v-divider
class="grey my-4" v-if="hasNotes"
class="grey my-4"
/> />
<section> <section>
<div v-for="(note, index) in recipe.notes" <div
:key="index + 'note'" v-for="(note, index) in recipe.notes"
:key="index + 'note'"
> >
<div class="print-section"> <div class="print-section">
<h4>{{ note.title }}</h4> <h4>{{ note.title }}</h4>
<SafeMarkdown :source="note.text" <SafeMarkdown
class="note-body" :source="note.text"
class="note-body"
/> />
</div> </div>
</div> </div>
@@ -150,8 +168,9 @@
<div class="print-section"> <div class="print-section">
<table class="nutrition-table"> <table class="nutrition-table">
<tbody> <tbody>
<tr v-for="(value, key) in recipe.nutrition" <tr
:key="key" v-for="(value, key) in recipe.nutrition"
:key="key"
> >
<template v-if="value"> <template v-if="value">
<td>{{ labels[key].label }}</td> <td>{{ labels[key].label }}</td>

View File

@@ -2,7 +2,8 @@
<div @click.prevent> <div @click.prevent>
<!-- User Rating --> <!-- User Rating -->
<v-hover v-slot="{ isHovering, props }"> <v-hover v-slot="{ isHovering, props }">
<v-rating v-if="isOwnGroup && (userRating || isHovering || !ratingsLoaded)" <v-rating
v-if="isOwnGroup && (userRating || isHovering || !ratingsLoaded)"
v-bind="props" v-bind="props"
:model-value="userRating" :model-value="userRating"
active-color="secondary" active-color="secondary"
@@ -13,10 +14,10 @@
hover hover
clearable clearable
@update:model-value="updateRating(+$event)" @update:model-value="updateRating(+$event)"
@click="updateRating"
/> />
<!-- Group Rating --> <!-- Group Rating -->
<v-rating v-else <v-rating
v-else
v-bind="props" v-bind="props"
:model-value="groupRating" :model-value="groupRating"
:half-increments="true" :half-increments="true"
@@ -83,7 +84,7 @@ export default defineNuxtComponent({
}); });
function updateRating(val?: number) { function updateRating(val?: number) {
if (!isOwnGroup.value) { if (!isOwnGroup.value || !val) {
return; return;
} }

View File

@@ -84,12 +84,12 @@
:buttons="[ :buttons="[
...(allowDelete ...(allowDelete
? [ ? [
{ {
icon: $globals.icons.delete, icon: $globals.icons.delete,
text: $t('general.delete'), text: $t('general.delete'),
event: 'delete', event: 'delete',
}, },
] ]
: []), : []),
{ {
icon: $globals.icons.close, icon: $globals.icons.close,

View File

@@ -78,7 +78,7 @@
<v-list-item-subtitle class="font-weight-medium" style="font-size: small;"> <v-list-item-subtitle class="font-weight-medium" style="font-size: small;">
{{ item.subtitle }} {{ item.subtitle }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
</div> </div>
</template> </template>
</v-list> </v-list>
@@ -100,9 +100,7 @@ import type { SideBarLink } from "~/types/application-types";
import { useAppInfo } from "~/composables/api"; import { useAppInfo } from "~/composables/api";
import { useCookbookPreferences } from "~/composables/use-users/preferences"; import { useCookbookPreferences } from "~/composables/use-users/preferences";
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store"; import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
import type { ReadCookBook } from "~/lib/api/types/cookbook"; import type { ReadCookBook } from "~/lib/api/types/cookbook";
import type { HouseholdSummary } from "~/lib/api/types/household";
export default defineNuxtComponent({ export default defineNuxtComponent({
setup() { setup() {
@@ -116,12 +114,8 @@ export default defineNuxtComponent({
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || ""); const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const cookbookPreferences = useCookbookPreferences(); const cookbookPreferences = useCookbookPreferences();
const ownCookbookStore = useCookbookStore(i18n); const ownCookbookStore = useCookbookStore(i18n);
const ownHouseholdStore = useHouseholdStore(i18n);
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({}); const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
const publicHouseholdStoreCache = ref<Record<string, ReturnType<typeof usePublicHouseholdStore>>>({});
function getPublicCookbookStore(slug: string) { function getPublicCookbookStore(slug: string) {
if (!publicCookbookStoreCache.value[slug]) { if (!publicCookbookStoreCache.value[slug]) {
@@ -130,13 +124,6 @@ export default defineNuxtComponent({
return publicCookbookStoreCache.value[slug]; return publicCookbookStoreCache.value[slug];
} }
function getPublicHouseholdStore(slug: string) {
if (!publicHouseholdStoreCache.value[slug]) {
publicHouseholdStoreCache.value[slug] = usePublicHouseholdStore(slug, i18n);
}
return publicHouseholdStoreCache.value[slug];
}
const cookbooks = computed(() => { const cookbooks = computed(() => {
if (isOwnGroup.value) { if (isOwnGroup.value) {
return ownCookbookStore.store.value; return ownCookbookStore.store.value;
@@ -148,24 +135,6 @@ export default defineNuxtComponent({
return []; return [];
}); });
const households = computed(() => {
if (isOwnGroup.value) {
return ownHouseholdStore.store.value;
}
else if (groupSlug.value) {
const publicStore = getPublicHouseholdStore(groupSlug.value);
return unref(publicStore.store);
}
return [];
});
const householdsById = computed(() => {
return households.value.reduce((acc, household) => {
acc[household.id] = household;
return acc;
}, {} as { [key: string]: HouseholdSummary });
});
const appInfo = useAppInfo(); const appInfo = useAppInfo();
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices); const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
@@ -197,11 +166,8 @@ export default defineNuxtComponent({
const ownLinks: SideBarLink[] = []; const ownLinks: SideBarLink[] = [];
const links: SideBarLink[] = []; const links: SideBarLink[] = [];
const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => { const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => {
const householdName = householdsById.value[cookbook.householdId]?.name || ""; const householdName = cookbook.household?.name || "";
if (!acc[householdName]) { (acc[householdName] ||= []).push(cookbook);
acc[householdName] = [];
}
acc[householdName].push(cookbook);
return acc; return acc;
}, {} as Record<string, ReadCookBook[]>); }, {} as Record<string, ReadCookBook[]>);

View File

@@ -33,20 +33,39 @@
<template v-for="nav in topLink"> <template v-for="nav in topLink">
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group v-if="nav.children" :key="(nav.key || nav.title) + 'multi-item'" <v-list-group
v-model="dropDowns[nav.title]" color="primary" :prepend-icon="nav.icon" :fluid="true"> v-if="nav.children"
:key="(nav.key || nav.title) + 'multi-item'"
v-model="dropDowns[nav.title]"
color="primary"
:prepend-icon="nav.icon"
:fluid="true"
>
<template #activator="{ props }"> <template #activator="{ props }">
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" /> <v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
</template> </template>
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to" <v-list-item
:prepend-icon="child.icon" :title="child.title" class="ml-4" /> v-for="child in nav.children"
:key="child.key || child.title"
exact
:to="child.to"
:prepend-icon="child.icon"
:title="child.title"
class="ml-4"
/>
</v-list-group> </v-list-group>
<!-- Single Item --> <!-- Single Item -->
<template v-else> <template v-else>
<v-list-item :key="(nav.key || nav.title) + 'single-item'" exact link :to="nav.to" <v-list-item
:prepend-icon="nav.icon" :title="nav.title" /> :key="(nav.key || nav.title) + 'single-item'"
exact
link
:to="nav.to"
:prepend-icon="nav.icon"
:title="nav.title"
/>
</template> </template>
</div> </div>
</template> </template>
@@ -60,14 +79,27 @@
<template v-for="nav in secondaryLinks"> <template v-for="nav in secondaryLinks">
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group v-if="nav.children" :key="(nav.key || nav.title) + 'multi-item'" <v-list-group
v-model="dropDowns[nav.title]" color="primary" :prepend-icon="nav.icon" fluid> v-if="nav.children"
:key="(nav.key || nav.title) + 'multi-item'"
v-model="dropDowns[nav.title]"
color="primary"
:prepend-icon="nav.icon"
fluid
>
<template #activator="{ props }"> <template #activator="{ props }">
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" /> <v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
</template> </template>
<v-list-item v-for="child in nav.children" :key="child.key || child.title" exact :to="child.to" <v-list-item
class="ml-2" :prepend-icon="child.icon" :title="child.title" /> v-for="child in nav.children"
:key="child.key || child.title"
exact
:to="child.to"
class="ml-2"
:prepend-icon="child.icon"
:title="child.title"
/>
</v-list-group> </v-list-group>
<!-- Single Item --> <!-- Single Item -->

View File

@@ -22,7 +22,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { size } = withDefaults(defineProps<{ size?: number }>(), { size: 75 }); withDefaults(defineProps<{ size?: number }>(), { size: 75 });
</script> </script>
<style scoped> <style scoped>

View File

@@ -41,7 +41,8 @@
:hide-details="!inputField.hint" :hide-details="!inputField.hint"
:persistent-hint="!!inputField.hint" :persistent-hint="!!inputField.hint"
density="comfortable" density="comfortable"
@change="emitBlur"> @change="emitBlur"
>
<template #label> <template #label>
<span class="ml-4"> <span class="ml-4">
{{ inputField.label }} {{ inputField.label }}

View File

@@ -10,9 +10,7 @@
:max-width="maxWidth ?? undefined" :max-width="maxWidth ?? undefined"
:content-class="top ? 'top-dialog' : undefined" :content-class="top ? 'top-dialog' : undefined"
:fullscreen="$vuetify.display.xs" :fullscreen="$vuetify.display.xs"
@keydown.enter="() => { @keydown.enter="submitOnEnter"
emit('submit'); dialog = false;
}"
@click:outside="emit('cancel')" @click:outside="emit('cancel')"
@keydown.esc="emit('cancel')" @keydown.esc="emit('cancel')"
> >
@@ -127,6 +125,7 @@ interface DialogProps {
canDelete?: boolean; canDelete?: boolean;
canConfirm?: boolean; canConfirm?: boolean;
canSubmit?: boolean; canSubmit?: boolean;
disableSubmitOnEnter?: boolean;
} }
interface DialogEmits { interface DialogEmits {
@@ -150,6 +149,7 @@ const props = withDefaults(defineProps<DialogProps>(), {
canDelete: false, canDelete: false,
canConfirm: false, canConfirm: false,
canSubmit: false, canSubmit: false,
disableSubmitOnEnter: false,
}); });
const emit = defineEmits<DialogEmits>(); const emit = defineEmits<DialogEmits>();
@@ -181,6 +181,14 @@ function submitEvent() {
submitted.value = true; submitted.value = true;
} }
function submitOnEnter() {
if (props.disableSubmitOnEnter) {
return;
}
submitEvent();
}
function deleteEvent() { function deleteEvent() {
emit("delete"); emit("delete");
submitted.value = true; submitted.value = true;
@@ -192,8 +200,8 @@ function open() {
} }
/* function close() { /* function close() {
dialog.value = false; dialog.value = false;
logDeprecatedProp("close"); logDeprecatedProp("close");
} */ } */
function logDeprecatedProp(val: string) { function logDeprecatedProp(val: string) {

View File

@@ -1,6 +1,6 @@
<template> <template>
<!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() --> <!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() -->
<div v-html="value" /> <div v-html="value" />
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@@ -1,16 +1,13 @@
import { useAsyncKey } from "../use-utils";
import type { AppInfo } from "~/lib/api/types/admin"; import type { AppInfo } from "~/lib/api/types/admin";
export function useAppInfo(): Ref<AppInfo | null> { export function useAppInfo(): Ref<AppInfo | null> {
const appInfo = ref<null | AppInfo>(null);
const i18n = useI18n(); const i18n = useI18n();
const { $axios } = useNuxtApp(); const { $axios } = useNuxtApp();
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value; $axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
useAsyncData(useAsyncKey(), async () => { const { data: appInfo } = useAsyncData("app-info", async () => {
const data = await $axios.get<AppInfo>("/api/app/about"); const data = await $axios.get<AppInfo>("/api/app/about");
appInfo.value = data.data; return data.data;
}); });
return appInfo; return appInfo;

View File

@@ -1,10 +1,10 @@
import { useAsyncKey } from "../use-utils"; import type { AsyncData, NuxtError } from "#app";
import type { BoundT } from "./types"; import type { BoundT } from "./types";
import type { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients"; import type { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import type { QueryValue } from "~/lib/api/base/route"; import type { QueryValue } from "~/lib/api/base/route";
interface ReadOnlyStoreActions<T extends BoundT> { interface ReadOnlyStoreActions<T extends BoundT> {
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>; getAll(page?: number, perPage?: number, params?: any): AsyncData<T[] | null, NuxtError<unknown> | null>;
refresh(page?: number, perPage?: number, params?: any): Promise<void>; refresh(page?: number, perPage?: number, params?: any): Promise<void>;
} }
@@ -21,6 +21,7 @@ interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
* a lot of refreshing hooks to be called on operations * a lot of refreshing hooks to be called on operations
*/ */
export function useReadOnlyActions<T extends BoundT>( export function useReadOnlyActions<T extends BoundT>(
storeKey: string,
api: BaseCRUDAPIReadOnly<T>, api: BaseCRUDAPIReadOnly<T>,
allRef: Ref<T[] | null> | null, allRef: Ref<T[] | null> | null,
loading: Ref<boolean>, loading: Ref<boolean>,
@@ -29,20 +30,24 @@ export function useReadOnlyActions<T extends BoundT>(
params.orderBy ??= "name"; params.orderBy ??= "name";
params.orderDirection ??= "asc"; params.orderDirection ??= "asc";
loading.value = true; const allItems = useAsyncData(storeKey, async () => {
const allItems = useAsyncData(useAsyncKey(), async () => { loading.value = true;
const { data } = await api.getAll(page, perPage, params); try {
loading.value = false; const { data } = await api.getAll(page, perPage, params);
if (data && allRef) { if (data && allRef) {
allRef.value = data.items; allRef.value = data.items;
} }
if (data) { if (data) {
return data.items ?? []; return data.items ?? [];
}
else {
return [];
}
} }
else { finally {
return []; loading.value = false;
} }
}); });
@@ -76,6 +81,7 @@ export function useReadOnlyActions<T extends BoundT>(
* a lot of refreshing hooks to be called on operations * a lot of refreshing hooks to be called on operations
*/ */
export function useStoreActions<T extends BoundT>( export function useStoreActions<T extends BoundT>(
storeKey: string,
api: BaseCRUDAPI<unknown, T, unknown>, api: BaseCRUDAPI<unknown, T, unknown>,
allRef: Ref<T[] | null> | null, allRef: Ref<T[] | null> | null,
loading: Ref<boolean>, loading: Ref<boolean>,
@@ -84,20 +90,24 @@ export function useStoreActions<T extends BoundT>(
params.orderBy ??= "name"; params.orderBy ??= "name";
params.orderDirection ??= "asc"; params.orderDirection ??= "asc";
loading.value = true; const allItems = useAsyncData(storeKey, async () => {
const allItems = useAsyncData(useAsyncKey(), async () => { loading.value = true;
const { data } = await api.getAll(page, perPage, params); try {
loading.value = false; const { data } = await api.getAll(page, perPage, params);
if (data && allRef) { if (data && allRef) {
allRef.value = data.items; allRef.value = data.items;
} }
if (data) { if (data) {
return data.items ?? []; return data.items ?? [];
}
else {
return [];
}
} }
else { finally {
return []; loading.value = false;
} }
}); });

View File

@@ -13,12 +13,13 @@ export const useData = function <T extends BoundT>(defaultObject: T) {
}; };
export const useReadOnlyStore = function <T extends BoundT>( export const useReadOnlyStore = function <T extends BoundT>(
storeKey: string,
store: Ref<T[]>, store: Ref<T[]>,
loading: Ref<boolean>, loading: Ref<boolean>,
api: BaseCRUDAPIReadOnly<T>, api: BaseCRUDAPIReadOnly<T>,
params = {} as Record<string, QueryValue>, params = {} as Record<string, QueryValue>,
) { ) {
const storeActions = useReadOnlyActions(api, store, loading); const storeActions = useReadOnlyActions(`${storeKey}-store-readonly`, api, store, loading);
const actions = { const actions = {
...storeActions, ...storeActions,
async refresh() { async refresh() {
@@ -29,21 +30,22 @@ export const useReadOnlyStore = function <T extends BoundT>(
}, },
}; };
if (!loading.value && (!store.value || store.value.length === 0)) { // initial hydration
const result = actions.getAll(1, -1, params); if (!loading.value && !store.value.length) {
store.value = result.value || []; actions.refresh();
} }
return { store, actions }; return { store, actions };
}; };
export const useStore = function <T extends BoundT>( export const useStore = function <T extends BoundT>(
storeKey: string,
store: Ref<T[]>, store: Ref<T[]>,
loading: Ref<boolean>, loading: Ref<boolean>,
api: BaseCRUDAPI<unknown, T, unknown>, api: BaseCRUDAPI<unknown, T, unknown>,
params = {} as Record<string, QueryValue>, params = {} as Record<string, QueryValue>,
) { ) {
const storeActions = useStoreActions(api, store, loading); const storeActions = useStoreActions(`${storeKey}-store`, api, store, loading);
const actions = { const actions = {
...storeActions, ...storeActions,
async refresh() { async refresh() {
@@ -54,9 +56,9 @@ export const useStore = function <T extends BoundT>(
}, },
}; };
if (!loading.value && (!store.value || store.value.length === 0)) { // initial hydration
const result = actions.getAll(1, -1, params); if (!loading.value && !store.value.length) {
store.value = result.value || []; actions.refresh();
} }
return { store, actions }; return { store, actions };

View File

@@ -29,26 +29,31 @@ interface PageState {
editMode: ComputedRef<EditorMode>; editMode: ComputedRef<EditorMode>;
/** /**
* true is the page is in edit mode and the edit mode is in form mode. * true is the page is in edit mode and the edit mode is in form mode.
*/ */
isEditForm: ComputedRef<boolean>; isEditForm: ComputedRef<boolean>;
/** /**
* true is the page is in edit mode and the edit mode is in json mode. * true is the page is in edit mode and the edit mode is in json mode.
*/ */
isEditJSON: ComputedRef<boolean>; isEditJSON: ComputedRef<boolean>;
/** /**
* true is the page is in view mode. * true is the page is in view mode.
*/ */
isEditMode: ComputedRef<boolean>; isEditMode: ComputedRef<boolean>;
/** /**
* true is the page is in cook mode. * true is the page is in cook mode.
*/ */
isCookMode: ComputedRef<boolean>; isCookMode: ComputedRef<boolean>;
/**
* true if the recipe is currently being parsed.
*/
isParsing: ComputedRef<boolean>;
setMode: (v: PageMode) => void; setMode: (v: PageMode) => void;
setEditMode: (v: EditorMode) => void; setEditMode: (v: EditorMode) => void;
toggleEditMode: () => void; toggleEditMode: () => void;
toggleCookMode: () => void; toggleCookMode: () => void;
toggleIsParsing: (v?: boolean) => void;
} }
type PageRefs = ReturnType<typeof pageRefs>; type PageRefs = ReturnType<typeof pageRefs>;
@@ -60,11 +65,12 @@ function pageRefs(slug: string) {
slugRef: ref(slug), slugRef: ref(slug),
pageModeRef: ref(PageMode.VIEW), pageModeRef: ref(PageMode.VIEW),
editModeRef: ref(EditorMode.FORM), editModeRef: ref(EditorMode.FORM),
isParsingRef: ref(false),
imageKey: ref(1), imageKey: ref(1),
}; };
} }
function pageState({ slugRef, pageModeRef, editModeRef, imageKey }: PageRefs): PageState { function pageState({ slugRef, pageModeRef, editModeRef, isParsingRef, imageKey }: PageRefs): PageState {
const { activateNavigationWarning, deactivateNavigationWarning } = useNavigationWarning(); const { activateNavigationWarning, deactivateNavigationWarning } = useNavigationWarning();
const toggleEditMode = () => { const toggleEditMode = () => {
@@ -83,6 +89,14 @@ function pageState({ slugRef, pageModeRef, editModeRef, imageKey }: PageRefs): P
pageModeRef.value = PageMode.COOK; pageModeRef.value = PageMode.COOK;
}; };
const toggleIsParsing = (v: boolean | null = null) => {
if (v === null) {
v = !isParsingRef.value;
}
isParsingRef.value = v;
};
const setEditMode = (v: EditorMode) => { const setEditMode = (v: EditorMode) => {
editModeRef.value = v; editModeRef.value = v;
}; };
@@ -113,6 +127,7 @@ function pageState({ slugRef, pageModeRef, editModeRef, imageKey }: PageRefs): P
setMode, setMode,
setEditMode, setEditMode,
toggleCookMode, toggleCookMode,
toggleIsParsing,
isEditForm: computed(() => { isEditForm: computed(() => {
return pageModeRef.value === PageMode.EDIT && editModeRef.value === EditorMode.FORM; return pageModeRef.value === PageMode.EDIT && editModeRef.value === EditorMode.FORM;
@@ -126,6 +141,9 @@ function pageState({ slugRef, pageModeRef, editModeRef, imageKey }: PageRefs): P
isCookMode: computed(() => { isCookMode: computed(() => {
return pageModeRef.value === PageMode.COOK; return pageModeRef.value === PageMode.COOK;
}), }),
isParsing: computed(() => {
return isParsingRef.value;
}),
}; };
} }

View File

@@ -17,10 +17,10 @@ export const useCategoryData = function () {
export const useCategoryStore = function (i18n?: Composer) { export const useCategoryStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<RecipeCategory>(store, loading, api.categories); return useStore<RecipeCategory>("category", store, loading, api.categories);
}; };
export const usePublicCategoryStore = function (groupSlug: string, i18n?: Composer) { export const usePublicCategoryStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<RecipeCategory>(store, publicLoading, api.categories); return useReadOnlyStore<RecipeCategory>("category", store, publicLoading, api.categories);
}; };

View File

@@ -9,7 +9,7 @@ const publicLoading = ref(false);
export const useCookbookStore = function (i18n?: Composer) { export const useCookbookStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
const store = useStore<ReadCookBook>(cookbooks, loading, api.cookbooks); const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);
const updateAll = async function (updateData: UpdateCookBook[]) { const updateAll = async function (updateData: UpdateCookBook[]) {
loading.value = true; loading.value = true;
@@ -25,5 +25,5 @@ export const useCookbookStore = function (i18n?: Composer) {
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) { export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<ReadCookBook>(cookbooks, publicLoading, api.cookbooks); return useReadOnlyStore<ReadCookBook>("cookbook", cookbooks, publicLoading, api.cookbooks);
}; };

View File

@@ -18,10 +18,10 @@ export const useFoodData = function () {
export const useFoodStore = function (i18n?: Composer) { export const useFoodStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<IngredientFood>(store, loading, api.foods); return useStore<IngredientFood>("food", store, loading, api.foods);
}; };
export const usePublicFoodStore = function (groupSlug: string, i18n?: Composer) { export const usePublicFoodStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<IngredientFood>(store, publicLoading, api.foods); return useReadOnlyStore<IngredientFood>("food", store, publicLoading, api.foods);
}; };

View File

@@ -9,10 +9,10 @@ const publicLoading = ref(false);
export const useHouseholdStore = function (i18n?: Composer) { export const useHouseholdStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useReadOnlyStore<HouseholdSummary>(store, loading, api.households); return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);
}; };
export const usePublicHouseholdStore = function (groupSlug: string, i18n?: Composer) { export const usePublicHouseholdStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<HouseholdSummary>(store, publicLoading, api.households); return useReadOnlyStore<HouseholdSummary>("household-public", store, publicLoading, api.households);
}; };

View File

@@ -17,5 +17,5 @@ export const useLabelData = function () {
export const useLabelStore = function (i18n?: Composer) { export const useLabelStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<MultiPurposeLabelOut>(store, loading, api.multiPurposeLabels); return useStore<MultiPurposeLabelOut>("label", store, loading, api.multiPurposeLabels);
}; };

View File

@@ -17,10 +17,10 @@ export const useTagData = function () {
export const useTagStore = function (i18n?: Composer) { export const useTagStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<RecipeTag>(store, loading, api.tags); return useStore<RecipeTag>("tag", store, loading, api.tags);
}; };
export const usePublicTagStore = function (groupSlug: string, i18n?: Composer) { export const usePublicTagStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<RecipeTag>(store, publicLoading, api.tags); return useReadOnlyStore<RecipeTag>("tag", store, publicLoading, api.tags);
}; };

View File

@@ -23,10 +23,10 @@ export const useToolData = function () {
export const useToolStore = function (i18n?: Composer) { export const useToolStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<RecipeTool>(store, loading, api.tools); return useStore<RecipeTool>("tool", store, loading, api.tools);
}; };
export const usePublicToolStore = function (groupSlug: string, i18n?: Composer) { export const usePublicToolStore = function (groupSlug: string, i18n?: Composer) {
const api = usePublicExploreApi(groupSlug, i18n).explore; const api = usePublicExploreApi(groupSlug, i18n).explore;
return useReadOnlyStore<RecipeTool>(store, publicLoading, api.tools); return useReadOnlyStore<RecipeTool>("tool", store, publicLoading, api.tools);
}; };

View File

@@ -18,5 +18,5 @@ export const useUnitData = function () {
export const useUnitStore = function (i18n?: Composer) { export const useUnitStore = function (i18n?: Composer) {
const api = useUserApi(i18n); const api = useUserApi(i18n);
return useStore<IngredientUnit>(store, loading, api.units); return useStore<IngredientUnit>("unit", store, loading, api.units);
}; };

View File

@@ -16,5 +16,5 @@ export const useUserStore = function (i18n?: Composer) {
const requests = useRequests(i18n); const requests = useRequests(i18n);
const api = new GroupUserAPIReadOnly(requests); const api = new GroupUserAPIReadOnly(requests);
return useReadOnlyStore<UserSummary>(store, loading, api, { orderBy: "full_name" }); return useReadOnlyStore<UserSummary>("user", store, loading, api, { orderBy: "full_name" });
}; };

View File

@@ -29,8 +29,8 @@ export function useGroupRecipeActionData() {
} }
export const useGroupRecipeActions = function ( export const useGroupRecipeActions = function (
orderBy: string | null = "title", orderBy: string | null = "title",
orderDirection: string | null = "asc", orderDirection: string | null = "asc",
) { ) {
const api = useUserApi(); const api = useUserApi();
@@ -78,7 +78,7 @@ export const useGroupRecipeActions = function (
}; };
const actions = { const actions = {
...useStoreActions<GroupRecipeActionOut>(api.groupRecipeActions, groupRecipeActions, loading), ...useStoreActions<GroupRecipeActionOut>("group-recipe-actions", api.groupRecipeActions, groupRecipeActions, loading),
flushStore() { flushStore() {
groupRecipeActions.value = []; groupRecipeActions.value = [];
}, },

View File

@@ -21,7 +21,7 @@ export const LOCALES = [
{ {
name: "Українська (Ukrainian)", name: "Українська (Ukrainian)",
value: "uk-UA", value: "uk-UA",
progress: 37, progress: 44,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -45,7 +45,7 @@ export const LOCALES = [
{ {
name: "Slovenščina (Slovenian)", name: "Slovenščina (Slovenian)",
value: "sl-SI", value: "sl-SI",
progress: 39, progress: 40,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -75,7 +75,7 @@ export const LOCALES = [
{ {
name: "Português do Brasil (Brazilian Portuguese)", name: "Português do Brasil (Brazilian Portuguese)",
value: "pt-BR", value: "pt-BR",
progress: 41, progress: 45,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -105,7 +105,7 @@ export const LOCALES = [
{ {
name: "Lietuvių (Lithuanian)", name: "Lietuvių (Lithuanian)",
value: "lt-LT", value: "lt-LT",
progress: 26, progress: 27,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -123,7 +123,7 @@ export const LOCALES = [
{ {
name: "Italiano (Italian)", name: "Italiano (Italian)",
value: "it-IT", value: "it-IT",
progress: 40, progress: 41,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -135,7 +135,7 @@ export const LOCALES = [
{ {
name: "Magyar (Hungarian)", name: "Magyar (Hungarian)",
value: "hu-HU", value: "hu-HU",
progress: 44, progress: 45,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -159,7 +159,7 @@ export const LOCALES = [
{ {
name: "Français (French)", name: "Français (French)",
value: "fr-FR", value: "fr-FR",
progress: 64, progress: 66,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -213,7 +213,7 @@ export const LOCALES = [
{ {
name: "Deutsch (German)", name: "Deutsch (German)",
value: "de-DE", value: "de-DE",
progress: 72, progress: 78,
dir: "ltr", dir: "ltr",
}, },
{ {
@@ -237,7 +237,7 @@ export const LOCALES = [
{ {
name: "Български (Bulgarian)", name: "Български (Bulgarian)",
value: "bg-BG", value: "bg-BG",
progress: 31, progress: 44,
dir: "ltr", dir: "ltr",
}, },
{ {

View File

@@ -0,0 +1,85 @@
import { useRecipeCreatePreferences } from "~/composables/use-users/preferences";
export interface UseNewRecipeOptionsProps {
enableImportKeywords?: boolean;
enableStayInEditMode?: boolean;
enableParseRecipe?: boolean;
}
export function useNewRecipeOptions(props: UseNewRecipeOptionsProps = {}) {
const {
enableImportKeywords = true,
enableStayInEditMode = true,
enableParseRecipe = true,
} = props;
const router = useRouter();
const recipeCreatePreferences = useRecipeCreatePreferences();
const importKeywordsAsTags = computed({
get() {
if (!enableImportKeywords) return false;
return recipeCreatePreferences.value.importKeywordsAsTags;
},
set(v: boolean) {
if (!enableImportKeywords) return;
recipeCreatePreferences.value.importKeywordsAsTags = v;
},
});
const stayInEditMode = computed({
get() {
if (!enableStayInEditMode) return false;
return recipeCreatePreferences.value.stayInEditMode;
},
set(v: boolean) {
if (!enableStayInEditMode) return;
recipeCreatePreferences.value.stayInEditMode = v;
},
});
const parseRecipe = computed({
get() {
if (!enableParseRecipe) return false;
return recipeCreatePreferences.value.parseRecipe;
},
set(v: boolean) {
if (!enableParseRecipe) return;
recipeCreatePreferences.value.parseRecipe = v;
},
});
function navigateToRecipe(recipeSlug: string, groupSlug: string, createPagePath: string) {
const editParam = enableStayInEditMode ? stayInEditMode.value : false;
const parseParam = enableParseRecipe ? parseRecipe.value : false;
const queryParams = new URLSearchParams();
if (editParam) {
queryParams.set("edit", "true");
}
if (parseParam) {
queryParams.set("parse", "true");
}
const queryString = queryParams.toString();
const recipeUrl = `/g/${groupSlug}/r/${recipeSlug}${queryString ? `?${queryString}` : ""}`;
// Replace current entry to prevent re-import on back navigation
router.replace(createPagePath).then(() => router.push(recipeUrl));
}
return {
// Computed properties for the checkboxes
importKeywordsAsTags,
stayInEditMode,
parseRecipe,
// Helper functions
navigateToRecipe,
// Props for conditional rendering
enableImportKeywords,
enableStayInEditMode,
enableParseRecipe,
};
}

View File

@@ -17,19 +17,19 @@ export interface OrganizerBase {
name: string; name: string;
} }
export type FieldType = export type FieldType
| "string" = | "string"
| "number" | "number"
| "boolean" | "boolean"
| "date" | "date"
| RecipeOrganizer; | RecipeOrganizer;
export type FieldValue = export type FieldValue
| string = | string
| number | number
| boolean | boolean
| Date | Date
| Organizer; | Organizer;
export interface SelectableItem { export interface SelectableItem {
label: string; label: string;

View File

@@ -177,8 +177,8 @@ export function useShoppingListItemActions(shoppingListId: string) {
} }
/** /**
* Processes the queue items and returns whether the processing was successful. * Processes the queue items and returns whether the processing was successful.
*/ */
async function processQueueItems( async function processQueueItems(
action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>, action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>,
itemQueueType: ItemQueueType, itemQueueType: ItemQueueType,

View File

@@ -59,6 +59,12 @@ export interface UserRecipeFinderPreferences {
includeToolsOnHand: boolean; includeToolsOnHand: boolean;
} }
export interface UserRecipeCreatePreferences {
importKeywordsAsTags: boolean;
stayInEditMode: boolean;
parseRecipe: boolean;
}
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> { export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
const fromStorage = useLocalStorage( const fromStorage = useLocalStorage(
"meal-planner-preferences", "meal-planner-preferences",
@@ -200,3 +206,19 @@ export function useRecipeFinderPreferences(): Ref<UserRecipeFinderPreferences> {
return fromStorage; return fromStorage;
} }
export function useRecipeCreatePreferences(): Ref<UserRecipeCreatePreferences> {
const fromStorage = useLocalStorage(
"recipe-create-preferences",
{
importKeywordsAsTags: false,
stayInEditMode: false,
parseRecipe: true,
},
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserRecipeCreatePreferences>;
return fromStorage;
}

View File

@@ -8,7 +8,7 @@ export const useToggleDarkMode = () => {
}; };
export const useAsyncKey = function () { export const useAsyncKey = function () {
return String(Date.now()); return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}; };
export const titleCase = function (str: string) { export const titleCase = function (str: string) {

View File

@@ -37,11 +37,6 @@ export const useMealieAuth = function () {
{ immediate: true }, { immediate: true },
); );
async function signIn(...params: Parameters<typeof auth.signIn>) {
await auth.signIn(...params);
refreshCookie(useRuntimeConfig().public.AUTH_TOKEN);
}
async function oauthSignIn() { async function oauthSignIn() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const { data: token } = await $axios.get<{ access_token: string; token_type: "bearer" }>("/api/auth/oauth/callback", { params }); const { data: token } = await $axios.get<{ access_token: string; token_type: "bearer" }>("/api/auth/oauth/callback", { params });
@@ -52,7 +47,7 @@ export const useMealieAuth = function () {
return { return {
user, user,
loggedIn, loggedIn,
signIn, signIn: auth.signIn,
signOut: auth.signOut, signOut: auth.signOut,
signUp: auth.signUp, signUp: auth.signUp,
refresh: auth.refresh, refresh: auth.refresh,

View File

@@ -1,24 +1,25 @@
// @ts-check // @ts-check
import stylisticJs from "@stylistic/eslint-plugin-js"; import stylistic from "@stylistic/eslint-plugin";
import withNuxt from "./.nuxt/eslint.config.mjs"; import withNuxt from "./.nuxt/eslint.config.mjs";
export default withNuxt({ export default withNuxt({
plugins: { plugins: {
"@stylistic/js": stylisticJs, "@stylistic": stylistic,
}, },
// Your custom configs here
rules: { rules: {
"@typescript-eslint/no-explicit-any": "off", "@stylistic/no-tabs": ["error"],
"vue/no-mutating-props": "warn",
"vue/no-v-html": "warn",
"object-curly-newline": "off",
"consistent-list-newline": "off",
"vue/first-attribute-linebreak": "off",
"@stylistic/js/no-tabs": ["error", { allowIndentationTabs: true }],
"@stylistic/no-tabs": ["error", { allowIndentationTabs: true }],
"@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"], "@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"vue/max-attributes-per-line": "off", "@typescript-eslint/no-explicit-any": "off",
"vue/html-indent": "off", "vue/first-attribute-linebreak": "error",
"vue/html-closing-bracket-newline": "off", "vue/html-closing-bracket-newline": "error",
"vue/max-attributes-per-line": [
"error",
{
singleline: 5,
multiline: 1,
},
],
"vue/no-mutating-props": "error",
"vue/no-v-html": "error",
}, },
}); });

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"import-original-keywords-as-tags": "Voer oorspronklike sleutelwoorde as merkers in", "import-original-keywords-as-tags": "Voer oorspronklike sleutelwoorde as merkers in",
"stay-in-edit-mode": "Bly in redigeer modus", "stay-in-edit-mode": "Bly in redigeer modus",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Voer vanaf zip in", "import-from-zip": "Voer vanaf zip in",
"import-from-zip-description": "Voer 'n enkele resep in wat vanaf 'n ander Mealie-instansie uitgevoer is.", "import-from-zip-description": "Voer 'n enkele resep in wat vanaf 'n ander Mealie-instansie uitgevoer is.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Additional Ingredients",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "يمكنك الإضافة مباشرة باستخدام بيانات خام", "scrape-recipe-you-can-import-from-raw-data-directly": "يمكنك الإضافة مباشرة باستخدام بيانات خام",
"import-original-keywords-as-tags": "استيراد الكلمات المفتاحية الأصلية كوسوم", "import-original-keywords-as-tags": "استيراد الكلمات المفتاحية الأصلية كوسوم",
"stay-in-edit-mode": "البقاء في وضع التعديل", "stay-in-edit-mode": "البقاء في وضع التعديل",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "استيراد من ملف Zip", "import-from-zip": "استيراد من ملف Zip",
"import-from-zip-description": "استيراد وصفة واحدة تم تصديرها من حساب \"ميلي\" آخر", "import-from-zip-description": "استيراد وصفة واحدة تم تصديرها من حساب \"ميلي\" آخر",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "إنشاء طعام مفقود: {food}", "missing-food": "إنشاء طعام مفقود: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "لا يوجد طعام" "no-food": "لا يوجد طعام",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "إعادة تعيين عدد الحصص", "reset-servings-count": "إعادة تعيين عدد الحصص",
"not-linked-ingredients": "مكونات إضافية", "not-linked-ingredients": "مكونات إضافية",

View File

@@ -4,7 +4,7 @@
"about-mealie": "Относно Mealie", "about-mealie": "Относно Mealie",
"api-docs": "API Документация", "api-docs": "API Документация",
"api-port": "API Порт", "api-port": "API Порт",
"application-mode": "Приложението", "application-mode": "Режим на приложение",
"database-type": "Тип на база данни", "database-type": "Тип на база данни",
"database-url": "URL адрес база данни", "database-url": "URL адрес база данни",
"default-group": "Група по подразбиране", "default-group": "Група по подразбиране",
@@ -69,7 +69,7 @@
"new-notification": "Ново известие", "new-notification": "Ново известие",
"event-notifiers": "Известия за събитие", "event-notifiers": "Известия за събитие",
"apprise-url-skipped-if-blank": "URL за известяване (пропуска се ако е празно)", "apprise-url-skipped-if-blank": "URL за известяване (пропуска се ако е празно)",
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.", "apprise-url-is-left-intentionally-blank": "Тъй като URL адресите на Apprise обикновено съдържат чувствителна информация, това поле е оставено умишлено празно по време на редактиране. Ако желаете да актуализирате URL адреса, моля, въведете новия тук, в противен случай го оставете празно, за да запазите текущия URL адрес.",
"enable-notifier": "Включи известията", "enable-notifier": "Включи известията",
"what-events": "За кои събития трябва да се получават известия?", "what-events": "За кои събития трябва да се получават известия?",
"user-events": "Потребителски събития", "user-events": "Потребителски събития",
@@ -81,7 +81,7 @@
"category-events": "Събития за категория", "category-events": "Събития за категория",
"when-a-new-user-joins-your-group": "Когато потребител се присъедини към твоята потребителска група", "when-a-new-user-joins-your-group": "Когато потребител се присъедини към твоята потребителска група",
"recipe-events": "Събития на рецептата", "recipe-events": "Събития на рецептата",
"label-events": "Label Events" "label-events": "Събития с етикети"
}, },
"general": { "general": {
"add": "Добави", "add": "Добави",
@@ -93,13 +93,13 @@
"confirm-delete-generic": "Сигурни ли сте, че желаете да изтриете това?", "confirm-delete-generic": "Сигурни ли сте, че желаете да изтриете това?",
"copied_message": "Копирано!", "copied_message": "Копирано!",
"create": "Добави", "create": "Добави",
"created": "Създадено", "created": "Последно добавени",
"custom": "Персонализиран", "custom": "Персонализиран",
"dashboard": "Табло", "dashboard": "Табло",
"delete": "Изтриване", "delete": "Изтриване",
"disabled": "Деактивирано", "disabled": "Деактивирано",
"download": "Изтегли", "download": "Изтегли",
"duplicate": "Дублирай", "duplicate": "Дублиране",
"edit": "Редактирай", "edit": "Редактирай",
"enabled": "Активиран", "enabled": "Активиран",
"exception": "Грешка", "exception": "Грешка",
@@ -135,15 +135,15 @@
"ok": "Добре", "ok": "Добре",
"options": "Опции:", "options": "Опции:",
"plural-name": "Име в множествено число", "plural-name": "Име в множествено число",
"print": "Принтирай", "print": "Отпечатване",
"print-preferences": "Настройки на принтиране", "print-preferences": "Настройки на печата",
"random": "Произволно", "random": "Произволна рецепта",
"rating": "Оценка", "rating": "Оценка",
"recent": "Скорошни", "recent": "Скорошни",
"recipe": "Рецепта", "recipe": "Рецепта",
"recipes": "Рецепти", "recipes": "Рецепти",
"rename-object": "Преименувай {0}", "rename-object": "Преименувай {0}",
"reset": "Нулирай", "reset": "По подразбиране",
"saturday": "Събота", "saturday": "Събота",
"save": "Запази", "save": "Запази",
"settings": "Настройки", "settings": "Настройки",
@@ -169,7 +169,7 @@
"tuesday": "Вторник", "tuesday": "Вторник",
"type": "Тип", "type": "Тип",
"update": "Актуализация", "update": "Актуализация",
"updated": "Обновено", "updated": "Последно обновени",
"upload": "Качи", "upload": "Качи",
"url": "URL", "url": "URL",
"view": "Преглед", "view": "Преглед",
@@ -198,7 +198,7 @@
"copy": "Копиране", "copy": "Копиране",
"color": "Цвят", "color": "Цвят",
"timestamp": "Времева отметка", "timestamp": "Времева отметка",
"last-made": "Последно приготвена на", "last-made": "Дата на последно приготвяне",
"learn-more": "Научи повече", "learn-more": "Научи повече",
"this-feature-is-currently-inactive": "Тази функционалност в момента е неактивна", "this-feature-is-currently-inactive": "Тази функционалност в момента е неактивна",
"clipboard-not-supported": "Не се поддържа клипборд", "clipboard-not-supported": "Не се поддържа клипборд",
@@ -210,7 +210,7 @@
"export-all": "Експортиране на всички", "export-all": "Експортиране на всички",
"refresh": "Опресняване", "refresh": "Опресняване",
"upload-file": "Качване на файл", "upload-file": "Качване на файл",
"created-on-date": "Създадено на {0}", "created-on-date": "Добавена на {0}",
"unsaved-changes": "Имате незапазени промени. Желаете ли да ги запазите преди да излезете? Натиснете Ок за запазване и Отказ за отхвърляне на промените.", "unsaved-changes": "Имате незапазени промени. Желаете ли да ги запазите преди да излезете? Натиснете Ок за запазване и Отказ за отхвърляне на промените.",
"clipboard-copy-failure": "Линкът към рецептата е копиран в клипборда.", "clipboard-copy-failure": "Линкът към рецептата е копиран в клипборда.",
"confirm-delete-generic-items": "Сигурни ли сте, че желаете да изтриете следните елементи?", "confirm-delete-generic-items": "Сигурни ли сте, че желаете да изтриете следните елементи?",
@@ -279,7 +279,7 @@
"admin-group-management-text": "Промените по тази група ще бъдат отразени моментално.", "admin-group-management-text": "Промените по тази група ще бъдат отразени моментално.",
"group-id-value": "ID на Групата: {0}", "group-id-value": "ID на Групата: {0}",
"total-households": "Общ брой домакинства", "total-households": "Общ брой домакинства",
"you-must-select-a-group-before-selecting-a-household": "You must select a group before selecting a household" "you-must-select-a-group-before-selecting-a-household": "Трябва да изберете група, преди да изберете домакинство"
}, },
"household": { "household": {
"household": "Домакинство", "household": "Домакинство",
@@ -300,8 +300,8 @@
"household-recipe-preferences": "Предпочитания за рецептите на домакинството", "household-recipe-preferences": "Предпочитания за рецептите на домакинството",
"default-recipe-preferences-description": "Това са настройките по подразбиране когато нова рецепта е създадена в домакинството ви. Тези настройки могат да бъдат променени за всяка рецепта в менюто за настройки на рецептата.", "default-recipe-preferences-description": "Това са настройките по подразбиране когато нова рецепта е създадена в домакинството ви. Тези настройки могат да бъдат променени за всяка рецепта в менюто за настройки на рецептата.",
"allow-users-outside-of-your-household-to-see-your-recipes": "Разреши на потребители от други домакинства да виждат рецептите ми", "allow-users-outside-of-your-household-to-see-your-recipes": "Разреши на потребители от други домакинства да виждат рецептите ми",
"allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link", "allow-users-outside-of-your-household-to-see-your-recipes-description": "Когато е активирано, можете да използвате публичен линк за споделяне, за да споделяте конкретни рецепти, без да оторизирате потребителя. Когато е деактивирано, можете да споделяте рецепти само с потребители, които са във вашето домакинство, или с предварително генериран личен линк",
"household-preferences": "Household Preferences" "household-preferences": "Предпочитания на домакинството"
}, },
"meal-plan": { "meal-plan": {
"create-a-new-meal-plan": "Създаване на нов хранителен план", "create-a-new-meal-plan": "Създаване на нов хранителен план",
@@ -323,15 +323,15 @@
"mealplan-settings": "Настройки на менюто", "mealplan-settings": "Настройки на менюто",
"mealplan-update-failed": "Неуспешно обновяване на седмичното меню", "mealplan-update-failed": "Неуспешно обновяване на седмичното меню",
"mealplan-updated": "Седмичното меню бе обновено", "mealplan-updated": "Седмичното меню бе обновено",
"mealplan-households-description": "If no household is selected, recipes can be added from any household", "mealplan-households-description": "Ако не е избрано домакинство, могат да се добавят рецепти от всяко домакинство",
"any-category": "Всяка категория", "any-category": "Всяка категория",
"any-tag": "Всеки етикет", "any-tag": "Всеки етикет",
"any-household": "Any Household", "any-household": "Всяко домакинство",
"no-meal-plan-defined-yet": "Все още няма създадено седмично меню", "no-meal-plan-defined-yet": "Все още няма създадено седмично меню",
"no-meal-planned-for-today": "За днес няма планирано меню", "no-meal-planned-for-today": "За днес няма планирано меню",
"numberOfDays-hint": "Number of days on page load", "numberOfDays-hint": "Брой дни за зареждане на страницата",
"numberOfDays-label": "Default Days", "numberOfDays-label": "Дни по подразбиране",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Само рецептите от тези категории ще бъдат използвани в хранителните планове", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Само рецепти от тези категории ще бъдат използвани в седмичното меню",
"planner": "Планьор", "planner": "Планьор",
"quick-week": "Бърза седмица", "quick-week": "Бърза седмица",
"side": "Предястие", "side": "Предястие",
@@ -359,7 +359,7 @@
"for-type-meal-types": "за {0}", "for-type-meal-types": "за {0}",
"meal-plan-rules": "Правила за съставяне на седмично меню", "meal-plan-rules": "Правила за съставяне на седмично меню",
"new-rule": "Ново правило", "new-rule": "Ново правило",
"meal-plan-rules-description": "You can create rules for auto selecting recipes for your meal plans. These rules are used by the server to determine the random pool of recipes to select from when creating meal plans. Note that if rules have the same day/type constraints then the rule filters will be merged. In practice, it's unnecessary to create duplicate rules, but it's possible to do so.", "meal-plan-rules-description": "Можете да създавате правила за автоматично избиране на рецепти за вашите хранителни планове. Тези правила се използват от сървъра, за да определи произволния набор от рецепти, от които да се избира при създаването на хранителни планове. Обърнете внимание, че ако правилата имат едни и същи ограничения за ден/тип, филтрите на правилата ще бъдат обединени. На практика не е необходимо да се създават дублиращи се правила, но е възможно да се направи.",
"new-rule-description": "Когато създавате ново правило за създаване на седмично меню, може да зададете ограничение правилото да бъде приложено за определен ден от седмицата и/или специфичен вид ястие. За да добавите правило за всички дни или всички типове ястия, Вие може да изберете \"Всички\", което ще го приложи за всички дни и/или видове ястия.", "new-rule-description": "Когато създавате ново правило за създаване на седмично меню, може да зададете ограничение правилото да бъде приложено за определен ден от седмицата и/или специфичен вид ястие. За да добавите правило за всички дни или всички типове ястия, Вие може да изберете \"Всички\", което ще го приложи за всички дни и/или видове ястия.",
"recipe-rules": "Правила на рецептата", "recipe-rules": "Правила на рецептата",
"applies-to-all-days": "Прилага се за всички дни", "applies-to-all-days": "Прилага се за всички дни",
@@ -420,7 +420,7 @@
}, },
"recipekeeper": { "recipekeeper": {
"title": "Recipe Keeper", "title": "Recipe Keeper",
"description-long": "Mealie can import recipes from Recipe Keeper. Export your recipes in zip format, then upload the .zip file below." "description-long": "Mealie може да импортира рецепти от Recipe Keeper. Експортирайте рецептите си в zip формат, след което качете .zip файла по-долу."
} }
}, },
"new-recipe": { "new-recipe": {
@@ -434,7 +434,7 @@
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Поставете в данните на рецептата си. Всеки ред ще бъде третиран като елемент от списъка.", "paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Поставете в данните на рецептата си. Всеки ред ще бъде третиран като елемент от списъка.",
"recipe-markup-specification": "Спецификации за маркиране на рецептата", "recipe-markup-specification": "Спецификации за маркиране на рецептата",
"recipe-url": "URL на рецептата", "recipe-url": "URL на рецептата",
"recipe-html-or-json": "Recipe HTML or JSON", "recipe-html-or-json": "HTML или JSON на рецептата",
"upload-a-recipe": "Качи рецепта", "upload-a-recipe": "Качи рецепта",
"upload-individual-zip-file": "Качи като индивидуален .zip файлов формат от друга инстанция на Mealie.", "upload-individual-zip-file": "Качи като индивидуален .zip файлов формат от друга инстанция на Mealie.",
"url-form-hint": "Копирай и постави линк от твоя любим сайт за рецепти", "url-form-hint": "Копирай и постави линк от твоя любим сайт за рецепти",
@@ -474,23 +474,23 @@
"comment": "Коментар", "comment": "Коментар",
"comments": "Коментари", "comments": "Коментари",
"delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?", "delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?", "admin-delete-confirmation": "Ще изтриете рецепта, която не е ваша, използвайки администраторски права. Сигурни ли сте?",
"delete-recipe": "Изтрий рецептата", "delete-recipe": "Изтрий рецептата",
"description": "Описание", "description": "Описание",
"disable-amount": "Изключи количествата за съставките", "disable-amount": "Изключи количествата за съставките",
"disable-comments": "Изключи коментарите", "disable-comments": "Изключи коментарите",
"duplicate": "Дублирай рецептата", "duplicate": "Дублиране на рецептата",
"duplicate-name": "Име на новата рецепта", "duplicate-name": "Име на новата рецепта",
"edit-scale": "Редактиране на размера", "edit-scale": "Редактиране на размера",
"fat-content": "Мазнини", "fat-content": "Мазнини",
"fiber-content": "Влакна", "fiber-content": "Влакна",
"grams": "грама", "grams": "грама",
"ingredient": "Съставка", "ingredient": "Съставка",
"ingredients": "Съставки", "ingredients": "Необходими продукти",
"insert-ingredient": "Въведете съставка", "insert-ingredient": "Въведете съставка",
"insert-section": "Въведете раздел", "insert-section": "Въведете раздел",
"insert-above": "Insert Above", "insert-above": "Вмъкни отгоре",
"insert-below": "Insert Below", "insert-below": "Вмъкни по-долу",
"instructions": "Инструкции", "instructions": "Инструкции",
"key-name-required": "Ключовото име е задължително", "key-name-required": "Ключовото име е задължително",
"landscape-view-coming-soon": "Пейзажен изглед", "landscape-view-coming-soon": "Пейзажен изглед",
@@ -503,7 +503,7 @@
"object-value": "Стойност на обект", "object-value": "Стойност на обект",
"original-url": "Оригинален линк", "original-url": "Оригинален линк",
"perform-time": "Време за готвене", "perform-time": "Време за готвене",
"prep-time": "Време за приготвяне", "prep-time": "Време за подготовка",
"protein-content": "Белтъци", "protein-content": "Белтъци",
"public-recipe": "Публична рецепта", "public-recipe": "Публична рецепта",
"recipe-created": "Рецептата е създадена", "recipe-created": "Рецептата е създадена",
@@ -511,27 +511,27 @@
"recipe-deleted": "Рецептата е изтрита", "recipe-deleted": "Рецептата е изтрита",
"recipe-image": "Изображение на рецептата", "recipe-image": "Изображение на рецептата",
"recipe-image-updated": "Изображението на рецептата беше обновено", "recipe-image-updated": "Изображението на рецептата беше обновено",
"recipe-name": "Име на рецептата", "recipe-name": "Наименование",
"recipe-settings": "Настройки на рецептата", "recipe-settings": "Настройки на рецептата",
"recipe-update-failed": "Обновяването на рецептата беше неуспешно", "recipe-update-failed": "Обновяването на рецептата беше неуспешно",
"recipe-updated": "Рецептата е обновена", "recipe-updated": "Рецептата е обновена",
"remove-from-favorites": "Премахни от любими", "remove-from-favorites": "Премахни от любими",
"remove-section": "Премахни раздел", "remove-section": "Премахни раздел",
"saturated-fat-content": "Saturated fat", "saturated-fat-content": "Наситени мазнини",
"save-recipe-before-use": "Запази рецептата преди да я използваш", "save-recipe-before-use": "Запази рецептата преди да я използваш",
"section-title": "Заглавие на раздела", "section-title": "Заглавие на раздела",
"servings": "Порция|порции", "servings": "Порции",
"serves-amount": "Serves {amount}", "serves-amount": "Количествата са за {amount} порции",
"share-recipe-message": "Искам да споделя моята рецепта {0} с теб.", "share-recipe-message": "Искам да споделя моята рецепта {0} с теб.",
"show-nutrition-values": "Покажи хранителните стойности", "show-nutrition-values": "Покажи хранителните стойности",
"sodium-content": "Натрий", "sodium-content": "Натрий",
"step-index": "Стъпка: {step}", "step-index": "Стъпка: {step}",
"sugar-content": "Захар", "sugar-content": "Захар",
"title": "Заглавие", "title": "Заглавие",
"total-time": "Общо време", "total-time": "Общо време за приготвяне",
"trans-fat-content": "Trans-fat", "trans-fat-content": "Транс мазнини",
"unable-to-delete-recipe": "Изтриването на рецептата е невъзможно", "unable-to-delete-recipe": "Изтриването на рецептата е невъзможно",
"unsaturated-fat-content": "Unsaturated fat", "unsaturated-fat-content": "Ненаситени мазнини",
"no-recipe": "Няма рецепта", "no-recipe": "Няма рецепта",
"locked-by-owner": "Заключена от собственика", "locked-by-owner": "Заключена от собственика",
"join-the-conversation": "Присъедини се към разговора", "join-the-conversation": "Присъедини се към разговора",
@@ -539,9 +539,9 @@
"entry-type": "Тип на записа", "entry-type": "Тип на записа",
"date-format-hint": "MM/DD/YYYY формат", "date-format-hint": "MM/DD/YYYY формат",
"date-format-hint-yyyy-mm-dd": "YYYY-MM-DD формат", "date-format-hint-yyyy-mm-dd": "YYYY-MM-DD формат",
"add-to-list": "Добави към списък", "add-to-list": "Добавяне към списък",
"add-to-plan": "Добави към план", "add-to-plan": "Добавяне към план",
"add-to-timeline": "Добави към историята на събитията", "add-to-timeline": "Добавяне към историята на събитията",
"recipe-added-to-list": "Рецептата е добавена към списъка", "recipe-added-to-list": "Рецептата е добавена към списъка",
"recipes-added-to-list": "Рецептите са добавени към списъка", "recipes-added-to-list": "Рецептите са добавени към списъка",
"successfully-added-to-list": "Успешно добавено в списъка", "successfully-added-to-list": "Успешно добавено в списъка",
@@ -549,9 +549,9 @@
"failed-to-add-recipes-to-list": "Неуспешно добавяне на рецепта към списъка", "failed-to-add-recipes-to-list": "Неуспешно добавяне на рецепта към списъка",
"failed-to-add-recipe-to-mealplan": "Рецептата не беше добавена към хранителния план", "failed-to-add-recipe-to-mealplan": "Рецептата не беше добавена към хранителния план",
"failed-to-add-to-list": "Неуспешно добавяне към списъка", "failed-to-add-to-list": "Неуспешно добавяне към списъка",
"yield": "Добив", "yield": "Количество",
"yields-amount-with-text": "Yields {amount} {text}", "yields-amount-with-text": "Порции {amount} {text}",
"yield-text": "Yield Text", "yield-text": "Забележка",
"quantity": "Количество", "quantity": "Количество",
"choose-unit": "Избери единица", "choose-unit": "Избери единица",
"press-enter-to-create": "Натисните Enter за да създадете", "press-enter-to-create": "Натисните Enter за да създадете",
@@ -561,10 +561,10 @@
"see-original-text": "Виж оригиналния текст", "see-original-text": "Виж оригиналния текст",
"original-text-with-value": "Оригинален текст: {originalText}", "original-text-with-value": "Оригинален текст: {originalText}",
"ingredient-linker": "Инструмент за свързване на съставки", "ingredient-linker": "Инструмент за свързване на съставки",
"unlinked": "Not linked yet", "unlinked": "Все още не е свързано",
"linked-to-other-step": "Свързано към друга стъпка", "linked-to-other-step": "Свързано към друга стъпка",
"auto": "Автоматично", "auto": "Автоматично",
"cook-mode": "Режим на готвене", "cook-mode": "Начин на приготвяне",
"link-ingredients": "Свържи съставките", "link-ingredients": "Свържи съставките",
"merge-above": "Обедини с по-горната", "merge-above": "Обедини с по-горната",
"move-to-bottom": "Премести най-долу", "move-to-bottom": "Премести най-долу",
@@ -579,18 +579,18 @@
"timeline-is-empty": "Няма история на събитията. Опитайте да приготвите рецептата!", "timeline-is-empty": "Няма история на събитията. Опитайте да приготвите рецептата!",
"timeline-no-events-found-try-adjusting-filters": "Няма намерени събития. Опитайте да промените филтрите си за търсене.", "timeline-no-events-found-try-adjusting-filters": "Няма намерени събития. Опитайте да промените филтрите си за търсене.",
"group-global-timeline": "{groupName} История на събитията", "group-global-timeline": "{groupName} История на събитията",
"open-timeline": "Отвори историята на събитията", "open-timeline": "История на събитията",
"made-this": "Сготвих рецептата", "made-this": "Сготвих рецептата",
"how-did-it-turn-out": "Как се получи?", "how-did-it-turn-out": "Как се получи?",
"user-made-this": "{user} направи това", "user-made-this": "{user} направи това",
"added-to-timeline": "Added to timeline", "added-to-timeline": "Добавено към историята на събитията",
"failed-to-add-to-timeline": "Failed to add to timeline", "failed-to-add-to-timeline": "Неуспешно добавяне към историята на събитията",
"failed-to-update-recipe": "Failed to update recipe", "failed-to-update-recipe": "Неуспешно актуализиране на рецептата",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image", "added-to-timeline-but-failed-to-add-image": "Добавено към хронологията, но добавянето на изображение бе неуспешно",
"api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.", "api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
"message-key": "Ключ на съобщението", "message-key": "Ключ на съобщението",
"parse": "Анализирай", "parse": "Анализирай",
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.", "ingredients-not-parsed-description": "Изглежда, че съставките ви все още не са анализирани. Кликнете върху бутона „{parse}“ по-долу, за да анализирате съставките си в структурирани храни.",
"attach-images-hint": "Прикачете снимки като ги влачете и пуснете в редактора", "attach-images-hint": "Прикачете снимки като ги влачете и пуснете в редактора",
"drop-image": "Премахване на изображение", "drop-image": "Премахване на изображение",
"enable-ingredient-amounts-to-use-this-feature": "Пуснете количествата на съставките за да използвате функционалността", "enable-ingredient-amounts-to-use-this-feature": "Пуснете количествата на съставките за да използвате функционалността",
@@ -601,17 +601,17 @@
"select-one-of-the-various-ways-to-create-a-recipe": "Изберете един от разнообразните начини за създаване на рецепта", "select-one-of-the-various-ways-to-create-a-recipe": "Изберете един от разнообразните начини за създаване на рецепта",
"looking-for-migrations": "Миграция на данни", "looking-for-migrations": "Миграция на данни",
"import-with-url": "Импортирай от линк", "import-with-url": "Импортирай от линк",
"create-recipe": "Добави рецепта", "create-recipe": "Добавяне на рецепта",
"create-recipe-description": "Създайте нова рецепта от чернова.", "create-recipe-description": "Създайте нова рецепта от чернова.",
"create-recipes": "Създайте рецепти", "create-recipes": "Създайте рецепти",
"import-with-zip": "Импортирай от .zip", "import-with-zip": "Импортирай от .zip",
"create-recipe-from-an-image": "Create Recipe from an Image", "create-recipe-from-an-image": "Create Recipe from an Image",
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.", "create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.", "crop-and-rotate-the-image": "Изрежете и завъртете изображението, така че да се вижда само текстът и той да е в правилната ориентация.",
"create-from-images": "Create from Images", "create-from-images": "Създаване от изображения",
"should-translate-description": "Translate the recipe into my language", "should-translate-description": "Преведете рецептата на моя език",
"please-wait-image-procesing": "Please wait, the image is processing. This may take some time.", "please-wait-image-procesing": "Моля, изчакайте, изображението се обработва. Това може да отнеме известно време.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Моля, изчакайте, изображенията се обработват. Това може да отнеме известно време.",
"bulk-url-import": "Импортиране на рецепти от линк", "bulk-url-import": "Импортиране на рецепти от линк",
"debug-scraper": "Отстраняване на грешки на скрейпъра", "debug-scraper": "Отстраняване на грешки на скрейпъра",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Добави рецепта като предоставиш име. Всички рецепти трябва да имат уникални имена.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Добави рецепта като предоставиш име. Всички рецепти трябва да имат уникални имена.",
@@ -620,16 +620,17 @@
"scrape-recipe-description": "Обходи рецепта по линк. Предоставете линк за сайт, който искате да бъде обходен. Mealie ще опита да обходи рецептата от този сайт и да я добави във Вашата колекция.", "scrape-recipe-description": "Обходи рецепта по линк. Предоставете линк за сайт, който искате да бъде обходен. Mealie ще опита да обходи рецептата от този сайт и да я добави във Вашата колекция.",
"scrape-recipe-have-a-lot-of-recipes": "Имате много рецепти, които искате да обходите наведнъж?", "scrape-recipe-have-a-lot-of-recipes": "Имате много рецепти, които искате да обходите наведнъж?",
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане", "scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?", "scrape-recipe-have-raw-html-or-json-data": "Имате ли сурови HTML или JSON данни?",
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "Можете да импортирате директно от сурови данни",
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети", "import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
"stay-in-edit-mode": "Остани в режим на редакция", "stay-in-edit-mode": "Остани в режим на редакция",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Импортирай от Zip", "import-from-zip": "Импортирай от Zip",
"import-from-zip-description": "Импортирай рецепта, която е била експортирана от друга инстанция на Mealie.", "import-from-zip-description": "Импортирай рецепта, която е била експортирана от друга инстанция на Mealie.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Импортиране от HTML или JSON",
"import-from-html-or-json-description": "Import a single recipe from raw HTML or JSON. This is useful if you have a recipe from a site that Mealie can't scrape normally, or from some other external source.", "import-from-html-or-json-description": "Импортирайте една рецепта от суров HTML или JSON. Това е полезно, ако имате рецепта от сайт, който Mealie не може да извлече нормално, или от друг външен източник.",
"json-import-format-description-colon": "To import via JSON, it must be in valid format:", "json-import-format-description-colon": "За да импортирате чрез JSON, той трябва да бъде във валиден формат:",
"json-editor": "JSON Editor", "json-editor": "JSON редактор",
"zip-files-must-have-been-exported-from-mealie": ".zip файловете трябва да бъдат експортирани от Mealie", "zip-files-must-have-been-exported-from-mealie": ".zip файловете трябва да бъдат експортирани от Mealie",
"create-a-recipe-by-uploading-a-scan": "Добави рецепта като качиш сканирано копие.", "create-a-recipe-by-uploading-a-scan": "Добави рецепта като качиш сканирано копие.",
"upload-a-png-image-from-a-recipe-book": "Качи png изображение от книга с рецепти", "upload-a-png-image-from-a-recipe-book": "Качи png изображение от книга с рецепти",
@@ -642,59 +643,65 @@
"report-deletion-failed": "Неуспешно изтриване на доклад", "report-deletion-failed": "Неуспешно изтриване на доклад",
"recipe-debugger": "Debugger на рецепти", "recipe-debugger": "Debugger на рецепти",
"recipe-debugger-description": "Вземете URL на рецептата, която желаете да проверите за грешки и го поставете тук. URL ще бъде обходен и резултатите ще бъдат визуализирани. Ако не виждате върнати данни, сайтът който се опитвате да обходите не се поддържа от Mealie или библиотеката за обхождане.", "recipe-debugger-description": "Вземете URL на рецептата, която желаете да проверите за грешки и го поставете тук. URL ще бъде обходен и резултатите ще бъдат визуализирани. Ако не виждате върнати данни, сайтът който се опитвате да обходите не се поддържа от Mealie или библиотеката за обхождане.",
"use-openai": "Use OpenAI", "use-openai": "Използвайте OpenAI",
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.", "recipe-debugger-use-openai-description": "Използвайте OpenAI за анализиране на резултатите, вместо да разчитате на библиотеката за скрепер. Когато създавате рецепта чрез URL адрес, това се прави автоматично, ако библиотеката за скрепер се провали, но можете да я тествате ръчно тук.",
"debug": "Отстраняване на грешки", "debug": "Отстраняване на грешки",
"tree-view": "Дървовиден изглед", "tree-view": "Дървовиден изглед",
"recipe-servings": "Recipe Servings", "recipe-servings": "Порции рецепта",
"recipe-yield": "Добиване от рецепта", "recipe-yield": "Добиване от рецепта",
"recipe-yield-text": "Recipe Yield Text", "recipe-yield-text": "Текст за порции на рецепта",
"unit": "Единица", "unit": "Единица",
"upload-image": "Качване на изображение", "upload-image": "Качване на изображение",
"screen-awake": "Запази екрана активен", "screen-awake": "Запази екрана активен",
"remove-image": "Премахване на изображение", "remove-image": "Премахване на изображение",
"nextStep": "Следваща стъпка", "nextStep": "Следваща стъпка",
"recipe-actions": "Recipe Actions", "recipe-actions": "Действия с рецепти",
"parser": { "parser": {
"ingredient-parser": "Ingredient Parser", "ingredient-parser": "Анализатор на съставки",
"explanation": "To use the ingredient parser, click the 'Parse All' button to start the process. Once the processed ingredients are available, you can review the items and verify that they were parsed correctly. The model's confidence score is displayed on the right of the item title. This score is an average of all the individual scores and may not always be completely accurate.", "explanation": "За да използвате анализатора на съставките, щракнете върху бутона „Анализ на всички“, за да стартирате процеса. След като обработените съставки са налични, можете да прегледате елементите и да проверите дали са били анализирани правилно. Коефициентът на достоверност на модела се показва отдясно на заглавието на елемента. Този резултат е средна стойност на всички отделни оценки и не винаги може да бъде напълно точен.",
"alerts-explainer": "Alerts will be displayed if a matching foods or unit is found but does not exists in the database.", "alerts-explainer": "Ще се показват предупреждения, ако бъдат намерени съответстващи храни или единици, но не съществуват в базата данни.",
"select-parser": "Select Parser", "select-parser": "Изберете парсер",
"natural-language-processor": "Natural Language Processor", "natural-language-processor": "Процесор за естествен език",
"brute-parser": "Brute Parser", "brute-parser": "Груб анализатор",
"openai-parser": "OpenAI Parser", "openai-parser": "OpenAI парсер",
"parse-all": "Parse All", "parse-all": "Разбор на всички",
"no-unit": "No unit", "no-unit": "Няма зададена мерна единица",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Създаване на липсваща мерна единица: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Създаване на липсваща храна: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "Тази мерна единица не може да бъде анализирана автоматично",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "Тази храна не може да бъде анализирана автоматично",
"no-food": "No Food" "no-food": "Не е зададен вид храна",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Нулиране на броя на порциите",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Допълнителни съставки",
"upload-another-image": "Upload another image", "upload-another-image": "Качете друго изображение",
"upload-images": "Upload images", "upload-images": "Качване на изображения",
"upload-more-images": "Upload more images", "upload-more-images": "Качете още изображения",
"set-as-cover-image": "Set as recipe cover image", "set-as-cover-image": "Задай като изображение на корицата на рецептата",
"cover-image": "Cover image" "cover-image": "Изображение на корицата"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Recipe Finder", "recipe-finder": "Търсачка на рецепти",
"recipe-finder-description": "Search for recipes based on ingredients you have on hand. You can also filter by tools you have available, and set a maximum number of missing ingredients or tools.", "recipe-finder-description": "Търсете рецепти въз основа на съставките, които имате под ръка. Можете също да филтрирате по налични инструменти и да зададете максимален брой липсващи съставки или инструменти.",
"selected-ingredients": "Selected Ingredients", "selected-ingredients": "Избрани съставки",
"no-ingredients-selected": "No ingredients selected", "no-ingredients-selected": "Няма избрани съставки",
"missing": "Missing", "missing": "Липсващ",
"no-recipes-found": "No recipes found", "no-recipes-found": "Няма намерени рецепти",
"no-recipes-found-description": "Try adding more ingredients to your search or adjusting your filters", "no-recipes-found-description": "Опитайте да добавите още съставки към търсенето си или да коригирате филтрите си",
"include-ingredients-on-hand": "Include Ingredients On Hand", "include-ingredients-on-hand": "Включете наличните съставки",
"include-tools-on-hand": "Include Tools On Hand", "include-tools-on-hand": "Включете наличните инструменти",
"max-missing-ingredients": "Max Missing Ingredients", "max-missing-ingredients": "Максимален брой липсващи съставки",
"max-missing-tools": "Max Missing Tools", "max-missing-tools": "Максимален брой липсващи инструменти",
"selected-tools": "Selected Tools", "selected-tools": "Избрани инструменти",
"other-filters": "Other Filters", "other-filters": "Други филтри",
"ready-to-make": "Ready to Make", "ready-to-make": "Готови за приготвяне",
"almost-ready-to-make": "Almost Ready to Make" "almost-ready-to-make": "Почти готови за приготвяне"
}, },
"search": { "search": {
"advanced-search": "Разширено търсене", "advanced-search": "Разширено търсене",
@@ -705,7 +712,7 @@
"or": "Или", "or": "Или",
"has-any": "Има някое", "has-any": "Има някое",
"has-all": "Има всички", "has-all": "Има всички",
"clear-selection": "Clear Selection", "clear-selection": "Изчистване на избора",
"results": "Резултати", "results": "Резултати",
"search": "Търсене", "search": "Търсене",
"search-mealie": "Търсене в Mealie (Натисни /)", "search-mealie": "Търсене в Mealie (Натисни /)",
@@ -721,10 +728,10 @@
"admin-settings": "Административни настройки", "admin-settings": "Административни настройки",
"backup": { "backup": {
"backup-created": "Архивът е създаден успешно", "backup-created": "Архивът е създаден успешно",
"backup-created-at-response-export_path": "Резервно копие е създадено на {path}", "backup-created-at-response-export_path": "Резервно копие е създадено в {path}",
"backup-deleted": "Резервното копие е изтрито", "backup-deleted": "Резервното копие е изтрито",
"restore-success": "Успешно възстановяване", "restore-success": "Успешно възстановяване",
"restore-fail": "Restore failed. Check your server logs for more details", "restore-fail": "Възстановяването не бе успешно. Проверете лог файловете на сървъра си за повече подробности",
"backup-tag": "Етикет на резервното копие", "backup-tag": "Етикет на резервното копие",
"create-heading": "Създай резервно копие", "create-heading": "Създай резервно копие",
"delete-backup": "Изтрий резервно копие", "delete-backup": "Изтрий резервно копие",
@@ -737,7 +744,7 @@
"backup-restore": "Възстановяване на резервно копие", "backup-restore": "Възстановяване на резервно копие",
"back-restore-description": "Възстановяването на това резервно копие ще презапише цялата текуща информация във Вашата база данни и директорията с данни, и ще ги замени със съдържанието от резервното копие. {cannot-be-undone} Ако възстановяването е успешно ще бъдете отписан от системата.", "back-restore-description": "Възстановяването на това резервно копие ще презапише цялата текуща информация във Вашата база данни и директорията с данни, и ще ги замени със съдържанието от резервното копие. {cannot-be-undone} Ако възстановяването е успешно ще бъдете отписан от системата.",
"cannot-be-undone": "Това действие не може да бъде отменено - използвайте с внимание.", "cannot-be-undone": "Това действие не може да бъде отменено - използвайте с внимание.",
"postgresql-note": "If you are using PostgreSQL, please review the {backup-restore-process} prior to restoring.", "postgresql-note": "Ако използвате PostgreSQL, моля, прегледайте {backup-restore-process} преди възстановяване.",
"backup-restore-process-in-the-documentation": "процес за резервно копие/възстановяване в документацията", "backup-restore-process-in-the-documentation": "процес за резервно копие/възстановяване в документацията",
"irreversible-acknowledgment": "Разбирам, че това действие е невъзвращаемо, разрушително и може да доведе до загуба на данни", "irreversible-acknowledgment": "Разбирам, че това действие е невъзвращаемо, разрушително и може да доведе до загуба на данни",
"restore-backup": "Възстановяване на резервно копие" "restore-backup": "Възстановяване на резервно копие"
@@ -841,7 +848,7 @@
"email-configured": "Email е конфигуриран", "email-configured": "Email е конфигуриран",
"email-test-results": "Резултати от тест на email", "email-test-results": "Резултати от тест на email",
"ready": "Готов", "ready": "Готов",
"not-ready": "Не е готово - Проверете променливите на средата", "not-ready": "Не е завършена - Проверете променливите на средата.",
"succeeded": "Успешно", "succeeded": "Успешно",
"failed": "Неуспешно", "failed": "Неуспешно",
"general-about": "Основни настройки", "general-about": "Основни настройки",
@@ -862,9 +869,9 @@
"oidc-ready": "Готов за OIDC", "oidc-ready": "Готов за OIDC",
"oidc-ready-error-text": "Не всички OIDC стойности са конфигурирани. Това може да бъде игнорирано, ако не използвате OIDC удостоверяване.", "oidc-ready-error-text": "Не всички OIDC стойности са конфигурирани. Това може да бъде игнорирано, ако не използвате OIDC удостоверяване.",
"oidc-ready-success-text": "Задължителните OIDC променливи са зададени.", "oidc-ready-success-text": "Задължителните OIDC променливи са зададени.",
"openai-ready": "OpenAI Ready", "openai-ready": "Готов за OpenAI",
"openai-ready-error-text": "Not all OpenAI Values are configured. This can be ignored if you are not using OpenAI features.", "openai-ready-error-text": "Не всички стойности на OpenAI са конфигурирани. Това може да се игнорира, ако не използвате функции на OpenAI.",
"openai-ready-success-text": "Required OpenAI variables are all set." "openai-ready-success-text": "Всички необходими променливи на OpenAI са зададени."
}, },
"shopping-list": { "shopping-list": {
"all-lists": "Всички списъци", "all-lists": "Всички списъци",
@@ -878,7 +885,7 @@
"food": "Продукт", "food": "Продукт",
"note": "Бележка", "note": "Бележка",
"label": "Етикет", "label": "Етикет",
"save-label": "Save Label", "save-label": "Запазване на етикета",
"linked-item-warning": "Елементът е добавен към една или повече рецепти. Редактиране на единиците или храните ще се отрази с непредвидими резултати когато добавяте или премахвате рецепта от списъка.", "linked-item-warning": "Елементът е добавен към една или повече рецепти. Редактиране на единиците или храните ще се отрази с непредвидими резултати когато добавяте или премахвате рецепта от списъка.",
"toggle-food": "Превключване на храна", "toggle-food": "Превключване на храна",
"manage-labels": "Управление на етикети", "manage-labels": "Управление на етикети",
@@ -894,12 +901,12 @@
"items-checked-count": "Няма отбелязани етикети|Един елемент е отбелязан|{count} елементи са отбелязани", "items-checked-count": "Няма отбелязани етикети|Един елемент е отбелязан|{count} елементи са отбелязани",
"no-label": "Няма етикет", "no-label": "Няма етикет",
"completed-on": "Приключена на {date}", "completed-on": "Приключена на {date}",
"you-are-offline": "You are offline", "you-are-offline": "Вие сте в офлайн режим",
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.", "you-are-offline-description": "Не всички функции са достъпни, докато сте офлайн. Все още можете да добавяте, променяте и премахвате елементи, но няма да можете да синхронизирате промените си със сървъра, докато не се свържете отново онлайн.",
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?", "are-you-sure-you-want-to-check-all-items": "Сигурни ли сте, че искате да изберете всички елементи?",
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?", "are-you-sure-you-want-to-uncheck-all-items": "Сигурни ли сте, че искате да премахнете отметката от всички елементи?",
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?", "are-you-sure-you-want-to-delete-checked-items": "Сигурни ли сте, че искате да изтриете всички отметнати елементи?",
"no-shopping-lists-found": "No Shopping Lists Found" "no-shopping-lists-found": "Не са намерени списъци за пазаруване"
}, },
"sidebar": { "sidebar": {
"all-recipes": "Всички рецепти", "all-recipes": "Всички рецепти",
@@ -1043,9 +1050,9 @@
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie", "authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
"permissions": "Права", "permissions": "Права",
"administrator": "Администратор", "administrator": "Администратор",
"user-can-invite-other-to-group": "User can invite others to group", "user-can-invite-other-to-group": "Потребителя може да добавя други в групата",
"user-can-manage-group": "Потребителя може да управлява групата", "user-can-manage-group": "Потребителя може да управлява групата",
"user-can-manage-household": "User can manage household", "user-can-manage-household": "Потребителят може да управлява домакинството",
"user-can-organize-group-data": "Потребителя може да организира данните на групата", "user-can-organize-group-data": "Потребителя може да организира данните на групата",
"enable-advanced-features": "Включване на разширени функции", "enable-advanced-features": "Включване на разширени функции",
"it-looks-like-this-is-your-first-time-logging-in": "Изглежда това е първият път, в който влизате.", "it-looks-like-this-is-your-first-time-logging-in": "Изглежда това е първият път, в който влизате.",
@@ -1076,8 +1083,8 @@
"food-data": "Данни за храните", "food-data": "Данни за храните",
"example-food-singular": "пример: Домат", "example-food-singular": "пример: Домат",
"example-food-plural": "пример: Домати", "example-food-plural": "пример: Домати",
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.", "label-overwrite-warning": "Това ще присвои избрания етикет на всички избрани храни и евентуално ще презапише съществуващите ви етикети.",
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list." "on-hand-checkbox-label": "Задаването на този флаг ще направи тази храна неотметната по подразбиране при добавяне на рецепта към списък за пазаруване."
}, },
"units": { "units": {
"seed-dialog-text": "Заредете базата данни с мерни единици на Вашия местен език.", "seed-dialog-text": "Заредете базата данни с мерни единици на Вашия местен език.",
@@ -1106,7 +1113,7 @@
"edit-label": "Редактиране на етикет", "edit-label": "Редактиране на етикет",
"new-label": "Нов етикет", "new-label": "Нов етикет",
"labels": "Етикети", "labels": "Етикети",
"assign-label": "Assign Label" "assign-label": "Присвояване на етикет"
}, },
"recipes": { "recipes": {
"purge-exports": "Изчистване на експортите", "purge-exports": "Изчистване на експортите",
@@ -1130,10 +1137,10 @@
"source-unit-will-be-deleted": "Изходната мерна единица ще бъде изтрита" "source-unit-will-be-deleted": "Изходната мерна единица ще бъде изтрита"
}, },
"recipe-actions": { "recipe-actions": {
"recipe-actions-data": "Recipe Actions Data", "recipe-actions-data": "Данни за действия с рецепти",
"new-recipe-action": "New Recipe Action", "new-recipe-action": "Ново действие с рецепта",
"edit-recipe-action": "Edit Recipe Action", "edit-recipe-action": "Редактиране на действието с рецепта",
"action-type": "Action Type" "action-type": "Вид действие"
}, },
"create-alias": "Създаване на псевдоним", "create-alias": "Създаване на псевдоним",
"manage-aliases": "Управление на псевдоними", "manage-aliases": "Управление на псевдоними",
@@ -1170,7 +1177,7 @@
"group-details": "Подробности за групата", "group-details": "Подробности за групата",
"group-details-description": "Преди да създадете акаунт, ще трябва да създадете група. Вашата група ще съдържа само Вас, но ще можете да поканите други по-късно. Членовете във вашата група могат да споделят планове за хранене, списъци за пазаруване, рецепти и други!", "group-details-description": "Преди да създадете акаунт, ще трябва да създадете група. Вашата група ще съдържа само Вас, но ще можете да поканите други по-късно. Членовете във вашата група могат да споделят планове за хранене, списъци за пазаруване, рецепти и други!",
"use-seed-data": "Използвай предварителни данни", "use-seed-data": "Използвай предварителни данни",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.", "use-seed-data-description": "Mealie се доставя с колекция от храни, единици и етикети, които могат да се използват за попълване на вашата група с полезни данни за организиране на вашите рецепти. Те са преведени на езика, който сте избрали в момента. Винаги можете да добавяте или променяте тези данни по-късно.",
"account-details": "Подробни данни за акаунта" "account-details": "Подробни данни за акаунта"
}, },
"validation": { "validation": {
@@ -1301,25 +1308,25 @@
"restore-from-v1-backup": "Имате резервно копие от предишна инстанция на Mealie v1? Можете да го възстановите тук.", "restore-from-v1-backup": "Имате резервно копие от предишна инстанция на Mealie v1? Можете да го възстановите тук.",
"manage-profile-or-get-invite-link": "Управлявайте собствения си профил или вземете връзка за покана, която да споделите с други." "manage-profile-or-get-invite-link": "Управлявайте собствения си профил или вземете връзка за покана, която да споделите с други."
}, },
"debug-openai-services": "Debug OpenAI Services", "debug-openai-services": "Отстраняване на грешки в услугите на OpenAI",
"debug-openai-services-description": "Use this page to debug OpenAI services. You can test your OpenAI connection and see the results here. If you have image services enabled, you can also provide an image.", "debug-openai-services-description": "Използвайте тази страница за отстраняване на грешки в услугите на OpenAI. Можете да тествате връзката си с OpenAI и да видите резултатите тук. Ако имате активирани услуги за изображения, можете също да предоставите изображение.",
"run-test": "Run Test", "run-test": "Изпълнение на теста",
"test-results": "Test Results", "test-results": "Резултати от теста",
"group-delete-note": "Групите от потребители или домакинства немогат да бъдат изтривани", "group-delete-note": "Групите от потребители или домакинства немогат да бъдат изтривани",
"household-delete-note": "Домакинства с потребители не могат да бъдат изтривани" "household-delete-note": "Домакинства с потребители не могат да бъдат изтривани"
}, },
"profile": { "profile": {
"welcome-user": "👋 Добре дошъл(а), {0}!", "welcome-user": "👋 Добре дошъл(а), {0}!",
"description": "Настройки на профил, рецепти и настройки на групата.", "description": "Настройки на профил, рецепти и настройки на групата.",
"invite-link": "Invite Link", "invite-link": "Линк за Покана",
"get-invite-link": "Вземи линк за покана", "get-invite-link": "Вземи линк за покана",
"get-public-link": "Вземи публичен линк", "get-public-link": "Вземи публичен линк",
"account-summary": "Обобщение на акаунта", "account-summary": "Обобщение на акаунта",
"account-summary-description": "Обобщение на информацията за Вашата група.", "account-summary-description": "Обобщение на информацията за Вашата група.",
"group-statistics": "Статистики на групата", "group-statistics": "Статистики на групата",
"group-statistics-description": "Вашата статистика на групата дава известна представа как използвате Mealie.", "group-statistics-description": "Вашата статистика на групата дава известна представа как използвате Mealie.",
"household-statistics": "Household Statistics", "household-statistics": "Статистика на домакинствата",
"household-statistics-description": "Your Household Statistics provide some insight how you're using Mealie.", "household-statistics-description": "Статистиката на вашето домакинство дава известна представа за това как използвате Mealie.",
"storage-capacity": "Капацитет за съхранение", "storage-capacity": "Капацитет за съхранение",
"storage-capacity-description": "Вашият капацитет за съхранение е изчисление на изображенията и активите, които сте качили.", "storage-capacity-description": "Вашият капацитет за съхранение е изчисление на изображенията и активите, които сте качили.",
"personal": "Лични", "personal": "Лични",
@@ -1329,13 +1336,13 @@
"api-tokens-description": "Управление на API токени за достъп от външни приложения.", "api-tokens-description": "Управление на API токени за достъп от външни приложения.",
"group-description": "Тези елементи се споделят във вашата група. Редактирането на един от тях ще го промени за цялата група!", "group-description": "Тези елементи се споделят във вашата група. Редактирането на един от тях ще го промени за цялата група!",
"group-settings": "Настройки на групата", "group-settings": "Настройки на групата",
"group-settings-description": "Manage your common group settings, like privacy settings.", "group-settings-description": "Управлявайте общите настройки на групата си, като например настройките за поверителност.",
"household-description": "These items are shared within your household. Editing one of them will change it for the whole household!", "household-description": "Тези елементи се споделят в рамките на вашето домакинство. Редактирането на един от тях ще го промени за цялото домакинство!",
"household-settings": "Household Settings", "household-settings": "Настройки на домакинството",
"household-settings-description": "Manage your household settings, like mealplan and privacy settings.", "household-settings-description": "Управлявайте настройките на домакинството си, като например план за хранене и настройки за поверителност.",
"cookbooks-description": "Управление на категории на рецепти и генериране на съответните страници.", "cookbooks-description": "Управление на категории на рецепти и генериране на съответните страници.",
"members": "Участници", "members": "Участници",
"members-description": "See who's in your household and manage their permissions.", "members-description": "Вижте кой е във вашето домакинство и управлявайте техните разрешения.",
"webhooks-description": "Настройте webhooks, които се задействат в дните, в които имате планиран план за хранене.", "webhooks-description": "Настройте webhooks, които се задействат в дните, в които имате планиран план за хранене.",
"notifiers": "Уведомители", "notifiers": "Уведомители",
"notifiers-description": "Настройте имейл и push известия, които се задействат при конкретни събития.", "notifiers-description": "Настройте имейл и push известия, които се задействат при конкретни събития.",
@@ -1360,9 +1367,9 @@
}, },
"cookbook": { "cookbook": {
"cookbooks": "Готварски книги", "cookbooks": "Готварски книги",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.", "description": "Готварските книги са друг начин за организиране на рецепти чрез създаване на напречни секции от рецепти, органайзери и други филтри. Създаването на готварска книга ще добави запис към страничната лента и всички рецепти с избраните филтри ще бъдат показани в готварската книга.",
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households", "hide-cookbooks-from-other-households": "Скриване на готварски книги от други домакинства",
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar", "hide-cookbooks-from-other-households-description": "Когато е активирано, в страничната лента ще се показват само готварски книги от вашето домакинство",
"public-cookbook": "Публична книга с рецепти", "public-cookbook": "Публична книга с рецепти",
"public-cookbook-description": "Публичните готварски книги могат да се споделят с потребители, които не са в Mealie, и ще се показват на страницата на вашите групи.", "public-cookbook-description": "Публичните готварски книги могат да се споделят с потребители, които не са в Mealie, и ще се показват на страницата на вашите групи.",
"filter-options": "Опции на филтъра", "filter-options": "Опции на филтъра",
@@ -1372,31 +1379,31 @@
"require-all-tools": "Изискване на всички инструменти", "require-all-tools": "Изискване на всички инструменти",
"cookbook-name": "Име на книгата с рецепти", "cookbook-name": "Име на книгата с рецепти",
"cookbook-with-name": "Книга с рецепти {0}", "cookbook-with-name": "Книга с рецепти {0}",
"household-cookbook-name": "{0} Cookbook {1}", "household-cookbook-name": "{0} Готварска книга {1}",
"create-a-cookbook": "Създай Готварска книга", "create-a-cookbook": "Създай Готварска книга",
"cookbook": "Готварска книга" "cookbook": "Готварска книга"
}, },
"query-filter": { "query-filter": {
"logical-operators": { "logical-operators": {
"and": "AND", "and": "И",
"or": "OR" "or": "ИЛИ"
}, },
"relational-operators": { "relational-operators": {
"equals": "equals", "equals": "е равно на",
"does-not-equal": "does not equal", "does-not-equal": "не е равно на",
"is-greater-than": "is greater than", "is-greater-than": "е по-голямо от",
"is-greater-than-or-equal-to": "is greater than or equal to", "is-greater-than-or-equal-to": "е по-голямо от или равно на",
"is-less-than": "is less than", "is-less-than": "е по-малко от",
"is-less-than-or-equal-to": "is less than or equal to" "is-less-than-or-equal-to": "e по-малко или равно на"
}, },
"relational-keywords": { "relational-keywords": {
"is": "is", "is": "е",
"is-not": "is not", "is-not": "не е",
"is-one-of": "is one of", "is-one-of": "е едно от",
"is-not-one-of": "is not one of", "is-not-one-of": "не е едно от",
"contains-all-of": "contains all of", "contains-all-of": "съдържа всички от",
"is-like": "is like", "is-like": "е като",
"is-not-like": "is not like" "is-not-like": "не е като"
} }
} }
} }

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Podeu importar directament des de les dades planes", "scrape-recipe-you-can-import-from-raw-data-directly": "Podeu importar directament des de les dades planes",
"import-original-keywords-as-tags": "Importa les paraules clau originals com a tags", "import-original-keywords-as-tags": "Importa les paraules clau originals com a tags",
"stay-in-edit-mode": "Segueix en el mode d'edició", "stay-in-edit-mode": "Segueix en el mode d'edició",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importa des d'un ZIP", "import-from-zip": "Importa des d'un ZIP",
"import-from-zip-description": "Importa una sola recepta que ha estat importada d'una altra instància de Mealie.", "import-from-zip-description": "Importa una sola recepta que ha estat importada d'una altra instància de Mealie.",
"import-from-html-or-json": "Importar des d'un HTML o JSON", "import-from-html-or-json": "Importar des d'un HTML o JSON",
@@ -669,7 +670,13 @@
"missing-food": "Crear menjar que manca: {food}", "missing-food": "Crear menjar que manca: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Sense menjar" "no-food": "Sense menjar",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reiniciar racions servides", "reset-servings-count": "Reiniciar racions servides",
"not-linked-ingredients": "Ingredients addicionals", "not-linked-ingredients": "Ingredients addicionals",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Můžete importovat přímo ze surových dat", "scrape-recipe-you-can-import-from-raw-data-directly": "Můžete importovat přímo ze surových dat",
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky", "import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
"stay-in-edit-mode": "Zůstat v režimu úprav", "stay-in-edit-mode": "Zůstat v režimu úprav",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importovat ze zipu", "import-from-zip": "Importovat ze zipu",
"import-from-zip-description": "Importovat jeden recept, který byl exportován z jiné instance Mealie.", "import-from-zip-description": "Importovat jeden recept, který byl exportován z jiné instance Mealie.",
"import-from-html-or-json": "Importovat z HTML nebo JSON", "import-from-html-or-json": "Importovat z HTML nebo JSON",
@@ -669,7 +670,13 @@
"missing-food": "Vytvořit chybějící jídlo: {food}", "missing-food": "Vytvořit chybějící jídlo: {food}",
"this-unit-could-not-be-parsed-automatically": "Tuto jednotku nelze analyzovat automaticky", "this-unit-could-not-be-parsed-automatically": "Tuto jednotku nelze analyzovat automaticky",
"this-food-could-not-be-parsed-automatically": "Toto jídlo nelze analyzovat automaticky", "this-food-could-not-be-parsed-automatically": "Toto jídlo nelze analyzovat automaticky",
"no-food": "Žádné jídlo" "no-food": "Žádné jídlo",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Resetovat počet porcí", "reset-servings-count": "Resetovat počet porcí",
"not-linked-ingredients": "Další ingredience", "not-linked-ingredients": "Další ingredience",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere direkte fra rå data", "scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere direkte fra rå data",
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker", "import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
"stay-in-edit-mode": "Bliv i redigeringstilstand", "stay-in-edit-mode": "Bliv i redigeringstilstand",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importer fra zip-fil", "import-from-zip": "Importer fra zip-fil",
"import-from-zip-description": "Importer en enkelt opskrift, der blev eksporteret fra en anden Mealie instans.", "import-from-zip-description": "Importer en enkelt opskrift, der blev eksporteret fra en anden Mealie instans.",
"import-from-html-or-json": "Importer fra HTML eller JSON", "import-from-html-or-json": "Importer fra HTML eller JSON",
@@ -669,7 +670,13 @@
"missing-food": "Opret manglende fødevare: {food}", "missing-food": "Opret manglende fødevare: {food}",
"this-unit-could-not-be-parsed-automatically": "Denne enhed kunne ikke fortolkes automatisk", "this-unit-could-not-be-parsed-automatically": "Denne enhed kunne ikke fortolkes automatisk",
"this-food-could-not-be-parsed-automatically": "Denne fødevare kunne ikke fortolkes automatisk", "this-food-could-not-be-parsed-automatically": "Denne fødevare kunne ikke fortolkes automatisk",
"no-food": "Ingen fødevarer" "no-food": "Ingen fødevarer",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Nulstil antal serveringer", "reset-servings-count": "Nulstil antal serveringer",
"not-linked-ingredients": "Yderligere ingredienser", "not-linked-ingredients": "Yderligere ingredienser",

View File

@@ -217,7 +217,7 @@
"organizers": "Organisieren", "organizers": "Organisieren",
"caution": "Vorsicht", "caution": "Vorsicht",
"show-advanced": "Erweiterte Optionen anzeigen", "show-advanced": "Erweiterte Optionen anzeigen",
"add-field": "Bedingung hinzufügen", "add-field": "Feld Hinzufügen",
"date-created": "Erstellungsdatum", "date-created": "Erstellungsdatum",
"date-updated": "Aktualisiert am" "date-updated": "Aktualisiert am"
}, },
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kannst direkt von Rohdaten importieren", "scrape-recipe-you-can-import-from-raw-data-directly": "Du kannst direkt von Rohdaten importieren",
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter", "import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben", "stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
"import-from-zip": "Von Zip importieren", "import-from-zip": "Von Zip importieren",
"import-from-zip-description": "Importiere ein einzelnes Rezept, das von einer anderen Mealie-Instanz exportiert wurde.", "import-from-zip-description": "Importiere ein einzelnes Rezept, das von einer anderen Mealie-Instanz exportiert wurde.",
"import-from-html-or-json": "Aus HTML oder JSON importieren", "import-from-html-or-json": "Aus HTML oder JSON importieren",
@@ -669,7 +670,13 @@
"missing-food": "Fehlendes Lebensmittel erstellen: {food}", "missing-food": "Fehlendes Lebensmittel erstellen: {food}",
"this-unit-could-not-be-parsed-automatically": "Diese Einheit konnte nicht automatisch analysiert werden", "this-unit-could-not-be-parsed-automatically": "Diese Einheit konnte nicht automatisch analysiert werden",
"this-food-could-not-be-parsed-automatically": "Dieses Lebensmittel konnte nicht automatisch analysiert werden", "this-food-could-not-be-parsed-automatically": "Dieses Lebensmittel konnte nicht automatisch analysiert werden",
"no-food": "Kein Lebensmittel" "no-food": "Kein Lebensmittel",
"review-parsed-ingredients": "Geparste Zutaten überprüfen",
"confidence-score": "Zuverlässigkeitswert",
"ingredient-parser-description": "Deine Zutaten wurden erfolgreich geparst. Bitte überprüfe die Zutaten, bei denen wir uns nicht sicher sind.",
"ingredient-parser-final-review-description": "Sobald alle Zutaten überprüft wurden, kannst du nochmal alle Zutaten kontrollieren, bevor die Änderungen ins Rezept übernommen werden.",
"add-text-as-alias-for-item": "Füge \"{text}\" als Alias für {item} hinzu",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Portionen zurücksetzen", "reset-servings-count": "Portionen zurücksetzen",
"not-linked-ingredients": "Zusätzliche Zutaten", "not-linked-ingredients": "Zusätzliche Zutaten",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Μπορείτε να κάνετε εισαγωγή απευθείας από ακατέργαστα δεδομένα", "scrape-recipe-you-can-import-from-raw-data-directly": "Μπορείτε να κάνετε εισαγωγή απευθείας από ακατέργαστα δεδομένα",
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες", "import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας", "stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
"import-from-zip": "Εισαγωγή μέσω zip", "import-from-zip": "Εισαγωγή μέσω zip",
"import-from-zip-description": "Εισαγωγή μιας μόνο συνταγής που εξάχθηκε από μια άλλη υπόσταση Mealie.", "import-from-zip-description": "Εισαγωγή μιας μόνο συνταγής που εξάχθηκε από μια άλλη υπόσταση Mealie.",
"import-from-html-or-json": "Εισαγωγή από HTML ή JSON", "import-from-html-or-json": "Εισαγωγή από HTML ή JSON",
@@ -669,7 +670,13 @@
"missing-food": "Δημιουργία τροφίμου που λείπει: {food}", "missing-food": "Δημιουργία τροφίμου που λείπει: {food}",
"this-unit-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτής της μονάδας", "this-unit-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτής της μονάδας",
"this-food-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτού του φαγητού", "this-food-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτού του φαγητού",
"no-food": "Χωρίς Τρόφιμο" "no-food": "Χωρίς Τρόφιμο",
"review-parsed-ingredients": "Επανεξέταση αναλυμένων συστατικών",
"confidence-score": "Βαθμολογία εμπιστοσύνης",
"ingredient-parser-description": "Τα συστατικά σας έχουν αναλυθεί επιτυχώς. Παρακαλούμε ελέγξτε τα συστατικά για τα οποία δεν είμαστε σίγουροι.",
"ingredient-parser-final-review-description": "Μόλις εξεταστούν όλα τα συστατικά, θα έχετε μία ακόμη ευκαιρία να επανεξετάσετε όλα τα συστατικά πριν εφαρμόσετε τις αλλαγές στη συνταγή σας.",
"add-text-as-alias-for-item": "Προσθήκη \"{text}\" ως ψευδώνυμο για το {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Επαναφορά μέτρησης μερίδων", "reset-servings-count": "Επαναφορά μέτρησης μερίδων",
"not-linked-ingredients": "Πρόσθετα συστατικά", "not-linked-ingredients": "Πρόσθετα συστατικά",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"import-original-keywords-as-tags": "Import original keywords as tags", "import-original-keywords-as-tags": "Import original keywords as tags",
"stay-in-edit-mode": "Stay in Edit mode", "stay-in-edit-mode": "Stay in Edit mode",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Import from Zip", "import-from-zip": "Import from Zip",
"import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.", "import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Additional Ingredients",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"import-original-keywords-as-tags": "Import original keywords as tags", "import-original-keywords-as-tags": "Import original keywords as tags",
"stay-in-edit-mode": "Stay in Edit mode", "stay-in-edit-mode": "Stay in Edit mode",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Import from Zip", "import-from-zip": "Import from Zip",
"import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.", "import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Additional Ingredients",

View File

@@ -45,7 +45,7 @@
"category-filter": "Filtros de Categorías", "category-filter": "Filtros de Categorías",
"category-update-failed": "Error al actualizar categoría", "category-update-failed": "Error al actualizar categoría",
"category-updated": "Categoría actualizada", "category-updated": "Categoría actualizada",
"uncategorized-count": "{count} no categorizado", "uncategorized-count": "{count} sin categorizar",
"create-a-category": "Crear una categoría", "create-a-category": "Crear una categoría",
"category-name": "Nombre de la categoría", "category-name": "Nombre de la categoría",
"category": "Categoría" "category": "Categoría"
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Puede importar directamente desde datos brutos", "scrape-recipe-you-can-import-from-raw-data-directly": "Puede importar directamente desde datos brutos",
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas", "import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
"stay-in-edit-mode": "Permanecer en modo edición", "stay-in-edit-mode": "Permanecer en modo edición",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importar desde zip", "import-from-zip": "Importar desde zip",
"import-from-zip-description": "Importa una receta única que fue exportada desde otra instancia de Mealie.", "import-from-zip-description": "Importa una receta única que fue exportada desde otra instancia de Mealie.",
"import-from-html-or-json": "Importar desde HTML o JSON", "import-from-html-or-json": "Importar desde HTML o JSON",
@@ -669,7 +670,13 @@
"missing-food": "Crear comida faltante: {food}", "missing-food": "Crear comida faltante: {food}",
"this-unit-could-not-be-parsed-automatically": "Esta unidad no pudo ser procesada automáticamente", "this-unit-could-not-be-parsed-automatically": "Esta unidad no pudo ser procesada automáticamente",
"this-food-could-not-be-parsed-automatically": "Esta comida no pudo ser procesada automáticamente", "this-food-could-not-be-parsed-automatically": "Esta comida no pudo ser procesada automáticamente",
"no-food": "Sin Comida" "no-food": "Sin Comida",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Restablecer contador de porciones", "reset-servings-count": "Restablecer contador de porciones",
"not-linked-ingredients": "Ingredientes adicionales", "not-linked-ingredients": "Ingredientes adicionales",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Sa võid otse importida töötlemata andmetest", "scrape-recipe-you-can-import-from-raw-data-directly": "Sa võid otse importida töötlemata andmetest",
"import-original-keywords-as-tags": "Impordi originaal võtmesõnad siltidena", "import-original-keywords-as-tags": "Impordi originaal võtmesõnad siltidena",
"stay-in-edit-mode": "Püsige redigeerimisrežiimis", "stay-in-edit-mode": "Püsige redigeerimisrežiimis",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Impordi .zip-st", "import-from-zip": "Impordi .zip-st",
"import-from-zip-description": "Impordi üks retsept, mis oli eksporditud teisest Mealie paigaldusest.", "import-from-zip-description": "Impordi üks retsept, mis oli eksporditud teisest Mealie paigaldusest.",
"import-from-html-or-json": "Impordi HTMLst või JSONist", "import-from-html-or-json": "Impordi HTMLst või JSONist",
@@ -669,7 +670,13 @@
"missing-food": "Loo puuduv toit: {food}", "missing-food": "Loo puuduv toit: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Toit puudub" "no-food": "Toit puudub",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Lähtesta portsionite arv", "reset-servings-count": "Lähtesta portsionite arv",
"not-linked-ingredients": "Lisa-koostisosad", "not-linked-ingredients": "Lisa-koostisosad",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Voit tuoda raakadatan suoraan", "scrape-recipe-you-can-import-from-raw-data-directly": "Voit tuoda raakadatan suoraan",
"import-original-keywords-as-tags": "Tuo alkuperäiset avainsanat tunnisteiksi", "import-original-keywords-as-tags": "Tuo alkuperäiset avainsanat tunnisteiksi",
"stay-in-edit-mode": "Pysy muokkaustilassa", "stay-in-edit-mode": "Pysy muokkaustilassa",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Tuo zip-arkistosta", "import-from-zip": "Tuo zip-arkistosta",
"import-from-zip-description": "Tuo yksi resepti, joka on viety toisesta Mealie-asennuksesta.", "import-from-zip-description": "Tuo yksi resepti, joka on viety toisesta Mealie-asennuksesta.",
"import-from-html-or-json": "Tuo HTML- tai JSON-tiedostosta", "import-from-html-or-json": "Tuo HTML- tai JSON-tiedostosta",
@@ -669,7 +670,13 @@
"missing-food": "Luo puuttuva ruoka: {food}", "missing-food": "Luo puuttuva ruoka: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Ei ruokaa" "no-food": "Ei ruokaa",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Palauta Annoksien Määrä", "reset-servings-count": "Palauta Annoksien Määrä",
"not-linked-ingredients": "Muut ainesosat", "not-linked-ingredients": "Muut ainesosat",

View File

@@ -561,7 +561,7 @@
"see-original-text": "Afficher le texte original", "see-original-text": "Afficher le texte original",
"original-text-with-value": "Texte original: {originalText}", "original-text-with-value": "Texte original: {originalText}",
"ingredient-linker": "Liaison dingrédients", "ingredient-linker": "Liaison dingrédients",
"unlinked": "Not linked yet", "unlinked": "Pas encore associée",
"linked-to-other-step": "Déjà associé à une autre étape", "linked-to-other-step": "Déjà associé à une autre étape",
"auto": "Auto", "auto": "Auto",
"cook-mode": "Mode Cuisine", "cook-mode": "Mode Cuisine",
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes", "scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags", "import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"stay-in-edit-mode": "Rester en mode édition", "stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip", "import-from-zip": "Importer depuis un zip",
"import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.", "import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.",
"import-from-html-or-json": "Importer depuis HTML ou JSON", "import-from-html-or-json": "Importer depuis HTML ou JSON",
@@ -669,7 +670,13 @@
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement", "this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement",
"this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement", "this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement",
"no-food": "Aucun aliment" "no-food": "Aucun aliment",
"review-parsed-ingredients": "Vérifier les ingrédients analysés",
"confidence-score": "Score de confiance",
"ingredient-parser-description": "Vos ingrédients ont été analysés avec succès. Veuillez vérifier les ingrédients dont nous ne sommes pas certains.",
"ingredient-parser-final-review-description": "Une fois que tous les ingrédients ont été analysés, vous aurez encore une chance de vérifier tous les ingrédients avant de les appliquer à votre recette.",
"add-text-as-alias-for-item": "Ajouter \"{text}\" comme alias pour {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",

View File

@@ -561,7 +561,7 @@
"see-original-text": "Afficher le texte original", "see-original-text": "Afficher le texte original",
"original-text-with-value": "Texte original: {originalText}", "original-text-with-value": "Texte original: {originalText}",
"ingredient-linker": "Association dingrédients", "ingredient-linker": "Association dingrédients",
"unlinked": "Not linked yet", "unlinked": "Pas encore associée",
"linked-to-other-step": "Lié à une autre étape", "linked-to-other-step": "Lié à une autre étape",
"auto": "Auto", "auto": "Auto",
"cook-mode": "Mode Cuisine", "cook-mode": "Mode Cuisine",
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes", "scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags", "import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"stay-in-edit-mode": "Rester en mode édition", "stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip", "import-from-zip": "Importer depuis un zip",
"import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.", "import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.",
"import-from-html-or-json": "Importer depuis HTML ou JSON", "import-from-html-or-json": "Importer depuis HTML ou JSON",
@@ -669,7 +670,13 @@
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement", "this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement",
"this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement", "this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement",
"no-food": "Aucun aliment" "no-food": "Aucun aliment",
"review-parsed-ingredients": "Vérifier les ingrédients analysés",
"confidence-score": "Score de confiance",
"ingredient-parser-description": "Vos ingrédients ont été analysés avec succès. Veuillez vérifier les ingrédients dont nous ne sommes pas certains.",
"ingredient-parser-final-review-description": "Une fois que tous les ingrédients ont été analysés, vous aurez encore une chance de vérifier tous les ingrédients avant de les appliquer à votre recette.",
"add-text-as-alias-for-item": "Ajouter \"{text}\" comme alias pour {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",
@@ -1170,7 +1177,7 @@
"group-details": "Détails du groupe", "group-details": "Détails du groupe",
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter dautres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes dachat, leurs recettes et plus encore!", "group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter dautres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes dachat, leurs recettes et plus encore!",
"use-seed-data": "Utiliser l'initialisation de données", "use-seed-data": "Utiliser l'initialisation de données",
"use-seed-data-description": "Mealie est livrée avec une collection d'aliments, d'unités et d'étiquettes qui peuvent être utilisés pour remplir votre groupe avec des données utiles pour organiser vos recettes. Ceux-ci sont traduits dans la langue que vous avez sélectionnée. Vous pouvez toujours ajouter ou modifier ces données plus tard.", "use-seed-data-description": "Mealie est livrée avec une collection d'aliments, d'unités et d'étiquettes qui peuvent être utilisés pour peupler votre groupe avec des données utiles pour organiser vos recettes. Ceux-ci sont traduits dans la langue que vous avez sélectionnée. Vous pouvez toujours ajouter ou modifier ces données plus tard.",
"account-details": "Détails du compte" "account-details": "Détails du compte"
}, },
"validation": { "validation": {

View File

@@ -23,7 +23,7 @@
"support": "Soutenir", "support": "Soutenir",
"version": "Version", "version": "Version",
"unknown-version": "inconnu", "unknown-version": "inconnu",
"sponsor": "Sponsoriser" "sponsor": "Soutenir"
}, },
"asset": { "asset": {
"assets": "Ressources", "assets": "Ressources",
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes", "scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags", "import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"stay-in-edit-mode": "Rester en mode édition", "stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip", "import-from-zip": "Importer depuis un zip",
"import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.", "import-from-zip-description": "Importer une recette qui a été exportée depuis une autre instance de Mealie.",
"import-from-html-or-json": "Importer depuis HTML ou JSON", "import-from-html-or-json": "Importer depuis HTML ou JSON",
@@ -669,7 +670,13 @@
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement", "this-unit-could-not-be-parsed-automatically": "Cette unité n'a pas pu être analysée automatiquement",
"this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement", "this-food-could-not-be-parsed-automatically": "Cet aliment n'a pas pu être analysé automatiquement",
"no-food": "Aucun aliment" "no-food": "Aucun aliment",
"review-parsed-ingredients": "Vérifier les ingrédients analysés",
"confidence-score": "Score de confiance",
"ingredient-parser-description": "Vos ingrédients ont été analysés avec succès. Veuillez vérifier les ingrédients dont nous ne sommes pas certains.",
"ingredient-parser-final-review-description": "Une fois que tous les ingrédients ont été analysés, vous aurez encore une chance de vérifier tous les ingrédients avant de les appliquer à votre recette.",
"add-text-as-alias-for-item": "Ajouter \"{text}\" comme alias pour {item}",
"delete-item": "Supprimer l'élément"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "É posível importar diretamente a partir de datos en bruto", "scrape-recipe-you-can-import-from-raw-data-directly": "É posível importar diretamente a partir de datos en bruto",
"import-original-keywords-as-tags": "Importar palavras-chave orixinais como etiquetas", "import-original-keywords-as-tags": "Importar palavras-chave orixinais como etiquetas",
"stay-in-edit-mode": "Permanecer no modo de edición", "stay-in-edit-mode": "Permanecer no modo de edición",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importar de Zip", "import-from-zip": "Importar de Zip",
"import-from-zip-description": "Importar unha única receita exportada de outra instancia Mealie.", "import-from-zip-description": "Importar unha única receita exportada de outra instancia Mealie.",
"import-from-html-or-json": "Importar a partir de HTML ou JSON", "import-from-html-or-json": "Importar a partir de HTML ou JSON",
@@ -669,7 +670,13 @@
"missing-food": "Crear a comida que falta: {food}", "missing-food": "Crear a comida que falta: {food}",
"this-unit-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente esta unidade", "this-unit-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente esta unidade",
"this-food-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente este alimento", "this-food-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente este alimento",
"no-food": "Sen Comida" "no-food": "Sen Comida",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reiniciar Contador de Porcións", "reset-servings-count": "Reiniciar Contador de Porcións",
"not-linked-ingredients": "Ingredientes Adicionais", "not-linked-ingredients": "Ingredientes Adicionais",

View File

@@ -69,7 +69,7 @@
"new-notification": "התראה חדשה", "new-notification": "התראה חדשה",
"event-notifiers": "מנגנוני התרעה על אירועים", "event-notifiers": "מנגנוני התרעה על אירועים",
"apprise-url-skipped-if-blank": "כתובת Apprise (דלג אם ריק)", "apprise-url-skipped-if-blank": "כתובת Apprise (דלג אם ריק)",
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.", "apprise-url-is-left-intentionally-blank": "מאחר שכתובות URL של Apprise לרוב מכילות מידע רגיש, שזה זה נותר ריק במכוון בעת העריכה. אם ברצונך לעדכן את כתובת ה URL, אנא הזן כאן את ה-URL החדש, אחרת השאר אותו ריק כדי לשמור את כתובת ה-URL הנוכחית.",
"enable-notifier": "הפעלת מתריע", "enable-notifier": "הפעלת מתריע",
"what-events": "לאילו אירועים לרשום את מתריע זה?", "what-events": "לאילו אירועים לרשום את מתריע זה?",
"user-events": "אירועי משתמש", "user-events": "אירועי משתמש",
@@ -81,7 +81,7 @@
"category-events": "אירועי קטגוריות", "category-events": "אירועי קטגוריות",
"when-a-new-user-joins-your-group": "כאשר משתמש חדש מצטרף לקבוצה", "when-a-new-user-joins-your-group": "כאשר משתמש חדש מצטרף לקבוצה",
"recipe-events": "אירועי מתכון", "recipe-events": "אירועי מתכון",
"label-events": "Label Events" "label-events": "הוסף תוויות לאירועים"
}, },
"general": { "general": {
"add": "הוספה", "add": "הוספה",
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי", "scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי",
"import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות", "import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות",
"stay-in-edit-mode": "השאר במצב עריכה", "stay-in-edit-mode": "השאר במצב עריכה",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "ייבא מקובץ", "import-from-zip": "ייבא מקובץ",
"import-from-zip-description": "ייבוא מתכון בודד שיוצא ממילי אחרת.", "import-from-zip-description": "ייבוא מתכון בודד שיוצא ממילי אחרת.",
"import-from-html-or-json": "ייבוא מ-HTML או JSON", "import-from-html-or-json": "ייבוא מ-HTML או JSON",
@@ -669,7 +670,13 @@
"missing-food": "יצירת אוכל חסר: {food}", "missing-food": "יצירת אוכל חסר: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "אין אוכל" "no-food": "אין אוכל",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "איפוס מספר המנות", "reset-servings-count": "איפוס מספר המנות",
"not-linked-ingredients": "מרכיבים נוספים", "not-linked-ingredients": "מרכיבים נוספים",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"import-original-keywords-as-tags": "Uvezi originalne ključne riječi kao oznake", "import-original-keywords-as-tags": "Uvezi originalne ključne riječi kao oznake",
"stay-in-edit-mode": "Ostanite u načinu uređivanja", "stay-in-edit-mode": "Ostanite u načinu uređivanja",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Uvoz iz Zip-a", "import-from-zip": "Uvoz iz Zip-a",
"import-from-zip-description": "Uvezi pojedinačni recept koji je izvezen iz druge instance Mealie aplikacije.", "import-from-zip-description": "Uvezi pojedinačni recept koji je izvezen iz druge instance Mealie aplikacije.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Additional Ingredients",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "A nyers adatokból közvetlenül is importálhat", "scrape-recipe-you-can-import-from-raw-data-directly": "A nyers adatokból közvetlenül is importálhat",
"import-original-keywords-as-tags": "Eredeti kulcsszavak importálása címkeként", "import-original-keywords-as-tags": "Eredeti kulcsszavak importálása címkeként",
"stay-in-edit-mode": "Maradjon Szerkesztés módban", "stay-in-edit-mode": "Maradjon Szerkesztés módban",
"parse-recipe-ingredients-after-import": "Recept összetevőinek elemzése importálás után",
"import-from-zip": "Importálás ZIP-ből", "import-from-zip": "Importálás ZIP-ből",
"import-from-zip-description": "Egy másik Mealie-példányból kiexportált recept egyedi importálása.", "import-from-zip-description": "Egy másik Mealie-példányból kiexportált recept egyedi importálása.",
"import-from-html-or-json": "Importálás HTML vagy JSON fájlból", "import-from-html-or-json": "Importálás HTML vagy JSON fájlból",
@@ -669,7 +670,13 @@
"missing-food": "Hiányzó élelmiszer létrehozása: {food}", "missing-food": "Hiányzó élelmiszer létrehozása: {food}",
"this-unit-could-not-be-parsed-automatically": "Ez az egység nem tudja automatikusan értelmezni", "this-unit-could-not-be-parsed-automatically": "Ez az egység nem tudja automatikusan értelmezni",
"this-food-could-not-be-parsed-automatically": "Ezt az ételt nem lehetett automatikusan feldolgozni", "this-food-could-not-be-parsed-automatically": "Ezt az ételt nem lehetett automatikusan feldolgozni",
"no-food": "Élelmiszer nélküli" "no-food": "Élelmiszer nélküli",
"review-parsed-ingredients": "Kinyert hozzávalók ellenőrzése",
"confidence-score": "Bizonyossági érték",
"ingredient-parser-description": "Sikeresen feldolgoztuk a hozzávalókat. Kérljük, nézze át azokat, amelyekben nem vagyunk teljesen biztosak.",
"ingredient-parser-final-review-description": "Miután az összes hozzávalót átnézte, még egyszer átnézheti az összes hozzávalót, mielőtt a változtatásokat átvezeti a receptbe.",
"add-text-as-alias-for-item": "Adja hozzá a „{text}” nevet {item} aliasaként",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Adagok számának visszaállítása", "reset-servings-count": "Adagok számának visszaállítása",
"not-linked-ingredients": "Kiegészítő hozzávalók", "not-linked-ingredients": "Kiegészítő hozzávalók",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly", "scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"import-original-keywords-as-tags": "Import original keywords as tags", "import-original-keywords-as-tags": "Import original keywords as tags",
"stay-in-edit-mode": "Stay in Edit mode", "stay-in-edit-mode": "Stay in Edit mode",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Import from Zip", "import-from-zip": "Import from Zip",
"import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.", "import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.",
"import-from-html-or-json": "Import from HTML or JSON", "import-from-html-or-json": "Import from HTML or JSON",
@@ -669,7 +670,13 @@
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients", "not-linked-ingredients": "Additional Ingredients",
@@ -874,7 +881,7 @@
"new-list": "New List", "new-list": "New List",
"quantity": "Quantity: {0}", "quantity": "Quantity: {0}",
"shopping-list": "Shopping List", "shopping-list": "Shopping List",
"shopping-lists": "Shopping Lists", "shopping-lists": "Innkaupalisti",
"food": "Food", "food": "Food",
"note": "Note", "note": "Note",
"label": "Label", "label": "Label",
@@ -1359,7 +1366,7 @@
"manage-data-migrations": "Manage Data Migrations" "manage-data-migrations": "Manage Data Migrations"
}, },
"cookbook": { "cookbook": {
"cookbooks": "Cookbooks", "cookbooks": "Uppskriftabók",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.", "description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households", "hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar", "hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",

View File

@@ -561,7 +561,7 @@
"see-original-text": "Vedi Testo Originale", "see-original-text": "Vedi Testo Originale",
"original-text-with-value": "Testo originale: {originalText}", "original-text-with-value": "Testo originale: {originalText}",
"ingredient-linker": "Linker degli Ingredienti", "ingredient-linker": "Linker degli Ingredienti",
"unlinked": "Not linked yet", "unlinked": "Non ancora collegato",
"linked-to-other-step": "Collegato ad un altro passaggio", "linked-to-other-step": "Collegato ad un altro passaggio",
"auto": "Automatico", "auto": "Automatico",
"cook-mode": "Modalità di Cottura", "cook-mode": "Modalità di Cottura",
@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "È possibile importare direttamente dai dati grezzi", "scrape-recipe-you-can-import-from-raw-data-directly": "È possibile importare direttamente dai dati grezzi",
"import-original-keywords-as-tags": "Importa parole chiave originali come tag", "import-original-keywords-as-tags": "Importa parole chiave originali come tag",
"stay-in-edit-mode": "Rimani in modalità Modifica", "stay-in-edit-mode": "Rimani in modalità Modifica",
"parse-recipe-ingredients-after-import": "Analizza gli ingredienti della ricetta dopo l'importazione",
"import-from-zip": "Importa da Zip", "import-from-zip": "Importa da Zip",
"import-from-zip-description": "Importa una singola ricetta esportata da un'altra istanza di Mealie.", "import-from-zip-description": "Importa una singola ricetta esportata da un'altra istanza di Mealie.",
"import-from-html-or-json": "Importa da HTML o JSON", "import-from-html-or-json": "Importa da HTML o JSON",
@@ -669,7 +670,13 @@
"missing-food": "Crea cibo mancante: {food}", "missing-food": "Crea cibo mancante: {food}",
"this-unit-could-not-be-parsed-automatically": "Questa unità non può essere analizzata automaticamente", "this-unit-could-not-be-parsed-automatically": "Questa unità non può essere analizzata automaticamente",
"this-food-could-not-be-parsed-automatically": "Questo alimento non può essere analizzato automaticamente", "this-food-could-not-be-parsed-automatically": "Questo alimento non può essere analizzato automaticamente",
"no-food": "Nessun Alimento" "no-food": "Nessun Alimento",
"review-parsed-ingredients": "Controlla ingredienti analizzati",
"confidence-score": "Punteggio Di Confidenza",
"ingredient-parser-description": "I tuoi ingredienti sono stati analizzati con successo. Controlla gli ingredienti di cui non siamo sicuri.",
"ingredient-parser-final-review-description": "Una volta che tutti gli ingredienti sono stati controllati, avrai ancora una possibilità di rivederli tutti prima di applicare le modifiche alla ricetta.",
"add-text-as-alias-for-item": "Aggiungi \"{text}\" come alias per {item}",
"delete-item": "Elimina elemento"
}, },
"reset-servings-count": "Reimposta conteggio porzioni", "reset-servings-count": "Reimposta conteggio porzioni",
"not-linked-ingredients": "Ingredienti Aggiuntivi", "not-linked-ingredients": "Ingredienti Aggiuntivi",

View File

@@ -624,6 +624,7 @@
"scrape-recipe-you-can-import-from-raw-data-directly": "生データから直接インポートできます", "scrape-recipe-you-can-import-from-raw-data-directly": "生データから直接インポートできます",
"import-original-keywords-as-tags": "元のキーワードをタグとしてインポート", "import-original-keywords-as-tags": "元のキーワードをタグとしてインポート",
"stay-in-edit-mode": "編集モードを維持", "stay-in-edit-mode": "編集モードを維持",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Zipからインポート", "import-from-zip": "Zipからインポート",
"import-from-zip-description": "別のMealieインスタンスからエクスポートされた1つのレシピをインポートします。", "import-from-zip-description": "別のMealieインスタンスからエクスポートされた1つのレシピをインポートします。",
"import-from-html-or-json": "HTML または JSON からインポート", "import-from-html-or-json": "HTML または JSON からインポート",
@@ -669,7 +670,13 @@
"missing-food": "欠けている食材を作成: {food}", "missing-food": "欠けている食材を作成: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically", "this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically", "this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "食材はありません" "no-food": "食材はありません",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
}, },
"reset-servings-count": "サービング数をリセット", "reset-servings-count": "サービング数をリセット",
"not-linked-ingredients": "追加の材料", "not-linked-ingredients": "追加の材料",

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