mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-23 07:40:15 +00:00
Compare commits
190 Commits
v3.15.2
...
mealie-next
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e9d4e1799 | |||
| 836b5f499b | |||
| 6b96d8126e | |||
| aaab52326c | |||
| 6c124a2dbd | |||
| 6458ecd447 | |||
| 9ea0991d53 | |||
| 62bb771758 | |||
| 0c04aa701f | |||
| fcba5216bc | |||
| 13fd74057c | |||
| 92735196ef | |||
| c1176b323d | |||
| dd94e1de77 | |||
| 4a099c168e | |||
| a166f127a2 | |||
| 8556fb752c | |||
| c2af718c29 | |||
| 9f7a968607 | |||
| 544071f3e7 | |||
| ecb6caf71b | |||
| f40bf3849e | |||
| 629d41d38a | |||
| e4a6274810 | |||
| e1fed11bb0 | |||
| d125c88da6 | |||
| aa2192a78f | |||
| 8f48887267 | |||
| 7cc50145ec | |||
| fe9fb77316 | |||
| 204bd2aeb3 | |||
| b8d5375478 | |||
| 4289860ffd | |||
| a1c724fac4 | |||
| ac0bb4fb2c | |||
| 040ec56c18 | |||
| f025bbce57 | |||
| 47c6d01617 | |||
| 653be9a604 | |||
| 2d8b74282a | |||
| 48752bcd06 | |||
| a46620d236 | |||
| 3bde6df958 | |||
| e1ddc06eff | |||
| 262b531add | |||
| 364af97060 | |||
| 7b0d1fde64 | |||
| 0af9633193 | |||
| b5987f5a46 | |||
| e24187fefb | |||
| 396fcd5ee4 | |||
| 5a3d202879 | |||
| 62377ae7ad | |||
| 7498e22278 | |||
| af6c9e074e | |||
| 71dba654b8 | |||
| ba69fcf824 | |||
| 8219ac0168 | |||
| 47f66676e4 | |||
| 31d9479d17 | |||
| 6a8eae7ce4 | |||
| 3bddfc21ce | |||
| 975a16c74b | |||
| 840da0e935 | |||
| 0e22f3f8fa | |||
| ff67fb6a4f | |||
| 97f37d0def | |||
| 37171d174b | |||
| f010c13661 | |||
| 84622af5f8 | |||
| 024dad6663 | |||
| f1998121aa | |||
| 94ca311616 | |||
| 44c4bbb9ab | |||
| 0c263c98c9 | |||
| c235dc8d4d | |||
| 1b7eda0f2c | |||
| f3725b7184 | |||
| 00a4b51ec1 | |||
| 2cf042fce9 | |||
| 55a8fdfee5 | |||
| 1ab5323f34 | |||
| fb4ba490af | |||
| d70978cd8b | |||
| 3b2bcca639 | |||
| 16163a9189 | |||
| 5ce448af7a | |||
| c3f87736d0 | |||
| f6fe92b400 | |||
| 823b938a2c | |||
| 8eb00c3dc0 | |||
| 642c826f2b | |||
| 493154caa8 | |||
| 71e0d99a46 | |||
| c52a4e10c9 | |||
| 8b9149a1ce | |||
| c8ff75c02a | |||
| 3d6ff52358 | |||
| f04b0c741c | |||
| 742b498c1d | |||
| eddb0c30e0 | |||
| 1cebfd56ab | |||
| 074ec7aab2 | |||
| af75c5f39d | |||
| 703db2931f | |||
| 52399547d6 | |||
| be4ff86c57 | |||
| 8a054b1be8 | |||
| 2dbfc7f72b | |||
| e492da67e2 | |||
| 811be08996 | |||
| fdd17182d8 | |||
| d340fdd9df | |||
| 551a92a031 | |||
| 8c06f49b02 | |||
| 9fd3fbca8b | |||
| a242aea9f2 | |||
| 6e9ad5fef1 | |||
| ee181a598b | |||
| 3a84b3f262 | |||
| a616e14bf9 | |||
| b902d2cd98 | |||
| 565736e116 | |||
| 7f29efc0e4 | |||
| 743c15a981 | |||
| 3be9193590 | |||
| c880c0865b | |||
| 294238f183 | |||
| 985b656d3f | |||
| 09c2a0b2ad | |||
| f2b087730e | |||
| e71b31e9cc | |||
| 41a9a1e018 | |||
| 7b2372edfc | |||
| 65f109dee4 | |||
| 8dc85640e1 | |||
| 6c5f1c2413 | |||
| bc3ae3c6c0 | |||
| 5b37eb012c | |||
| f354f12853 | |||
| 062484dec9 | |||
| b5d991c516 | |||
| b60aeed8dc | |||
| 1f42ba4934 | |||
| 32e0404564 | |||
| a1a26b23c4 | |||
| e66c0dea58 | |||
| fb5b028b92 | |||
| 2854449213 | |||
| 1bd423d741 | |||
| a754693787 | |||
| 176587079f | |||
| 718d232517 | |||
| 072f93d02d | |||
| 70749b740a | |||
| 60daa3b4e2 | |||
| 2da78b4fb5 | |||
| a5daaaab9e | |||
| ad77b9851f | |||
| 3db94d876c | |||
| 39529ed606 | |||
| b3fd2ccb33 | |||
| fce0538671 | |||
| b3ce0faf26 | |||
| 870b793d5f | |||
| 26dfeac956 | |||
| cfb60228f7 | |||
| c9a0cac055 | |||
| 83bc2f3889 | |||
| 0ffc1e7bf7 | |||
| e166baa33c | |||
| 3e25005ea6 | |||
| c92ebf2099 | |||
| 8e429834af | |||
| 2ca5694391 | |||
| 8d8987ab05 | |||
| 372474ea2b | |||
| 5b93129368 | |||
| ffeb4dceaf | |||
| 5fc4851ef5 | |||
| d9e933d5ae | |||
| 0a07835338 | |||
| 7a85ea6ae9 | |||
| c4c60f1645 | |||
| 9f7ba8dc08 | |||
| c4799ceb9e | |||
| 828be095a2 | |||
| 18718fb647 | |||
| fb545962dd | |||
| 781a08ef54 |
@@ -44,6 +44,7 @@
|
||||
8000, // used by mkdocs
|
||||
9000,
|
||||
9091, // used by docker production
|
||||
51204, // used for test coverage report
|
||||
24678 // used by nuxt when hot-reloading using polling
|
||||
],
|
||||
// Use 'onCreateCommand' to run commands at the end of container creation.
|
||||
|
||||
@@ -20,6 +20,10 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Install from the committed lockfile; never re-resolve (see pyproject
|
||||
# [tool.uv] exclude-newer cooling window).
|
||||
UV_FROZEN: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ permissions:
|
||||
jobs:
|
||||
sync-locales:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Install from the committed lockfile; never re-resolve (see pyproject
|
||||
# [tool.uv] exclude-newer cooling window).
|
||||
UV_FROZEN: "1"
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Pull Request Linter
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [edited] # This captures the PR title changing
|
||||
types: [edited, reopened] # This captures the PR title/body changing
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
@@ -41,3 +41,50 @@ jobs:
|
||||
ignoreLabels: |
|
||||
bot
|
||||
ignore-semantic-pull-request
|
||||
|
||||
validate-template:
|
||||
name: Validate PR template
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check required PR template sections
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
if (pr.user.type === "Bot") {
|
||||
console.log("Skipping template check for bot");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: ".github/pull_request_template.md",
|
||||
});
|
||||
|
||||
const template = Buffer.from(response.data.content, "base64").toString("utf8");
|
||||
const lines = template.split("\n");
|
||||
|
||||
const requiredHeadings = [];
|
||||
let lastHeading = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
lastHeading = line.trim();
|
||||
} else if (line.trim() === "_(REQUIRED)_" && lastHeading) {
|
||||
requiredHeadings.push(lastHeading);
|
||||
lastHeading = null;
|
||||
}
|
||||
}
|
||||
|
||||
const body = pr.body || "";
|
||||
const missing = requiredHeadings.filter(h => !body.includes(h));
|
||||
|
||||
if (missing.length > 0) {
|
||||
core.setFailed(`Missing headings:\n${missing.join("\n")}`);
|
||||
} else {
|
||||
console.log("All required headings present");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ jobs:
|
||||
name: "Lint PR"
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/workflows/pull-request-lint.yml
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
|
||||
@@ -13,6 +13,10 @@ jobs:
|
||||
|
||||
env:
|
||||
PRODUCTION: false
|
||||
# Install from the committed lockfile; never re-resolve. The rolling
|
||||
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
|
||||
# every uv command re-resolve and fail on in-window pins.
|
||||
UV_FROZEN: "1"
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Test Block": {
|
||||
"prefix": "mtest",
|
||||
"body": [
|
||||
"import { mount } from \"@vue/test-utils\";",
|
||||
"import { describe, expect, test, vi } from \"vitest\";",
|
||||
"import { makeWrapper } from \"~/tests/utils\";",
|
||||
"",
|
||||
"const wrapper = () => makeWrapper(() => {",
|
||||
" return ${1:composable}();",
|
||||
"});",
|
||||
"",
|
||||
"describe(\"${TM_FILENAME_BASE/(.*)\\..+$/$1/}\", () => {",
|
||||
" describe(\"${2:method}\", () => {",
|
||||
" test(\"It does the thing\", () => {",
|
||||
" const { ${2:method} } = wrapper();",
|
||||
" const result = ${2:method}();",
|
||||
" expect(result).toBe(EXPECTED);",
|
||||
" });",
|
||||
" });",
|
||||
"});",
|
||||
"",
|
||||
],
|
||||
"description": "Insert a test block",
|
||||
"scope": "typescript",
|
||||
"include": [
|
||||
"**/*.test.{ts,tsx,vue}"
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -6,4 +6,6 @@ Since this software is still considered beta/WIP support is always only given fo
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For general security vulnerabilities you're welcome to open a GitHub issues or contribute a fix. If you feel the vulnerability should not be disclosed you can open a generic issue on GitHub and email to the details to [ob92oy0sl@mozmail.com](mailto:ob92oy0sl@mozmail.com) which is monitored by the maintainer.
|
||||
This repository has [private vulnerability reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/privately-reporting-a-security-vulnerability) enabled. To confidentially report a security issue, click the **"Report a vulnerability"** button on the [Security tab](../../security/advisories/new) of this repository. This allows you to submit details directly to the maintainers without public disclosure.
|
||||
|
||||
For non-sensitive issues or general feedback, feel free to open a GitHub issue or contribute a fix via pull request.
|
||||
|
||||
@@ -7,6 +7,10 @@ env:
|
||||
DEFAULT_GROUP: Home
|
||||
DEFAULT_HOUSEHOLD: Family
|
||||
PRODUCTION: false
|
||||
# Install from the committed lockfile; never re-resolve. Required because the
|
||||
# rolling `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise
|
||||
# make every `uv run`/`uv sync` re-resolve and fail on in-window pins.
|
||||
UV_FROZEN: "1"
|
||||
API_PORT: 9000
|
||||
API_DOCS: True
|
||||
TOKEN_TIME: 256 # hours
|
||||
|
||||
+6
-1
@@ -1,7 +1,7 @@
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:24@sha256:80fc934952c8f1b2b4d39907af7211f8a9fff1a4c2cf673fb49099292c251cec \
|
||||
FROM node:24@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea \
|
||||
AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
@@ -52,6 +52,11 @@ RUN apt-get update \
|
||||
|
||||
RUN pip install uv
|
||||
|
||||
# Install from the committed lockfile; never re-resolve. The rolling
|
||||
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
|
||||
# `uv export` below re-resolve and fail on in-window pins.
|
||||
ENV UV_FROZEN=1
|
||||
|
||||
WORKDIR /mealie
|
||||
|
||||
# copy project files here to ensure they will be cached.
|
||||
|
||||
@@ -58,9 +58,6 @@ load_secrets() {
|
||||
"OIDC_CONFIGURATION_URL"
|
||||
"OIDC_CLIENT_ID"
|
||||
"OIDC_CLIENT_SECRET"
|
||||
|
||||
"OPENAI_BASE_URL"
|
||||
"OPENAI_API_KEY"
|
||||
)
|
||||
|
||||
# If any secrets are set, prefer them over base environment variables.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
!!! info
|
||||
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/)
|
||||
If adding via images make sure to enable [Mealie's AI Integration](https://docs.mealie.io/documentation/getting-started/installation/ai-providers)
|
||||
|
||||
## 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.
|
||||
@@ -23,7 +23,7 @@ An API key is needed to authenticate with mealie. To create an api key for a use
|
||||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -42,6 +42,10 @@ Before you can start using OIDC Authentication, you must first configure a new c
|
||||
http://localhost:9091/login
|
||||
https://mealie.example.com/login
|
||||
|
||||
If you are hosting Mealie behind a reverse proxy (nginx, Caddy, ...) to terminate TLS, make sure to start Mealie's Gunicorn server
|
||||
with `--forwarded-allow-ips=<ip-of-proxy>`, otherwise the `X-Forwarded-*` headers will be ignored and the generated OIDC redirect
|
||||
URI will use the wrong scheme (http instead of https). This will lead to authentication errors with strict OIDC providers.
|
||||
|
||||
3. Configure allowed scopes
|
||||
|
||||
The scopes required are `openid profile email`
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
6. Click the Edit button/icon again
|
||||
7. Scroll to the ingredients and you should see new fields for Amount, Unit, Food, and Note. The Note in particular will contain the original text of the Recipe.
|
||||
8. Click `Parse` and you will be taken to the ingredient parsing page.
|
||||
9. Choose your parser. The `Natural Language Parser` works very well, but you can also use the `Brute Parser`, or the `OpenAI Parser` if you've [enabled OpenAI support](./installation/backend-config.md#openai).
|
||||
9. Choose your parser. The `Natural Language Parser` works very well, but you can also use the `Brute Parser`, or the `OpenAI Parser` if you've [enabled AI support](./installation/ai-providers.md).
|
||||
10. Click `Parse All`, and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food was not found, you can click a button to accept an automatically suggested Food to add to the database. Or, manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
12. When done, click `Save All` and you will be taken back to the recipe. Now the Unit and Food fields of the recipe should be filled out.
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
|
||||
### Creating Recipes
|
||||
Mealie offers several ways to create recipes:
|
||||
|
||||
- **Recipe Scraper:** Create recipes from hundreds of websites by simply providing a URL.
|
||||
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR to import it.
|
||||
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and parse the recipe.
|
||||
- **Recipe HTML or JSON:** Copy/paste structured HTML or JSON and Mealie can import it.
|
||||
- **Manual Editor:** Create recipes from scratch using the integrated editor.
|
||||
|
||||
Mealie's [AI integration](./installation/ai-providers.md) greatly expands the ways you can create recipes:
|
||||
|
||||
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR and AI to import it.
|
||||
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and turn it into a recipe.
|
||||
|
||||
[Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# AI Integration
|
||||
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie's AI integration enables several features and enhancements throughout the application. To enable AI features, you must have access to an AI provider (such as OpenAI). Mealie works with any OpenAI-compatible API.
|
||||
|
||||
## Configuration
|
||||
|
||||
To set up AI providers, visit your group settings.
|
||||
|
||||
[Group Settings Demo](https://demo.mealie.io/group){ .md-button .md-button--primary }
|
||||
|
||||
- To enable AI features at all, you *must* set a default provider (e.g. `gpt-5`)
|
||||
- To enable image recognition features, such as creating a recipe from an image, configure a provider capable of image recognition (e.g. `gpt-5`)
|
||||
- To enable audio transcription features, such as importing a recipe from a video, configure a provider capable of audio transcriptions (e.g. `whisper-1`)
|
||||
|
||||
For most users, choosing an OpenAI model (such as `gpt-5`) and supplying the OpenAI API key is all you need to do. Note that while OpenAI has a free tier, it's not sufficiently capable for Mealie (or most other production use cases). For more information, check out [OpenAI's rate limits](https://platform.openai.com/docs/guides/rate-limits). If you deposit $5 into your OpenAI account, you will be permanently bumped up to Tier 1, which is sufficient for Mealie. Cost per-request is dependant on many factors, but Mealie tries to keep token counts conservative.
|
||||
|
||||
If you have another provider you'd like to use, such as Azure, you can configure Mealie to use that instead as long as it has an OpenAI-compatible API. For instance, a common self-hosted alternative to OpenAI is [Ollama](https://ollama.com/). To use Ollama with Mealie, set your `base_url` to `http://localhost:11434/v1` (where `http://localhost:11434` is wherever you're hosting Ollama, and `/v1` enables the OpenAI-compatible endpoints). Note that you *must* provide an API key, even though it is ultimately ignored by Ollama.
|
||||
|
||||
Note that some models are capable of handling multiple features (e.g. `gpt-5` can handle both normal chat requests and image recognition requests). You may configure one provider for multiple provider features.
|
||||
|
||||
While Mealie has prompts for each AI task, you can override these with your own prompts if you'd like. For more information, check out the [backend configuration](../installation/backend-config.md).
|
||||
|
||||
## AI Features
|
||||
- The OpenAI Ingredient Parser can be used as an alternative to the NLP and Brute Force parsers. Simply choose the OpenAI parser while parsing ingredients (:octicons-tag-24: v1.7.0)
|
||||
- When importing a recipe via URL, if the default recipe scraper is unable to read the recipe data from a webpage, the webpage contents will be parsed by OpenAI (:octicons-tag-24: v1.9.0)
|
||||
- You can import an image of a written recipe, which is sent to OpenAI and imported into Mealie. The recipe can be hand-written or typed, as long as the text is in the picture. You can also optionally have OpenAI translate the recipe into your own language (:octicons-tag-24: v1.12.0)
|
||||
- You can import a recipe via a video URL (e.g., a YouTube link). The video is transcribed AI, and the transcription is parsed into a recipe (:octicons-tag-24: v3.13.0)
|
||||
@@ -10,7 +10,7 @@
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| 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 and the OIDC callback url |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid. Must be <= 9600 (400 days, in hours). |
|
||||
| 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 |
|
||||
@@ -29,6 +29,7 @@
|
||||
| --------------------------- | :-----: | ----------------------------------------------------------------------------------- |
|
||||
| SECURITY_MAX_LOGIN_ATTEMPTS | 5 | Maximum times a user can provide an invalid password before their account is locked |
|
||||
| SECURITY_USER_LOCKOUT_TIME | 24 | Time in hours for how long a users account is locked |
|
||||
| ALLOWED_IFRAME_HOSTS | `""` | Comma-separated extra hostnames allowed as `<iframe>` sources in recipe content. Extends the built-in list of trusted video providers (YouTube, Vimeo). Subdomains are included automatically. Only `https` sources are permitted. Adding hosts here opts into rendering embeds from those origins to all viewers, including the public, so add only origins you trust. |
|
||||
|
||||
### Database
|
||||
|
||||
@@ -114,27 +115,16 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
||||
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
|
||||
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| OIDC_CLIENT_TIMEOUT | default | Configures the timeout value of the httpx client used for OIDC communications. If set to the string `default`, does not configure the value (uses the library's default of 5.0s). If set to the string `None`, disables the timeout entirely. If set to a numeric value, uses that as the timeout. |
|
||||
|
||||
### OpenAI
|
||||
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
|
||||
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
|
||||
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./ai-providers.md).
|
||||
|
||||
| Variables | Default | Description |
|
||||
|-------------------------------------------------------------------------|:-----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_AUDIO_MODEL <br/> :octicons-tag-24: v3.13.0 | whisper-1 | Which OpenAI model to use for audio transcriptions, if enabled. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES <br/> :octicons-tag-24: v1.12.0 | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_ENABLE_TRANSCRIPTION_SERVICES <br/> :octicons-tag-24: v3.13.0 | True | Whether to enable OpenAI transcription services, such as creating recipes via video URL. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| OPENAI_CUSTOM_PROMPT_DIR <br/> :octicons-tag-24: v3.10.0 | None. | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
|
||||
|
||||
### Theming
|
||||
@@ -314,7 +304,6 @@ at least these sensitive environment variables when working within shared enviro
|
||||
- `POSTGRES_PASSWORD`
|
||||
- `SMTP_PASSWORD`
|
||||
- `LDAP_QUERY_PASSWORD`
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
[docker-secrets]: https://docs.docker.com/compose/use-secrets/
|
||||
[secrets]: #docker-secrets
|
||||
|
||||
@@ -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:
|
||||
|
||||
1. Take a backup just in case!
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.15.2`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.19.2`
|
||||
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
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# OpenAI Integration
|
||||
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie's OpenAI integration enables several features and enhancements throughout the application. To enable OpenAI features, you must have an account with OpenAI and configure Mealie to use the OpenAI API key (for more information, check out the [backend configuration](./backend-config.md#openai)).
|
||||
|
||||
## Configuration
|
||||
|
||||
For most users, supplying the OpenAI API key is all you need to do; you will use the regular OpenAI service with the default language model. Note that while OpenAI has a free tier, it's not sufficiently capable for Mealie (or most other production use cases). For more information, check out [OpenAI's rate limits](https://platform.openai.com/docs/guides/rate-limits). If you deposit $5 into your OpenAI account, you will be permanently bumped up to Tier 1, which is sufficient for Mealie. Cost per-request is dependant on many factors, but Mealie tries to keep token counts conservative.
|
||||
|
||||
Alternatively, if you have another service you'd like to use in-place of OpenAI, you can configure Mealie to use that instead, as long as it has an OpenAI-compatible API. For instance, a common self-hosted alternative to OpenAI is [Ollama](https://ollama.com/). To use Ollama with Mealie, change your `OPENAI_BASE_URL` to `http://localhost:11434/v1` (where `http://localhost:11434` is wherever you're hosting Ollama, and `/v1` enables the OpenAI-compatible endpoints). Note that you *must* provide an API key, even though it is ultimately ignored by Ollama.
|
||||
|
||||
If you wish to disable image recognition features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_IMAGE_SERVICES` to `False`.
|
||||
If you wish to disable transcription features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_TRANSCRIPTION_SERVICES` to `False`.
|
||||
|
||||
For more information on what configuration options are available, check out the [backend configuration](./backend-config.md#openai).
|
||||
|
||||
|
||||
|
||||
## OpenAI Features
|
||||
- The OpenAI Ingredient Parser can be used as an alternative to the NLP and Brute Force parsers. Simply choose the OpenAI parser while parsing ingredients (:octicons-tag-24: v1.7.0)
|
||||
- When importing a recipe via URL, if the default recipe scraper is unable to read the recipe data from a webpage, the webpage contents will be parsed by OpenAI (:octicons-tag-24: v1.9.0)
|
||||
- You can import an image of a written recipe, which is sent to OpenAI and imported into Mealie. The recipe can be hand-written or typed, as long as the text is in the picture. You can also optionally have OpenAI translate the recipe into your own language (:octicons-tag-24: v1.12.0)
|
||||
- You can import a recipe via a video URL (e.g., a YouTube link). The video is transcribed using OpenAI's Whisper model, and the transcription is parsed into a recipe (:octicons-tag-24: v3.13.0)
|
||||
@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.19.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.19.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ theme:
|
||||
custom_dir: docs/overrides
|
||||
features:
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- navigation.expand
|
||||
@@ -75,7 +76,7 @@ nav:
|
||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||
- Security: "documentation/getting-started/installation/security.md"
|
||||
- Logs: "documentation/getting-started/installation/logs.md"
|
||||
- OpenAI: "documentation/getting-started/installation/open-ai.md"
|
||||
- AI Providers: "documentation/getting-started/installation/ai-providers.md"
|
||||
- Usage:
|
||||
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
|
||||
- Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md"
|
||||
|
||||
@@ -20,16 +20,12 @@
|
||||
max-width: 1100px !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-application {
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
.v-theme--dark.v-application {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-navigation-drawer {
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-card {
|
||||
background-color: #1e1e1e !important;
|
||||
.v-theme--dark .v-navigation-drawer {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.left-border {
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>AI providers can now be configured directly in Mealie, without managing environment variables or secrets.</p>
|
||||
<div class="mb-2">
|
||||
AI providers enable features such as:
|
||||
<ul class="ml-6">
|
||||
<li>Creating recipes from images</li>
|
||||
<li>Importing recipes from videos (YouTube, TikTok, etc.)</li>
|
||||
<li>Enhanced ingredient parsing</li>
|
||||
<li>And more!</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr class="mt-2 mb-4">
|
||||
<p>
|
||||
<span v-if="group?.aiProviderSettings?.aiEnabled">
|
||||
Your group already has AI providers configured.
|
||||
</span>
|
||||
<span v-else>
|
||||
Your group does not currently have any AI providers configured.
|
||||
</span>
|
||||
<span v-if="user?.canManage">
|
||||
You can manage them here:
|
||||
<br>
|
||||
<v-btn class="mt-2" color="primary" to="/group">
|
||||
{{ $t("profile.group-settings") }}
|
||||
</v-btn>
|
||||
</span>
|
||||
<span v-else-if="!group?.aiProviderSettings?.aiEnabled">
|
||||
Contact a group manager or server admin to set up AI providers for your group.
|
||||
</span>
|
||||
</p>
|
||||
<div v-if="user?.admin">
|
||||
<br>
|
||||
<p>
|
||||
As an admin, you can configure AI providers for any group. Unlike the old environment variable approach, providers are configured per-group:
|
||||
<br>
|
||||
<v-btn class="mt-2" color="primary" to="/admin/manage/groups">
|
||||
{{ $t("group.admin-group-management") }}
|
||||
</v-btn>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import type { AnnouncementMeta } from "~/composables/use-announcements";
|
||||
|
||||
const { user } = useMealieAuth();
|
||||
const { group } = useGroupSelf();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const meta: AnnouncementMeta = {
|
||||
title: "Improved AI Provider Configuration",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
To harden Mealie against malicious content, <code><iframe></code> embeds in recipe
|
||||
instructions, notes, and descriptions are now restricted to a trusted set of hosts.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
By default, embeds are allowed only from well-known video providers:
|
||||
<ul class="ml-6">
|
||||
<li>YouTube</li>
|
||||
<li>Vimeo</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Existing recipes that embed content from <strong>other</strong> hosts will no longer render
|
||||
those embeds. The rest of the recipe is unaffected.
|
||||
</p>
|
||||
<div v-if="user?.admin">
|
||||
<hr class="mt-2 mb-4">
|
||||
<p>
|
||||
As an admin, you can allow additional hosts with the <code>ALLOWED_IFRAME_HOSTS</code>
|
||||
environment variable (comma-separated). It extends the built-in defaults, and only
|
||||
<code>https</code> sources are permitted. See the configuration docs for details:
|
||||
<br>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
color="primary"
|
||||
href="https://docs.mealie.io/documentation/getting-started/installation/backend-config/"
|
||||
target="_blank"
|
||||
>
|
||||
Backend Configuration
|
||||
</v-btn>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AnnouncementMeta } from "~/composables/use-announcements";
|
||||
|
||||
const { user } = useMealieAuth();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const meta: AnnouncementMeta = {
|
||||
title: "Recipe embeds restricted to trusted hosts",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
:title="isEdit ? $t('group.ai-provider-settings.edit-provider') : $t('group.ai-provider-settings.create-provider')"
|
||||
:icon="$globals.icons.robot"
|
||||
:loading="loading"
|
||||
can-submit
|
||||
:submit-icon="isEdit ? $globals.icons.save : $globals.icons.createAlt"
|
||||
:submit-text="isEdit ? $t('general.update') : $t('general.create')"
|
||||
:submit-disabled="submitDisabled"
|
||||
@submit="handleSubmit"
|
||||
@close="resetForm"
|
||||
>
|
||||
<v-card-text v-if="init" style="max-height: 70vh; overflow-y: auto;">
|
||||
<v-form ref="form" v-no-autofill>
|
||||
<v-text-field
|
||||
v-model="formData.name"
|
||||
:label="$t('group.ai-provider-settings.provider-name')"
|
||||
:rules="[validators.required]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="formData.model"
|
||||
:label="$t('group.ai-provider-settings.model')"
|
||||
:hint="$t('group.ai-provider-settings.model-description')"
|
||||
:rules="[validators.required]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="formData.apiKey"
|
||||
:label="$t('group.ai-provider-settings.api-key')"
|
||||
:hint="$t(
|
||||
isEdit
|
||||
? 'group.ai-provider-settings.api-key-description-edit'
|
||||
: 'group.ai-provider-settings.api-key-description-create',
|
||||
)"
|
||||
:persistent-hint="isEdit"
|
||||
:rules="isEdit ? [] : [validators.required]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="formData.baseUrl"
|
||||
:label="$t('group.ai-provider-settings.base-url')"
|
||||
:hint="$t('group.ai-provider-settings.base-url-description')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-number-input
|
||||
v-model.number="formData.timeout"
|
||||
:label="$t('group.ai-provider-settings.request-timeout-seconds')"
|
||||
type="number"
|
||||
:min="0"
|
||||
hide-details
|
||||
control-variant="stacked"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-expansion-panels v-model="advancedPanel" variant="accordion">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title class="text-subtitle-2" expand-icon="$expand" collapse-icon="$expand">
|
||||
{{ $t('search.advanced') }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text class="px-0">
|
||||
<div class="mb-2 text-subtitle-2">
|
||||
{{ $t('group.ai-provider-settings.request-headers') }}
|
||||
</div>
|
||||
<BaseKeyValueEditor
|
||||
v-model="formData.requestHeaders"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-divider class="mb-4" />
|
||||
<div class="mb-2 text-subtitle-2">
|
||||
{{ $t('group.ai-provider-settings.request-params') }}
|
||||
</div>
|
||||
<BaseKeyValueEditor
|
||||
v-model="formData.requestParams"
|
||||
/>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<AppLoader v-else waiting-text="" />
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAIProviders } from "~/composables/use-ai-providers";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
providerId?: string;
|
||||
}>(), {
|
||||
providerId: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "create", data: AIProviderCreate): void;
|
||||
(e: "update", id: string, data: AIProviderUpdate): void;
|
||||
}>();
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
const { loading, getOne } = useAIProviders();
|
||||
const init = ref(false);
|
||||
|
||||
const form = ref();
|
||||
const advancedPanel = ref<number | undefined>(undefined);
|
||||
|
||||
const isEdit = computed(() => !!props.providerId);
|
||||
|
||||
const defaultForm = () => ({
|
||||
name: "",
|
||||
model: "",
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
timeout: 300,
|
||||
requestHeaders: {} as Record<string, string>,
|
||||
requestParams: {} as Record<string, string>,
|
||||
});
|
||||
|
||||
const formData = reactive(defaultForm());
|
||||
|
||||
const submitDisabled = computed(() => {
|
||||
return !formData.name?.trim() || !formData.model?.trim() || (!isEdit.value && !formData.apiKey?.trim());
|
||||
});
|
||||
|
||||
// Fetch existing provider when editing; reset form for create mode
|
||||
watch(
|
||||
() => [dialog.value, props.providerId] as const,
|
||||
async ([open, id]) => {
|
||||
if (!open) return;
|
||||
if (!id) {
|
||||
// Create mode — just show the empty form
|
||||
resetForm();
|
||||
init.value = true;
|
||||
return;
|
||||
}
|
||||
init.value = false;
|
||||
const { data } = await getOne(id);
|
||||
init.value = true;
|
||||
if (data) {
|
||||
formData.name = data.name;
|
||||
formData.model = data.model;
|
||||
formData.apiKey = "";
|
||||
formData.baseUrl = data.baseUrl ?? "";
|
||||
formData.timeout = data.timeout ?? 300;
|
||||
formData.requestHeaders = { ...(data.requestHeaders ?? {}) };
|
||||
formData.requestParams = { ...(data.requestParams ?? {}) };
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function handleSubmit() {
|
||||
// Required field guard (button is also disabled, but keep as a safeguard)
|
||||
if (!formData.name?.trim() || !formData.model?.trim()) return;
|
||||
if (!isEdit.value && !formData.apiKey?.trim()) return;
|
||||
|
||||
if (isEdit.value && props.providerId) {
|
||||
const payload: AIProviderUpdate & { apiKey?: string } = {
|
||||
name: formData.name,
|
||||
model: formData.model,
|
||||
baseUrl: formData.baseUrl || null,
|
||||
timeout: formData.timeout,
|
||||
requestHeaders: Object.keys(formData.requestHeaders).length ? formData.requestHeaders : undefined,
|
||||
requestParams: Object.keys(formData.requestParams).length ? formData.requestParams : undefined,
|
||||
};
|
||||
if (formData.apiKey) {
|
||||
payload.apiKey = formData.apiKey;
|
||||
}
|
||||
emit("update", props.providerId, payload);
|
||||
}
|
||||
else {
|
||||
const createPayload = {
|
||||
name: formData.name,
|
||||
model: formData.model,
|
||||
apiKey: formData.apiKey,
|
||||
baseUrl: formData.baseUrl || null,
|
||||
timeout: formData.timeout,
|
||||
requestHeaders: Object.keys(formData.requestHeaders).length ? formData.requestHeaders : undefined,
|
||||
requestParams: Object.keys(formData.requestParams).length ? formData.requestParams : undefined,
|
||||
};
|
||||
emit("create", createPayload as AIProviderCreate);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, defaultForm());
|
||||
form.value?.reset();
|
||||
advancedPanel.value = undefined;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div v-if="providerSettings">
|
||||
<BaseCardSectionTitle v-if="!hideHeader" :title="$t('group.ai-provider-settings.ai-provider-settings')">
|
||||
<template v-if="noDefaultProviderWarning" #append-title>
|
||||
<v-tooltip location="bottom" color="warning">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" size="small" color="warning" class="ms-2">
|
||||
{{ $globals.icons.alert }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ $t('group.ai-provider-settings.no-default-provider-warning') }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</BaseCardSectionTitle>
|
||||
<v-card-text v-if="!hideHeader" class="pt-0 pb-10 px-0">
|
||||
{{ $t("group.ai-provider-settings.ai-provider-settings-description") }}
|
||||
</v-card-text>
|
||||
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="local.defaultProviderId"
|
||||
:label="$t('group.ai-provider-settings.default-provider')"
|
||||
:items="local.providers"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
/>
|
||||
<v-card-subtitle class="mt-1">
|
||||
{{ $t("group.ai-provider-settings.default-provider-description") }}
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="local.audioProviderId"
|
||||
:label="$t('group.ai-provider-settings.audio-provider')"
|
||||
:items="local.providers"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
/>
|
||||
<v-card-subtitle class="mt-1">
|
||||
{{ $t("group.ai-provider-settings.audio-provider-description") }}
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="local.imageProviderId"
|
||||
:label="$t('group.ai-provider-settings.image-provider')"
|
||||
:items="local.providers"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
/>
|
||||
<v-card-subtitle class="mt-1">
|
||||
{{ $t("group.ai-provider-settings.image-provider-description") }}
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<GroupAIProviderDialog
|
||||
v-model="dialogOpen"
|
||||
:provider-id="editingProviderId ?? undefined"
|
||||
@create="(data) => $emit('create', data)"
|
||||
@update="(id, data) => $emit('update', id, data)"
|
||||
/>
|
||||
|
||||
<BaseCardSectionTitle
|
||||
:title="$t('group.ai-provider-settings.providers')"
|
||||
size="medium"
|
||||
class="pt-2"
|
||||
>
|
||||
<template #append-title>
|
||||
<BaseButton
|
||||
:text="$t('group.ai-provider-settings.create-provider')"
|
||||
class="ms-auto my-2"
|
||||
create
|
||||
small
|
||||
@click="openCreate"
|
||||
/>
|
||||
</template>
|
||||
</BaseCardSectionTitle>
|
||||
|
||||
<v-card
|
||||
v-for="provider in local.providers"
|
||||
:key="provider.id"
|
||||
variant="tonal"
|
||||
class="pa-0 mb-4"
|
||||
>
|
||||
<v-row no-gutters>
|
||||
<v-col :cols="10">
|
||||
<v-card-text>
|
||||
{{ provider.name }}
|
||||
</v-card-text>
|
||||
</v-col>
|
||||
|
||||
<v-col :cols="2">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.edit,
|
||||
text: $t('general.edit'),
|
||||
event: 'edit',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]"
|
||||
@edit="openEdit(provider.id)"
|
||||
@delete="$emit('delete', provider.id)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
import type { AIProviderSettingsOut } from "~/lib/api/types/user";
|
||||
|
||||
const providerSettings = defineModel<AIProviderSettingsOut>({ required: true });
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
hideHeader?: boolean;
|
||||
}>(), {
|
||||
hideHeader: false,
|
||||
});
|
||||
|
||||
const { hideHeader } = toRefs(props);
|
||||
|
||||
const local = reactive({ ...providerSettings.value });
|
||||
watch(local, (newVal) => { providerSettings.value = { ...newVal }; });
|
||||
// Sync back when the parent refreshes after create/update/delete
|
||||
watch(providerSettings, (newVal) => { if (newVal) Object.assign(local, newVal); });
|
||||
|
||||
const noDefaultProviderWarning = computed(
|
||||
() => local.providers.length > 0 && !local.defaultProviderId,
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: "create", data: AIProviderCreate): void;
|
||||
(e: "update", id: string, data: AIProviderUpdate): void;
|
||||
(e: "delete", id: string): void;
|
||||
}>();
|
||||
|
||||
const dialogOpen = ref(false);
|
||||
const editingProviderId = ref<string | null>(null);
|
||||
|
||||
function openCreate() {
|
||||
editingProviderId.value = null;
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(id: string) {
|
||||
editingProviderId.value = id;
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
</script>
|
||||
@@ -59,9 +59,10 @@
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
<p v-if="deleteTarget" class="mt-4 ml-4">
|
||||
<p v-if="deleteTarget" class="mt-4 mb-0 font-weight-bold">
|
||||
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
|
||||
</p>
|
||||
<slot name="delete-dialog-bottom" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -88,6 +89,7 @@
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-card>
|
||||
<slot name="delete-dialog-bottom" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -151,7 +153,7 @@ const createDialog = defineModel("createDialog", { type: Boolean, default: false
|
||||
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
|
||||
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -185,6 +187,10 @@ defineProps({
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
onDeleteDialogOpen: {
|
||||
type: Function as PropType<(items: any[]) => Promise<void>>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
@@ -212,8 +218,11 @@ const editEventHandler = (item: any) => {
|
||||
const deleteTarget = ref<any>(null);
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
function deleteEventHandler(item: any) {
|
||||
async function deleteEventHandler(item: any) {
|
||||
deleteTarget.value = item;
|
||||
if (props.onDeleteDialogOpen) {
|
||||
await props.onDeleteDialogOpen([item]);
|
||||
}
|
||||
deleteDialog.value = true;
|
||||
}
|
||||
|
||||
@@ -222,8 +231,11 @@ function deleteEventHandler(item: any) {
|
||||
const bulkDeleteTarget = ref<Array<any>>([]);
|
||||
const bulkDeleteDialog = ref(false);
|
||||
|
||||
function bulkDeleteEventHandler(items: Array<any>) {
|
||||
async function bulkDeleteEventHandler(items: Array<any>) {
|
||||
bulkDeleteTarget.value = items;
|
||||
if (props.onDeleteDialogOpen) {
|
||||
await props.onDeleteDialogOpen(items);
|
||||
}
|
||||
bulkDeleteDialog.value = true;
|
||||
console.log("Bulk Delete Event Handler", items);
|
||||
}
|
||||
|
||||
@@ -41,19 +41,14 @@
|
||||
>
|
||||
<v-select
|
||||
v-if="index"
|
||||
:model-value="field.logicalOperator"
|
||||
:model-value="field.logicalOperator?.value"
|
||||
:items="[logOps.AND, logOps.OR]"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- left parenthesis -->
|
||||
@@ -67,14 +62,9 @@
|
||||
:model-value="field.leftParenthesis"
|
||||
:items="['', '(', '((', '(((']"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setLeftParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- field name -->
|
||||
@@ -84,19 +74,14 @@
|
||||
:class="config.col.class"
|
||||
>
|
||||
<v-select
|
||||
chips
|
||||
:model-value="field.label"
|
||||
:items="fieldDefs"
|
||||
variant="underlined"
|
||||
item-title="label"
|
||||
item-value="label"
|
||||
class="text-center"
|
||||
@update:model-value="setField(index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- relational operator -->
|
||||
@@ -107,19 +92,14 @@
|
||||
>
|
||||
<v-select
|
||||
v-if="field.type !== 'boolean'"
|
||||
:model-value="field.relationalOperatorValue"
|
||||
:model-value="field.relationalOperatorValue?.value"
|
||||
:items="field.relationalOperatorChoices"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- field value -->
|
||||
@@ -275,14 +255,9 @@
|
||||
:model-value="field.rightParenthesis"
|
||||
:items="['', ')', '))', ')))']"
|
||||
variant="underlined"
|
||||
class="text-center"
|
||||
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<span :class="config.select.textClass" style="width: 100%;">
|
||||
{{ item.raw }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- field actions -->
|
||||
@@ -723,9 +698,6 @@ const config = computed(() => {
|
||||
col: {
|
||||
class: "d-flex justify-center align-end py-0",
|
||||
},
|
||||
select: {
|
||||
textClass: "d-flex justify-center text-center",
|
||||
},
|
||||
items: {
|
||||
icon: {
|
||||
cols: (_index: number) => 2,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
import { truncateText as truncatePlainText } from "~/lib/sanitize/text";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
|
||||
@@ -50,10 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
defineEmits(["item-selected"]);
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
const content = node.textContent || "";
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
return truncatePlainText(text, length, clamp);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
density="compact"
|
||||
:label="$t('recipe.recipe-name')"
|
||||
autofocus
|
||||
@keyup.enter="duplicateRecipe()"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:items-per-page="15"
|
||||
class="elevation-0"
|
||||
:loading="loading"
|
||||
:search="search"
|
||||
return-object
|
||||
>
|
||||
<template #[`item.name`]="{ item }">
|
||||
@@ -86,6 +87,7 @@ interface Props {
|
||||
loading?: boolean;
|
||||
recipes?: Recipe[];
|
||||
showHeaders?: ShowHeaders;
|
||||
search?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
|
||||
@@ -321,7 +321,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
const householdsWithFood = subIng.food?.householdsWithIngredientFood || [];
|
||||
ownIngs.push({
|
||||
checked: !householdsWithFood.includes(currentHouseholdSlug.value),
|
||||
ingredient: { ...subIng, quantity: (ing.quantity || 1) * (subIng.quantity || 1) },
|
||||
ingredient: subIng,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<BaseDialog
|
||||
v-model="dialogDeleteImage"
|
||||
:title="$t('recipe.delete-image')"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
color="error"
|
||||
can-delete
|
||||
@delete="deleteImage"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-image-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
offset-y
|
||||
@@ -37,18 +49,6 @@
|
||||
delete
|
||||
@click="dialogDeleteImage = true"
|
||||
/>
|
||||
<BaseDialog
|
||||
v-model="dialogDeleteImage"
|
||||
:title="$t('recipe.delete-image')"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
color="error"
|
||||
can-delete
|
||||
@delete="deleteImage"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-image-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="mt-n5">
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #append-item>
|
||||
<div class="px-2">
|
||||
<div v-if="showCreateUnit" class="px-2">
|
||||
<BaseButton
|
||||
block
|
||||
size="small"
|
||||
@@ -147,7 +147,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #append-item>
|
||||
<div class="px-2">
|
||||
<div v-if="showCreateFood" class="px-2">
|
||||
<BaseButton
|
||||
block
|
||||
size="small"
|
||||
@@ -344,6 +344,11 @@ const foodData = useFoodData();
|
||||
const foodAutocomplete = ref<HTMLInputElement>();
|
||||
const { search: foodSearch, filtered: filteredFoods } = useSearch(foodStore.store);
|
||||
|
||||
const showCreateFood = computed(() =>
|
||||
!!foodSearch.value
|
||||
&& !filteredFoods.value.some((f: any) => (f.name ?? "").toLowerCase() === foodSearch.value.toLowerCase()),
|
||||
);
|
||||
|
||||
async function createAssignFood() {
|
||||
foodData.data.name = foodSearch.value;
|
||||
model.value.food = await foodStore.actions.createOne(foodData.data) || undefined;
|
||||
@@ -376,6 +381,11 @@ const unitsData = useUnitData();
|
||||
const unitAutocomplete = ref<HTMLInputElement>();
|
||||
const { search: unitSearch, filtered: filteredUnits } = useSearch(unitStore.store);
|
||||
|
||||
const showCreateUnit = computed(() =>
|
||||
!!unitSearch.value
|
||||
&& !filteredUnits.value.some((u: any) => (u.name ?? "").toLowerCase() === unitSearch.value.toLowerCase()),
|
||||
);
|
||||
|
||||
async function createAssignUnit() {
|
||||
unitsData.data.name = unitSearch.value;
|
||||
model.value.unit = await unitStore.actions.createOne(unitsData.data) || undefined;
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
>
|
||||
<h3
|
||||
v-if="showTitleEditor[index]"
|
||||
class="mt-2"
|
||||
class="mt-4 mb-0"
|
||||
>
|
||||
{{ ingredient.title }}
|
||||
</h3>
|
||||
<v-divider v-if="showTitleEditor[index]" />
|
||||
<v-divider v-if="showTitleEditor[index]" class="my-2" />
|
||||
<v-list-item
|
||||
density="compact"
|
||||
class="pa-0"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
class="pa-0 ma-0"
|
||||
@update:model-value="resetSearchInput"
|
||||
@click:append="dialog = true"
|
||||
@keyup.enter="handleEnter"
|
||||
>
|
||||
<template #chip="{ item, index }">
|
||||
<v-chip
|
||||
@@ -32,6 +33,26 @@
|
||||
@click:close="removeByIndex(index)"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-if="showAdd"
|
||||
#no-data
|
||||
>
|
||||
<div class="caption text-center pb-2">
|
||||
{{ $t("recipe.press-enter-to-create") }}
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-if="showAdd && searchInput"
|
||||
#append-item
|
||||
>
|
||||
<div class="px-2">
|
||||
<BaseButton
|
||||
block
|
||||
size="small"
|
||||
@click="createItem()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-if="showAdd"
|
||||
#append
|
||||
@@ -180,6 +201,32 @@ function appendCreated(item: any) {
|
||||
selected.value = [...selected.value, item];
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (!searchInput.value) {
|
||||
return;
|
||||
}
|
||||
const exactMatch = items.value.some(
|
||||
(item: any) => (item.name ?? "").toLowerCase() === searchInput.value.toLowerCase(),
|
||||
);
|
||||
if (!exactMatch) {
|
||||
createItem();
|
||||
}
|
||||
}
|
||||
|
||||
async function createItem() {
|
||||
if (!searchInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = storeMap[props.selectorType].actions;
|
||||
// @ts-expect-error different organizer types have different required fields
|
||||
const newItem = await actions.createOne({ name: searchInput.value });
|
||||
if (newItem) {
|
||||
appendCreated(newItem);
|
||||
}
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
const dialog = ref(false);
|
||||
|
||||
const searchInput = ref("");
|
||||
|
||||
@@ -336,8 +336,16 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// When set, the isEditMode watcher skips its URL cleanup because saveRecipe
|
||||
// is navigating to a new slug that naturally omits ?edit=true.
|
||||
const isNavigatingAfterRename = ref(false);
|
||||
|
||||
watch(isEditMode, (newVal) => {
|
||||
if (!newVal) {
|
||||
if (isNavigatingAfterRename.value) {
|
||||
isNavigatingAfterRename.value = false;
|
||||
return;
|
||||
}
|
||||
paramsEdit.value = undefined;
|
||||
}
|
||||
});
|
||||
@@ -355,13 +363,17 @@ watch(isParsing, () => {
|
||||
async function saveRecipe() {
|
||||
const { data, error } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
|
||||
if (!error) {
|
||||
if (data?.slug && data.slug !== route.params.slug) {
|
||||
isNavigatingAfterRename.value = true;
|
||||
}
|
||||
setMode(PageMode.VIEW);
|
||||
}
|
||||
if (data?.slug) {
|
||||
router.push(`/g/${groupSlug.value}/r/` + data.slug);
|
||||
recipe.value = data as NoUndefinedField<Recipe>;
|
||||
// Update the snapshot after successful save
|
||||
originalRecipe.value = deepCopy(recipe.value);
|
||||
if (data.slug !== route.params.slug) {
|
||||
router.replace(`/g/${groupSlug.value}/r/` + data.slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-6
@@ -200,6 +200,7 @@ import { useUserApi } from "~/composables/api";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useParsingPreferences } from "~/composables/use-users/preferences";
|
||||
|
||||
@@ -215,7 +216,7 @@ const emit = defineEmits<{
|
||||
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
||||
}>();
|
||||
|
||||
const { $appInfo } = useNuxtApp();
|
||||
const { group } = useGroupSelf();
|
||||
const i18n = useGlobalI18n();
|
||||
const api = useUserApi();
|
||||
const drag = ref(false);
|
||||
@@ -240,7 +241,7 @@ const availableParsers = computed(() => {
|
||||
{
|
||||
text: i18n.t("recipe.parser.openai-parser"),
|
||||
value: "openai",
|
||||
hide: !$appInfo.enableOpenai,
|
||||
hide: !group.value?.aiProviderSettings?.aiEnabled,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -371,14 +372,18 @@ async function parseIngredients() {
|
||||
}
|
||||
state.loading.parser = true;
|
||||
try {
|
||||
const ingsAsString = props.ingredients
|
||||
.filter(ing => !ing.referencedRecipe)
|
||||
.map(ing => ingredientToParserString(ing));
|
||||
const filteredIngredients = props.ingredients.filter(ing => !ing.referencedRecipe);
|
||||
const ingsAsString = filteredIngredients.map(ing => ingredientToParserString(ing));
|
||||
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
|
||||
if (error || !data) {
|
||||
throw new Error("Failed to parse ingredients");
|
||||
}
|
||||
parsedIngs.value = data;
|
||||
|
||||
// Restore section titles from original ingredients — the parser doesn't return them
|
||||
data.forEach((parsed, index) => {
|
||||
parsed.ingredient.title = filteredIngredients[index]?.title || "";
|
||||
});
|
||||
|
||||
const parsed = data ?? [];
|
||||
const recipeRefs = props.ingredients.filter(ing => ing.referencedRecipe).map(ing => ({
|
||||
input: ing.note || "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div style="height: 100%;">
|
||||
<v-row class="my-0 mx-7">
|
||||
<v-row class="mb-0 mt-3 mx-7">
|
||||
<v-spacer />
|
||||
<v-col class="text-right">
|
||||
<!-- Filters -->
|
||||
@@ -44,6 +44,7 @@
|
||||
:model-value="option.checked"
|
||||
color="primary"
|
||||
readonly
|
||||
hide-details
|
||||
@click="toggleEventTypeOption(option.value)"
|
||||
>
|
||||
<template #label>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<v-col v-else cols="9" style="margin: auto; text-align: center">
|
||||
{{ event.subject }}
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col :cols="useMobileFormat ? 'auto' : '1'" class="px-0 pt-0">
|
||||
<RecipeTimelineContextMenu
|
||||
v-if="currentUser && currentUser.id == event.userId && event.eventType != 'system'"
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
permanent
|
||||
rounded="t-xl"
|
||||
location="bottom"
|
||||
class="pa-4 pt-2 mb-0"
|
||||
width="300"
|
||||
rail-width="85"
|
||||
:rail="rail"
|
||||
elevation="4"
|
||||
>
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<v-card-actions class="pa-0">
|
||||
<div class="position-relative" style="flex: 1;">
|
||||
<InputLabelType
|
||||
ref="foodInputRef"
|
||||
v-model="listItem.food"
|
||||
v-model:item-id="listItem.foodId!"
|
||||
:items="foods"
|
||||
:label="rail ? $t('shopping-list.add-item') : $t('shopping-list.food')"
|
||||
:icon="$globals.icons.foods"
|
||||
:style="rail ? 'margin-inline: 3px;' : undefined"
|
||||
:search="rail"
|
||||
:menu-props="{ location: menuDirection }"
|
||||
create
|
||||
@create="createAssignFood"
|
||||
/>
|
||||
<!-- Intercept clicks when collapsed so the drawer expands before the autocomplete opens -->
|
||||
<div
|
||||
v-if="rail"
|
||||
class="position-absolute"
|
||||
style="inset: 0; cursor: text;"
|
||||
@click="expandAndFocus"
|
||||
/>
|
||||
</div>
|
||||
<BaseButtonGroup
|
||||
v-if="!rail"
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
event: 'cancel',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@save="$emit('save')"
|
||||
@cancel="rail = true; $emit('cancel')"
|
||||
/>
|
||||
</v-card-actions>
|
||||
|
||||
<ShoppingListItemDetails
|
||||
v-if="!rail"
|
||||
v-model="listItem"
|
||||
:labels="labels"
|
||||
:units="units"
|
||||
@save="$emit('save')"
|
||||
/>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useShoppingListItemEditor } from "~/composables/shopping-list-page/use-shopping-list-item-editor";
|
||||
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import ShoppingListItemDetails from "./ShoppingListItemDetails.vue";
|
||||
|
||||
// modelValue as reactive v-model
|
||||
const listItem = defineModel<ShoppingListItemCreate | ShoppingListItemOut>({ required: true });
|
||||
|
||||
defineProps({
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
foods: {
|
||||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
(e: "save" | "cancel" | "delete"): void;
|
||||
}>();
|
||||
|
||||
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
||||
|
||||
const { smAndDown } = useDisplay();
|
||||
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
||||
|
||||
const foodInputRef = ref<{ focus: () => void } | null>(null);
|
||||
const rail = ref(true);
|
||||
|
||||
async function expandAndFocus() {
|
||||
rail.value = false;
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
foodInputRef.value?.focus();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => listItem.value.quantity,
|
||||
(newQty) => {
|
||||
if (!newQty) {
|
||||
listItem.value.quantity = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => listItem.value.food,
|
||||
(newFood) => {
|
||||
listItem.value.label = newFood?.label || null;
|
||||
listItem.value.labelId = listItem.value.label?.id || null;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@@ -1,154 +1,165 @@
|
||||
<template>
|
||||
<v-container
|
||||
v-if="!edit"
|
||||
class="pa-0"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
class="flex-nowrap align-center"
|
||||
<div style="overflow-x: hidden;">
|
||||
<v-container
|
||||
v-if="!edit"
|
||||
class="pa-0"
|
||||
:style="{
|
||||
transform: `translateX(${isRtl ? -swiping : swiping}px)`,
|
||||
transition: swiping === 0 ? 'transform 0.2s ease' : 'none',
|
||||
opacity: swiping >= SWIPE_THRESHOLD ? 0.5 : 1,
|
||||
}"
|
||||
>
|
||||
<v-col :cols="itemLabelCols">
|
||||
<div class="d-flex align-center flex-nowrap">
|
||||
<v-checkbox
|
||||
:model-value="listItem.checked"
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mt-0 flex-shrink-0"
|
||||
color="null"
|
||||
@click="toggleChecked"
|
||||
/>
|
||||
<div
|
||||
class="ml-2 text-truncate"
|
||||
:class="listItem.checked ? 'strike-through' : ''"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<RecipeIngredientListItem :ingredient="listItem" />
|
||||
<v-row
|
||||
ref="swipeRowRef"
|
||||
v-touch="{ move: onSwipeMove, start: onSwipeStart, end: onSwipeEnd }"
|
||||
style="touch-action: pan-y;"
|
||||
no-gutters
|
||||
class="flex-nowrap align-center"
|
||||
>
|
||||
<v-col :cols="itemLabelCols">
|
||||
<div class="d-flex align-center flex-nowrap">
|
||||
<v-checkbox
|
||||
:model-value="listItem.checked"
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mt-0 flex-shrink-0"
|
||||
color="null"
|
||||
@click="toggleChecked"
|
||||
/>
|
||||
<div
|
||||
class="ml-2 text-truncate"
|
||||
:class="listItem.checked ? 'strike-through' : ''"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<RecipeIngredientListItem :ingredient="listItem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col
|
||||
cols="auto"
|
||||
class="text-right"
|
||||
>
|
||||
<div
|
||||
v-if="!listItem.checked"
|
||||
style="min-width: 72px"
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col
|
||||
cols="auto"
|
||||
class="text-right"
|
||||
>
|
||||
<v-menu
|
||||
offset-x
|
||||
start
|
||||
min-width="125px"
|
||||
<div
|
||||
v-if="!listItem.checked"
|
||||
style="min-width: 72px"
|
||||
>
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-tooltip
|
||||
v-if="recipeList && recipeList.length"
|
||||
open-delay="200"
|
||||
transition="slide-x-reverse-transition"
|
||||
density="compact"
|
||||
location="end"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
v-bind="tooltipProps"
|
||||
@click="displayRecipeRefs = !displayRecipeRefs"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.potSteam }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Toggle Recipes</span>
|
||||
</v-tooltip>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
@click="toggleEdit(true)"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="handle"
|
||||
icon
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="action in contextMenu"
|
||||
:key="action.event"
|
||||
density="compact"
|
||||
@click="contextHandler(action.event)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ action.text }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="!listItem.checked && recipeList && recipeList.length && displayRecipeRefs"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col
|
||||
cols="auto"
|
||||
style="width: 100%;"
|
||||
<v-menu
|
||||
offset-x
|
||||
start
|
||||
min-width="125px"
|
||||
>
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-tooltip
|
||||
v-if="recipeList && recipeList.length"
|
||||
open-delay="200"
|
||||
transition="slide-x-reverse-transition"
|
||||
density="compact"
|
||||
location="end"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
v-bind="tooltipProps"
|
||||
@click="displayRecipeRefs = !displayRecipeRefs"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.potSteam }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Toggle Recipes</span>
|
||||
</v-tooltip>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
icon
|
||||
@click="toggleEdit(true)"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="handle"
|
||||
icon
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="action in contextMenu"
|
||||
:key="action.event"
|
||||
density="compact"
|
||||
@click="contextHandler(action.event)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ action.text }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="!listItem.checked && recipeList && recipeList.length && displayRecipeRefs"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
>
|
||||
<RecipeList
|
||||
:recipes="recipeList"
|
||||
:list-item="listItem"
|
||||
:disabled="isOffline"
|
||||
size="small"
|
||||
tile
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="listItem.checked"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
<v-col
|
||||
cols="auto"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<RecipeList
|
||||
:recipes="recipeList"
|
||||
:list-item="listItem"
|
||||
:disabled="isOffline"
|
||||
size="small"
|
||||
tile
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="listItem.checked"
|
||||
no-gutters
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {
|
||||
date: listItem.updatedAt ? $d(new Date(listItem.updatedAt)) : '',
|
||||
}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div
|
||||
v-else
|
||||
class="mb-1 mt-6"
|
||||
>
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {
|
||||
date: listItem.updatedAt ? $d(new Date(listItem.updatedAt)) : '',
|
||||
}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div
|
||||
v-else
|
||||
class="mb-1 mt-6"
|
||||
>
|
||||
<ShoppingListItemEditor
|
||||
v-model="localListItem"
|
||||
:labels="labels"
|
||||
:units="units"
|
||||
:foods="foods"
|
||||
@save="save"
|
||||
@cancel="toggleEdit(false)"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
<ShoppingListItemEditor
|
||||
v-model="localListItem"
|
||||
:labels="labels"
|
||||
:units="units"
|
||||
:foods="foods"
|
||||
class="ma-2"
|
||||
@save="save"
|
||||
@cancel="toggleEdit(false)"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -156,10 +167,10 @@
|
||||
import { useOnline } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
|
||||
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||
import type { IngredientUnit, IngredientFood, RecipeSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
const model = defineModel<ShoppingListItemOut>({ type: Object as () => ShoppingListItemOut, required: true });
|
||||
|
||||
@@ -187,6 +198,24 @@ const emit = defineEmits<{
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
|
||||
const { isRtl } = useRtl();
|
||||
const swipeRowRef = ref<InstanceType<typeof import("vuetify/components").VRow> | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
const el = swipeRowRef.value?.$el as HTMLElement | undefined;
|
||||
if (!el) return;
|
||||
el.addEventListener(
|
||||
"touchmove",
|
||||
(e: TouchEvent) => {
|
||||
if (swipeInfo.value.gesture === "swipe") {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
});
|
||||
const i18n = useI18n();
|
||||
const displayRecipeRefs = ref(false);
|
||||
const itemLabelCols = computed<string>(() => (model.value?.checked ? "auto" : "6"));
|
||||
@@ -237,6 +266,68 @@ function save() {
|
||||
edit.value = false;
|
||||
}
|
||||
|
||||
type SwipeGesture = null | "scroll" | "swipe";
|
||||
|
||||
const swipeInfo = ref({
|
||||
touchstartX: 0,
|
||||
touchstartY: 0,
|
||||
touchendX: 0,
|
||||
touchendY: 0,
|
||||
gesture: null as SwipeGesture,
|
||||
});
|
||||
|
||||
function getSwipePoint(e: any) {
|
||||
const touch = e?.touches?.[0] ?? e?.changedTouches?.[0] ?? e;
|
||||
return { x: touch?.clientX ?? 0, y: touch?.clientY ?? 0 };
|
||||
}
|
||||
|
||||
function resetSwipe() {
|
||||
swipeInfo.value = { touchstartX: 0, touchstartY: 0, touchendX: 0, touchendY: 0, gesture: null };
|
||||
}
|
||||
|
||||
function onSwipeStart(payload: any) {
|
||||
const { x, y } = getSwipePoint(payload.originalEvent);
|
||||
swipeInfo.value = { touchstartX: x, touchstartY: y, touchendX: x, touchendY: y, gesture: null };
|
||||
}
|
||||
|
||||
function onSwipeMove(payload: any) {
|
||||
const { x, y } = getSwipePoint(payload.originalEvent);
|
||||
swipeInfo.value.touchendX = x;
|
||||
swipeInfo.value.touchendY = y;
|
||||
|
||||
if (!swipeInfo.value.gesture) {
|
||||
const deltaX = Math.abs(x - swipeInfo.value.touchstartX);
|
||||
const deltaY = Math.abs(y - swipeInfo.value.touchstartY);
|
||||
if (deltaY > 8 && deltaY > deltaX) {
|
||||
swipeInfo.value.gesture = "scroll";
|
||||
}
|
||||
else if (deltaX > 8 && deltaX > deltaY) {
|
||||
swipeInfo.value.gesture = "swipe";
|
||||
}
|
||||
else if (deltaX > 8 || deltaY > 8) {
|
||||
// Diagonal / ambiguous — default to scroll
|
||||
swipeInfo.value.gesture = "scroll";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSwipeEnd() {
|
||||
if (swipeInfo.value.gesture === "swipe" && swiping.value >= SWIPE_THRESHOLD) {
|
||||
toggleChecked();
|
||||
}
|
||||
resetSwipe();
|
||||
}
|
||||
|
||||
const swiping = computed(() => {
|
||||
if (swipeInfo.value.gesture !== "swipe") {
|
||||
return 0;
|
||||
}
|
||||
const deltaX = isRtl.value
|
||||
? swipeInfo.value.touchstartX - swipeInfo.value.touchendX
|
||||
: swipeInfo.value.touchendX - swipeInfo.value.touchstartX;
|
||||
return Math.max(0, Math.min(deltaX, 100));
|
||||
});
|
||||
|
||||
const recipeList = computed<RecipeSummary[]>(() => {
|
||||
const ret: RecipeSummary[] = [];
|
||||
if (!listItem.value.recipeReferences) return ret;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="d-flex ga-3">
|
||||
<v-number-input
|
||||
v-model="listItem.quantity"
|
||||
hide-details
|
||||
:label="$t('form.quantity-label-abbreviated')"
|
||||
:min="0"
|
||||
:precision="null"
|
||||
control-variant="stacked"
|
||||
style="flex: 1"
|
||||
inset
|
||||
/>
|
||||
<InputLabelType
|
||||
v-model="listItem.unit"
|
||||
v-model:item-id="listItem.unitId!"
|
||||
:items="units"
|
||||
:label="$t('recipe.unit')"
|
||||
:icon="$globals.icons.units"
|
||||
:menu-props="{ location: menuDirection }"
|
||||
style="flex: 3"
|
||||
create
|
||||
@create="createAssignUnit"
|
||||
/>
|
||||
</div>
|
||||
<v-textarea
|
||||
v-model="listItem.note"
|
||||
hide-details
|
||||
:label="$t('shopping-list.note')"
|
||||
rows="1"
|
||||
auto-grow
|
||||
@keypress="handleNoteKeyPress"
|
||||
/>
|
||||
<div class="d-flex flex-wrap align-end ga-3">
|
||||
<InputLabelType
|
||||
v-model="listItem.label"
|
||||
v-model:item-id="listItem.labelId!"
|
||||
:items="labels"
|
||||
:label="$t('shopping-list.label')"
|
||||
:menu-props="{ location: menuDirection }"
|
||||
style="flex: 1 0 200px"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="listItem.labelId && listItem.food && listItem.labelId !== listItem.food.labelId"
|
||||
small
|
||||
color="info"
|
||||
:icon="$globals.icons.tagArrowRight"
|
||||
:text="$t('shopping-list.save-label')"
|
||||
class="mt-2 align-items-flex-start"
|
||||
style="flex-grow: 0"
|
||||
@click="assignLabelToFood"
|
||||
/>
|
||||
<v-spacer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useShoppingListItemEditor } from "~/composables/shopping-list-page/use-shopping-list-item-editor";
|
||||
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
// modelValue as reactive v-model
|
||||
const listItem = defineModel<ShoppingListItemCreate | ShoppingListItemOut>({ required: true });
|
||||
|
||||
defineProps({
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ (e: "save"): void }>();
|
||||
|
||||
const { assignLabelToFood, createAssignUnit } = useShoppingListItemEditor(listItem);
|
||||
|
||||
const { smAndDown } = useDisplay();
|
||||
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
||||
|
||||
function handleNoteKeyPress(event: KeyboardEvent) {
|
||||
// Save on Enter
|
||||
if (!event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
emit("save");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,112 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="pb-3 pt-1">
|
||||
<div class="d-md-flex align-center mb-2" style="gap: 20px">
|
||||
<div>
|
||||
<v-number-input
|
||||
v-model="listItem.quantity"
|
||||
hide-details
|
||||
:label="$t('form.quantity-label-abbreviated')"
|
||||
:min="0"
|
||||
:precision="null"
|
||||
control-variant="stacked"
|
||||
inset
|
||||
style="width: 100px;"
|
||||
/>
|
||||
</div>
|
||||
<InputLabelType
|
||||
v-model="listItem.unit"
|
||||
v-model:item-id="listItem.unitId!"
|
||||
:items="units"
|
||||
:label="$t('recipe.unit')"
|
||||
:icon="$globals.icons.units"
|
||||
create
|
||||
@create="createAssignUnit"
|
||||
/>
|
||||
<InputLabelType
|
||||
v-model="listItem.food"
|
||||
v-model:item-id="listItem.foodId!"
|
||||
:items="foods"
|
||||
:label="$t('shopping-list.food')"
|
||||
:icon="$globals.icons.foods"
|
||||
:autofocus="autoFocus === 'food'"
|
||||
create
|
||||
@create="createAssignFood"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-md-flex align-center" style="gap: 20px">
|
||||
<v-textarea
|
||||
v-model="listItem.note"
|
||||
hide-details
|
||||
:label="$t('shopping-list.note')"
|
||||
rows="1"
|
||||
auto-grow
|
||||
:autofocus="autoFocus === 'note'"
|
||||
@keypress="handleNoteKeyPress"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-end" style="gap: 20px">
|
||||
<div class="d-flex align-end">
|
||||
<div style="max-width: 300px" class="mt-3 mr-auto">
|
||||
<InputLabelType
|
||||
v-model="listItem.label"
|
||||
v-model:item-id="listItem.labelId!"
|
||||
:items="labels"
|
||||
:label="$t('shopping-list.label')"
|
||||
width="250"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton
|
||||
v-if="listItem.labelId && listItem.food && listItem.labelId !== listItem.food.labelId"
|
||||
small
|
||||
color="info"
|
||||
:icon="$globals.icons.tagArrowRight"
|
||||
:text="$t('shopping-list.save-label')"
|
||||
class="mt-2 align-items-flex-start"
|
||||
@click="assignLabelToFood"
|
||||
/>
|
||||
<v-spacer />
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
...(allowDelete
|
||||
? [
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
event: 'cancel',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@save="$emit('save')"
|
||||
@cancel="$emit('cancel')"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
<v-card variant="elevated" class="pa-2" border="primary s-lg opacity-100">
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<InputLabelType
|
||||
v-model="listItem.food"
|
||||
v-model:item-id="listItem.foodId!"
|
||||
:items="foods"
|
||||
:label="$t('shopping-list.food')"
|
||||
:icon="$globals.icons.foods"
|
||||
:autofocus="autoFocus === 'food'"
|
||||
create
|
||||
@create="createAssignFood"
|
||||
/>
|
||||
<ShoppingListItemDetails
|
||||
v-model="listItem"
|
||||
:labels="labels"
|
||||
:units="units"
|
||||
@save="$emit('save')"
|
||||
/>
|
||||
</div>
|
||||
<v-card-actions class="justify-end pa-0">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
...(allowDelete
|
||||
? [
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
event: 'cancel',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@save="$emit('save')"
|
||||
@cancel="$emit('cancel')"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useShoppingListItemEditor } from "~/composables/shopping-list-page/use-shopping-list-item-editor";
|
||||
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||
import ShoppingListItemDetails from "./ShoppingListItemDetails.vue";
|
||||
|
||||
// modelValue as reactive v-model
|
||||
const listItem = defineModel<ShoppingListItemCreate | ShoppingListItemOut>({ required: true });
|
||||
@@ -132,16 +80,11 @@ defineProps({
|
||||
});
|
||||
|
||||
// const emit = defineEmits<["save", "cancel", "delete"]>();
|
||||
const emit = defineEmits<{
|
||||
(e: "save", item: ShoppingListItemOut): void;
|
||||
(e: "cancel" | "delete"): void;
|
||||
defineEmits<{
|
||||
(e: "save" | "cancel" | "delete"): void;
|
||||
}>();
|
||||
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
|
||||
const unitStore = useUnitStore();
|
||||
const unitData = useUnitData();
|
||||
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
||||
|
||||
watch(
|
||||
() => listItem.value.quantity,
|
||||
@@ -161,49 +104,4 @@ watch(
|
||||
);
|
||||
|
||||
const autoFocus = computed(() => (!listItem.value.food && listItem.value.note ? "note" : "food"));
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val } as any);
|
||||
|
||||
foodData.data.name = val;
|
||||
const newFood = await foodStore.actions.createOne(foodData.data);
|
||||
if (newFood) {
|
||||
listItem.value.food = newFood;
|
||||
listItem.value.foodId = newFood.id;
|
||||
}
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
async function createAssignUnit(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val } as any);
|
||||
|
||||
unitData.data.name = val;
|
||||
const newUnit = await unitStore.actions.createOne(unitData.data);
|
||||
if (newUnit) {
|
||||
listItem.value.unit = newUnit;
|
||||
listItem.value.unitId = newUnit.id;
|
||||
}
|
||||
unitData.reset();
|
||||
}
|
||||
|
||||
async function assignLabelToFood() {
|
||||
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
listItem.value.food.labelId = listItem.value.labelId;
|
||||
await foodStore.actions.updateOne(listItem.value.food);
|
||||
}
|
||||
|
||||
function handleNoteKeyPress(event: KeyboardEvent) {
|
||||
const e = event as KeyboardEvent & { key: string; shiftKey: boolean };
|
||||
if (!e.shiftKey && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
emit("save");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
style="border-color: lightgrey;"
|
||||
:to="link.to"
|
||||
height="100%"
|
||||
class="d-flex flex-column mt-4"
|
||||
class="d-flex flex-column mt-4 pa-2"
|
||||
>
|
||||
<div
|
||||
v-if="$vuetify.display.smAndDown"
|
||||
|
||||
@@ -96,21 +96,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { SideBarLink } from "~/types/application-types";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $appInfo, $globals } = useNuxtApp();
|
||||
const { $globals } = useNuxtApp();
|
||||
const display = useDisplay();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const { group } = useGroupSelf();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const cookbookPreferences = useCookbookPreferences();
|
||||
const ownCookbookStore = useCookbookStore(i18n);
|
||||
const ownCookbookStore = computed(() => isOwnGroup.value ? useCookbookStore(i18n) : null);
|
||||
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
|
||||
|
||||
function getPublicCookbookStore(slug: string) {
|
||||
@@ -121,8 +123,8 @@ function getPublicCookbookStore(slug: string) {
|
||||
}
|
||||
|
||||
const cookbooks = computed(() => {
|
||||
if (isOwnGroup.value) {
|
||||
return ownCookbookStore.store.value;
|
||||
if (ownCookbookStore.value) {
|
||||
return ownCookbookStore.value.store.value;
|
||||
}
|
||||
else if (groupSlug.value) {
|
||||
const publicStore = getPublicCookbookStore(groupSlug.value);
|
||||
@@ -131,7 +133,7 @@ const cookbooks = computed(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
const showImageImport = computed(() => $appInfo.enableOpenaiImageServices);
|
||||
const showImageImport = computed(() => group.value?.aiProviderSettings?.imageProviderEnabled);
|
||||
|
||||
const sidebar = ref<boolean>(false);
|
||||
onMounted(() => {
|
||||
@@ -205,7 +207,7 @@ const createLinks = computed(() => [
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.fileImage,
|
||||
title: i18n.t("recipe.create-from-images"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-images"),
|
||||
to: `/g/${groupSlug.value}/r/create/image`,
|
||||
restricted: true,
|
||||
hide: !showImageImport.value,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-model="toastAlert.open"
|
||||
location="top"
|
||||
:color="toastAlert.color"
|
||||
timeout="2000"
|
||||
:timeout="toastAlert.timeout ?? 2000"
|
||||
>
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
@@ -19,9 +19,12 @@
|
||||
<template #actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="toastAlert.open = false"
|
||||
@click="() => {
|
||||
toastAlert.action?.onClick();
|
||||
toastAlert.open = false
|
||||
}"
|
||||
>
|
||||
{{ $t('general.close') }}
|
||||
{{ toastAlert.action?.message ?? $t('general.close') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
'mt-8': section,
|
||||
}"
|
||||
>
|
||||
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
|
||||
<v-card-title :class="`text-title-${size} pl-0 py-0 d-flex align-center`" style="font-weight: normal;">
|
||||
<slot name="prepend-title" />
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
size="small"
|
||||
@@ -16,6 +17,7 @@
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
{{ title }}
|
||||
<slot name="append-title" />
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
v-if="$slots.default"
|
||||
@@ -30,11 +32,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Size = "large" | "medium" | "small";
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: String as () => Size,
|
||||
default: "large",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
|
||||
@@ -192,6 +192,14 @@ function submitOnEnter() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.canConfirm) {
|
||||
if (!props.submitDisabled) {
|
||||
emit("confirm");
|
||||
dialog.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
submitEvent();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(value, key) in (modelValue ?? {})"
|
||||
:key="key"
|
||||
class="d-flex align-center mb-2 gap-2"
|
||||
>
|
||||
<v-text-field
|
||||
:model-value="key"
|
||||
:label="resolvedKeyLabel"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
readonly
|
||||
class="me-3 flex-grow-1"
|
||||
/>
|
||||
<v-text-field
|
||||
:model-value="value"
|
||||
:label="resolvedValueLabel"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="ms-3 flex-grow-1"
|
||||
@update:model-value="updateValue(key, $event)"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="removeEntry(key)"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center mt-2 gap-2" @focusout="onNewEntryFocusOut">
|
||||
<v-text-field
|
||||
v-model="newKey"
|
||||
:label="resolvedKeyLabel"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="me-3 flex-grow-1"
|
||||
@keydown.enter.prevent="addEntry"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="newValue"
|
||||
:label="resolvedValueLabel"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="ms-3 flex-grow-1"
|
||||
@keydown.enter.prevent="addEntry"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
:disabled="!newKey?.trim()"
|
||||
@click="addEntry"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.createAlt }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
const i18n = useGlobalI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: Record<string, string> | null;
|
||||
keyLabel?: string;
|
||||
valueLabel?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: Record<string, string>): void;
|
||||
}>();
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const resolvedKeyLabel = computed(() => props.keyLabel ?? i18n.t("general.key"));
|
||||
const resolvedValueLabel = computed(() => props.valueLabel ?? i18n.t("general.value"));
|
||||
|
||||
const newKey = ref("");
|
||||
const newValue = ref("");
|
||||
|
||||
function current(): Record<string, string> {
|
||||
return { ...(props.modelValue ?? {}) };
|
||||
}
|
||||
|
||||
function addEntry() {
|
||||
const key = newKey.value?.trim();
|
||||
if (!key) return;
|
||||
const updated = current();
|
||||
updated[key] = newValue.value;
|
||||
emit("update:modelValue", updated);
|
||||
newKey.value = "";
|
||||
newValue.value = "";
|
||||
}
|
||||
|
||||
function onNewEntryFocusOut(e: FocusEvent) {
|
||||
const relatedTarget = e.relatedTarget as HTMLElement | null;
|
||||
const currentTarget = e.currentTarget as HTMLElement;
|
||||
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
|
||||
addEntry();
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(key: string, value: string) {
|
||||
const updated = current();
|
||||
updated[key] = value;
|
||||
emit("update:modelValue", updated);
|
||||
}
|
||||
|
||||
function removeEntry(key: string) {
|
||||
const updated = current();
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete updated[key];
|
||||
emit("update:modelValue", updated);
|
||||
}
|
||||
</script>
|
||||
@@ -7,12 +7,15 @@
|
||||
item-title="name"
|
||||
return-object
|
||||
:items="filteredItems"
|
||||
:prepend-icon="icon || $globals.icons.tags"
|
||||
:prepend-inner-icon="icon || (search ? $globals.icons.search : $globals.icons.tags)"
|
||||
:menu-icon="search ? '' : undefined"
|
||||
:rounded="search ? true : '4px'"
|
||||
:custom-filter="() => true"
|
||||
:variant="search ? 'solo-filled' : undefined"
|
||||
color="primary"
|
||||
auto-select-first
|
||||
clearable
|
||||
color="primary"
|
||||
hide-details
|
||||
:custom-filter="() => true"
|
||||
@keyup.enter="emitCreate"
|
||||
>
|
||||
<template
|
||||
@@ -55,6 +58,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -86,4 +93,8 @@ function emitCreate() {
|
||||
emit("create", searchInput.value);
|
||||
autocompleteRef.value?.blur();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus: () => autocompleteRef.value?.focus(),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
enum DOMPurifyHook {
|
||||
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||
}
|
||||
import { sanitizeMarkdownHtml } from "~/lib/sanitize/markdown";
|
||||
|
||||
const props = defineProps({
|
||||
source: {
|
||||
@@ -18,48 +14,11 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
});
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: [
|
||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
|
||||
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
const { $appInfo } = useNuxtApp();
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
return sanitizeMarkdownHtml(rawHtml, $appInfo?.allowedIframeHosts ?? []);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,17 +14,25 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWakeLock } from "@vueuse/core";
|
||||
import { useUserExperiencePreferences } from "~/composables/use-users/preferences";
|
||||
|
||||
const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
|
||||
const userExperiencePreferences = useUserExperiencePreferences();
|
||||
|
||||
function handleLock() {
|
||||
if (userExperiencePreferences.value.lockScreen) {
|
||||
lockScreen();
|
||||
}
|
||||
else {
|
||||
unlockScreen();
|
||||
}
|
||||
}
|
||||
|
||||
const wakeLock = computed({
|
||||
get: () => isActive.value,
|
||||
get: () => userExperiencePreferences.value.lockScreen,
|
||||
set: () => {
|
||||
if (isActive.value) {
|
||||
unlockScreen();
|
||||
}
|
||||
else {
|
||||
lockScreen();
|
||||
}
|
||||
userExperiencePreferences.value.lockScreen = !userExperiencePreferences.value.lockScreen;
|
||||
handleLock();
|
||||
},
|
||||
});
|
||||
async function lockScreen() {
|
||||
@@ -34,11 +42,11 @@ async function lockScreen() {
|
||||
}
|
||||
}
|
||||
async function unlockScreen() {
|
||||
if (wakeIsSupported || isActive) {
|
||||
if (wakeIsSupported || isActive.value) {
|
||||
console.debug("Wake Lock Released");
|
||||
await release();
|
||||
}
|
||||
}
|
||||
onMounted(() => lockScreen());
|
||||
onMounted(() => handleLock());
|
||||
onUnmounted(() => unlockScreen());
|
||||
</script>
|
||||
|
||||
@@ -13,9 +13,10 @@ describe("useStoreActions", () => {
|
||||
|
||||
const mockStore = ref([]);
|
||||
const mockLoading = ref(false);
|
||||
const mockInitialized = ref(false);
|
||||
|
||||
test("deleteMany calls deleteOne for each ID and refreshes once", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
|
||||
mockApi.deleteOne = vi.fn().mockResolvedValue({ response: { data: {} } });
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -32,7 +33,7 @@ describe("useStoreActions", () => {
|
||||
});
|
||||
|
||||
test("deleteMany handles empty array", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
|
||||
mockApi.deleteOne = vi.fn();
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -44,7 +45,7 @@ describe("useStoreActions", () => {
|
||||
});
|
||||
|
||||
test("deleteMany sets loading state", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
|
||||
mockApi.deleteOne = vi.fn().mockResolvedValue({});
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -55,4 +56,25 @@ describe("useStoreActions", () => {
|
||||
await promise;
|
||||
expect(mockLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test("refresh sets initialized to true even when store returns empty results", async () => {
|
||||
const localInitialized = ref(false);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, localInitialized);
|
||||
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
|
||||
expect(localInitialized.value).toBe(false);
|
||||
await actions.refresh();
|
||||
expect(localInitialized.value).toBe(true);
|
||||
});
|
||||
|
||||
test("refresh sets initialized to true when store returns items", async () => {
|
||||
const localInitialized = ref(false);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, localInitialized);
|
||||
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [{ id: "1", name: "item" }] } });
|
||||
|
||||
await actions.refresh();
|
||||
expect(localInitialized.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useReadOnlyActions<T extends BoundT>(
|
||||
api: BaseCRUDAPIReadOnly<T>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
defaultQueryParams: Record<string, QueryValue> = {},
|
||||
): ReadOnlyStoreActions<T> {
|
||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
@@ -69,6 +70,7 @@ export function useReadOnlyActions<T extends BoundT>(
|
||||
allRef.value = data.items;
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -89,6 +91,7 @@ export function useStoreActions<T extends BoundT>(
|
||||
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
defaultQueryParams: Record<string, QueryValue> = {},
|
||||
): StoreActions<T> {
|
||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
@@ -132,6 +135,7 @@ export function useStoreActions<T extends BoundT>(
|
||||
allRef.value = data.items;
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ export const useReadOnlyStore = function <T extends BoundT>(
|
||||
storeKey: string,
|
||||
store: Ref<T[]>,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
api: BaseCRUDAPIReadOnly<T>,
|
||||
params = {} as Record<string, QueryValue>,
|
||||
) {
|
||||
const storeActions = useReadOnlyActions(`${storeKey}-store-readonly`, api, store, loading);
|
||||
const storeActions = useReadOnlyActions(`${storeKey}-store-readonly`, api, store, loading, initialized);
|
||||
const actions = {
|
||||
...storeActions,
|
||||
async refresh() {
|
||||
@@ -27,11 +28,12 @@ export const useReadOnlyStore = function <T extends BoundT>(
|
||||
},
|
||||
flushStore() {
|
||||
store.value = [];
|
||||
initialized.value = false;
|
||||
},
|
||||
};
|
||||
|
||||
// initial hydration
|
||||
if (!loading.value && !store.value.length) {
|
||||
if (!loading.value && !initialized.value) {
|
||||
actions.refresh();
|
||||
}
|
||||
|
||||
@@ -42,10 +44,11 @@ export const useStore = function <T extends BoundT>(
|
||||
storeKey: string,
|
||||
store: Ref<T[]>,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||
params = {} as Record<string, QueryValue>,
|
||||
) {
|
||||
const storeActions = useStoreActions(`${storeKey}-store`, api, store, loading);
|
||||
const storeActions = useStoreActions(`${storeKey}-store`, api, store, loading, initialized);
|
||||
const actions = {
|
||||
...storeActions,
|
||||
async refresh() {
|
||||
@@ -53,11 +56,12 @@ export const useStore = function <T extends BoundT>(
|
||||
},
|
||||
flushStore() {
|
||||
store.value = [];
|
||||
initialized.value = false;
|
||||
},
|
||||
};
|
||||
|
||||
// initial hydration
|
||||
if (!loading.value && !store.value.length) {
|
||||
if (!loading.value && !initialized.value) {
|
||||
actions.refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function cont(x: number, D: number, mixed: boolean) {
|
||||
let Q_2 = 1;
|
||||
let Q_1 = 0;
|
||||
let Q = 0;
|
||||
let A = Math.floor(B);
|
||||
let A: number;
|
||||
while (Q_1 < D) {
|
||||
A = Math.floor(B);
|
||||
P = A * P_1 + P_2;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { IngredientFood, RecipeSummary, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
|
||||
|
||||
export const MOCK_ITEM: ShoppingListItemOut = {
|
||||
shoppingListId: "",
|
||||
id: "",
|
||||
groupId: "",
|
||||
householdId: "",
|
||||
display: "MOCK_ITEM",
|
||||
updatedAt: "100",
|
||||
position: 1,
|
||||
checked: false,
|
||||
createdAt: "100",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE: RecipeSummary = {
|
||||
id: "recipe-id",
|
||||
name: "Recipe!",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE2: RecipeSummary = {
|
||||
...MOCK_RECIPE,
|
||||
id: undefined,
|
||||
name: "Recipe 2!",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD: IngredientFood = {
|
||||
id: "1",
|
||||
name: "food 1",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD2: IngredientFood = {
|
||||
id: "2",
|
||||
name: "food 2",
|
||||
};
|
||||
|
||||
export const MOCK_LABEL: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_LABEL2: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL2",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_SHOPPING_LIST: ShoppingListOut = {
|
||||
groupId: "",
|
||||
userId: "",
|
||||
id: "",
|
||||
householdId: "",
|
||||
labelSettings: [
|
||||
MOCK_LABEL,
|
||||
MOCK_LABEL2,
|
||||
],
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
recipeReferences: [{
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE,
|
||||
}, {
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE2,
|
||||
}],
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import * as vueusecore from "@vueuse/core";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListCopy } from "../use-shopping-list-copy";
|
||||
import { MOCK_ITEM } from "./mocks";
|
||||
|
||||
vi.mock("@vueuse/core", { spy: true });
|
||||
|
||||
const mockCopy = vi.fn().mockImplementation(args => new Promise(resolve => resolve(args)));
|
||||
|
||||
vi.mocked(vueusecore.useClipboard).mockImplementation(() => {
|
||||
return {
|
||||
isSupported: computed(() => true),
|
||||
copied: computed(() => true),
|
||||
text: computed(() => ""),
|
||||
copy: mockCopy,
|
||||
};
|
||||
});
|
||||
const wrapper = () => makeWrapper(useShoppingListCopy);
|
||||
|
||||
const TEST_HEADER = "SPECIAL HEADER!";
|
||||
|
||||
const MOCK_LIST: { [key: string]: ShoppingListItemOut[] } = {
|
||||
[TEST_HEADER]: [MOCK_ITEM],
|
||||
[TEST_HEADER + "2"]: [MOCK_ITEM],
|
||||
};
|
||||
|
||||
describe("Shopping list copy composable", () => {
|
||||
describe("copyListItems", () => {
|
||||
test("copies markdown lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "markdown");
|
||||
const expected = [
|
||||
"# SPECIAL HEADER!",
|
||||
"- [ ] MOCK_ITEM",
|
||||
"",
|
||||
"# SPECIAL HEADER!2",
|
||||
"- [ ] MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
test("copies plain text lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "plain");
|
||||
const expected = [
|
||||
"[SPECIAL HEADER!]",
|
||||
"MOCK_ITEM",
|
||||
"",
|
||||
"[SPECIAL HEADER!2]",
|
||||
"MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedLabelHeading", () => {
|
||||
test("copies markdown headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("markdown", TEST_HEADER);
|
||||
expect(header).toEqual(`# ${TEST_HEADER}`);
|
||||
});
|
||||
test("copies plain text headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("plain", TEST_HEADER);
|
||||
expect(header).toEqual(`[${TEST_HEADER}]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedListItem", () => {
|
||||
test("copies markdown items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("markdown", MOCK_ITEM);
|
||||
expect(header).toEqual(`- [ ] ${MOCK_ITEM.display}`);
|
||||
});
|
||||
test("copies plain text items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", MOCK_ITEM);
|
||||
expect(header).toEqual(MOCK_ITEM.display);
|
||||
});
|
||||
test("copies items without a display as empty", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", { ...MOCK_ITEM, display: undefined });
|
||||
expect(header).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListSorting } from "../use-shopping-list-sorting";
|
||||
import { MOCK_FOOD, MOCK_FOOD2, MOCK_ITEM, MOCK_LABEL, MOCK_LABEL2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = () => makeWrapper(() => {
|
||||
const { t } = useI18n();
|
||||
return {
|
||||
t,
|
||||
...useShoppingListSorting(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-sorting", () => {
|
||||
describe("sortItems", () => {
|
||||
const { sortItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: 0 }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts by createdAt next", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: "0" });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: "0" }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts similar items into the same spot", () => {
|
||||
const result = sortItems(MOCK_ITEM, MOCK_ITEM);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
});
|
||||
describe("sortListItems", () => {
|
||||
const { sortListItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: [MOCK_ITEM, { ...MOCK_ITEM, position: 0 }, { ...MOCK_ITEM, createdAt: "0" }] };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, position: 0 },
|
||||
{ ...MOCK_ITEM, createdAt: "0" },
|
||||
MOCK_ITEM,
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe("updateItemsByLabel", () => {
|
||||
const { updateItemsByLabel, t } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("ignores checked items", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("returns unordered labels if no ordering is specified", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST,
|
||||
labelSettings: undefined,
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("groupAndSortListItemsByFood", () => {
|
||||
const { groupAndSortListItemsByFood } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(MOCK_SHOPPING_LIST.listItems);
|
||||
});
|
||||
test("groups checked items together", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2 },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("populates position and created at if not present", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList: ShoppingListOut = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListState } from "../use-shopping-list-state";
|
||||
import { MOCK_ITEM, MOCK_RECIPE, MOCK_RECIPE2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = (list: ShoppingListOut = MOCK_SHOPPING_LIST) => makeWrapper(() => {
|
||||
const { shoppingList, ...state } = useShoppingListState();
|
||||
shoppingList.value = list;
|
||||
return {
|
||||
shoppingList,
|
||||
...state,
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-state", () => {
|
||||
describe("checked items are sorted", () => {
|
||||
const { sortCheckedItems } = wrapper();
|
||||
|
||||
test("by timestamp", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "200" });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "0" });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
});
|
||||
test("by position if timestamps match", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 2 });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const sorted3 = sortCheckedItems({ ...MOCK_ITEM, position: undefined }, { ...MOCK_ITEM, position: undefined });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
expect(sorted3).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recipeMap", () => {
|
||||
test("Updates to match shopping list recipe references", () => {
|
||||
const { recipeMap } = wrapper();
|
||||
expect(recipeMap).toEqual(new Map([
|
||||
[MOCK_RECIPE.id, MOCK_RECIPE],
|
||||
["", MOCK_RECIPE2],
|
||||
]));
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const { recipeMap } = wrapper({ ...MOCK_SHOPPING_LIST, recipeReferences: undefined });
|
||||
expect(recipeMap).toEqual(new Map([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("checked and unchecked items", () => {
|
||||
test("update appropriately", () => {
|
||||
const mockCheckedItem = { ...MOCK_ITEM, checked: true };
|
||||
const { listItems: { checked, unchecked } } = wrapper({
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
mockCheckedItem,
|
||||
],
|
||||
});
|
||||
expect(unchecked[0]).toEqual(MOCK_ITEM);
|
||||
expect(checked[0]).toEqual(mockCheckedItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ModelRef } from "vue";
|
||||
import type { ShoppingListItemOut, ShoppingListItemCreate } from "~/lib/api/types/household";
|
||||
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "../store";
|
||||
|
||||
export function useShoppingListItemEditor(listItem: ModelRef<ShoppingListItemOut | ShoppingListItemCreate, string, ShoppingListItemOut | ShoppingListItemCreate, ShoppingListItemOut | ShoppingListItemCreate>) {
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
|
||||
const unitStore = useUnitStore();
|
||||
const unitData = useUnitData();
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val } as any);
|
||||
|
||||
foodData.data.name = val;
|
||||
const newFood = await foodStore.actions.createOne(foodData.data);
|
||||
if (newFood) {
|
||||
listItem.value.food = newFood;
|
||||
listItem.value.foodId = newFood.id;
|
||||
}
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
async function createAssignUnit(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val } as any);
|
||||
|
||||
unitData.data.name = val;
|
||||
const newUnit = await unitStore.actions.createOne(unitData.data);
|
||||
if (newUnit) {
|
||||
listItem.value.unit = newUnit;
|
||||
listItem.value.unitId = newUnit.id;
|
||||
}
|
||||
unitData.reset();
|
||||
}
|
||||
|
||||
async function assignLabelToFood() {
|
||||
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
listItem.value.food.labelId = listItem.value.labelId;
|
||||
await foodStore.actions.updateOne(listItem.value.food);
|
||||
}
|
||||
|
||||
return {
|
||||
assignLabelToFood,
|
||||
createAssignFood,
|
||||
createAssignUnit,
|
||||
};
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<RecipeCategory[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetCategoryStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useCategoryData = function () {
|
||||
@@ -23,10 +27,10 @@ export const useCategoryData = function () {
|
||||
|
||||
export const useCategoryStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeCategory>("category", store, loading, api.categories);
|
||||
return useStore<RecipeCategory>("category", store, loading, initialized, api.categories);
|
||||
};
|
||||
|
||||
export const usePublicCategoryStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeCategory>("category", store, publicLoading, api.categories);
|
||||
return useReadOnlyStore<RecipeCategory>("category", store, publicLoading, publicInitialized, api.categories);
|
||||
};
|
||||
|
||||
@@ -5,17 +5,21 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const cookbooks: Ref<ReadCookBook[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetCookbookStore() {
|
||||
cookbooks.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useCookbookStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);
|
||||
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, initialized, api.cookbooks);
|
||||
|
||||
const updateAll = async function (updateData: UpdateCookBook[]) {
|
||||
loading.value = true;
|
||||
@@ -31,5 +35,5 @@ export const useCookbookStore = function (i18n?: Composer) {
|
||||
|
||||
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<ReadCookBook>("cookbook", cookbooks, publicLoading, api.cookbooks);
|
||||
return useReadOnlyStore<ReadCookBook>("cookbook", cookbooks, publicLoading, publicInitialized, api.cookbooks);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,16 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<IngredientFood[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetFoodStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useFoodData = function () {
|
||||
@@ -24,10 +28,10 @@ export const useFoodData = function () {
|
||||
|
||||
export const useFoodStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<IngredientFood>("food", store, loading, api.foods);
|
||||
return useStore<IngredientFood>("food", store, loading, initialized, api.foods);
|
||||
};
|
||||
|
||||
export const usePublicFoodStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<IngredientFood>("food", store, publicLoading, api.foods);
|
||||
return useReadOnlyStore<IngredientFood>("food", store, publicLoading, publicInitialized, api.foods);
|
||||
};
|
||||
|
||||
@@ -5,20 +5,24 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<HouseholdSummary[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetHouseholdStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useHouseholdStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);
|
||||
return useReadOnlyStore<HouseholdSummary>("household", store, loading, initialized, api.households);
|
||||
};
|
||||
|
||||
export const usePublicHouseholdStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<HouseholdSummary>("household-public", store, publicLoading, api.households);
|
||||
return useReadOnlyStore<HouseholdSummary>("household-public", store, publicLoading, publicInitialized, api.households);
|
||||
};
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetLabelStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
export const useLabelData = function () {
|
||||
@@ -22,5 +24,5 @@ export const useLabelData = function () {
|
||||
|
||||
export const useLabelStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<MultiPurposeLabelOut>("label", store, loading, api.multiPurposeLabels);
|
||||
return useStore<MultiPurposeLabelOut>("label", store, loading, initialized, api.multiPurposeLabels);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,16 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<RecipeTag[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetTagStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useTagData = function () {
|
||||
@@ -23,10 +27,10 @@ export const useTagData = function () {
|
||||
|
||||
export const useTagStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeTag>("tag", store, loading, api.tags);
|
||||
return useStore<RecipeTag>("tag", store, loading, initialized, api.tags);
|
||||
};
|
||||
|
||||
export const usePublicTagStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeTag>("tag", store, publicLoading, api.tags);
|
||||
return useReadOnlyStore<RecipeTag>("tag", store, publicLoading, publicInitialized, api.tags);
|
||||
};
|
||||
|
||||
@@ -9,12 +9,16 @@ interface RecipeToolWithOnHand extends RecipeTool {
|
||||
|
||||
const store: Ref<RecipeTool[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetToolStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useToolData = function () {
|
||||
@@ -29,10 +33,10 @@ export const useToolData = function () {
|
||||
|
||||
export const useToolStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeTool>("tool", store, loading, api.tools);
|
||||
return useStore<RecipeTool>("tool", store, loading, initialized, api.tools);
|
||||
};
|
||||
|
||||
export const usePublicToolStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeTool>("tool", store, publicLoading, api.tools);
|
||||
return useReadOnlyStore<RecipeTool>("tool", store, publicLoading, publicInitialized, api.tools);
|
||||
};
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<IngredientUnit[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetUnitStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
export const useUnitData = function () {
|
||||
@@ -23,5 +25,5 @@ export const useUnitData = function () {
|
||||
|
||||
export const useUnitStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<IngredientUnit>("unit", store, loading, api.units);
|
||||
return useStore<IngredientUnit>("unit", store, loading, initialized, api.units);
|
||||
};
|
||||
|
||||
@@ -6,10 +6,12 @@ import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||
|
||||
const store: Ref<UserSummary[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetUserStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
|
||||
@@ -21,5 +23,5 @@ export const useUserStore = function (i18n?: Composer) {
|
||||
const requests = useRequests(i18n);
|
||||
const api = new GroupUserAPIReadOnly(requests);
|
||||
|
||||
return useReadOnlyStore<UserSummary>("user", store, loading, api, { orderBy: "full_name" });
|
||||
return useReadOnlyStore<UserSummary>("user", store, loading, initialized, api, { orderBy: "full_name" });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
|
||||
export function useAIProviders() {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
async function getOne(id: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
return await api.aiProviders.getOne(id);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOne(payload: AIProviderCreate) {
|
||||
loading.value = true;
|
||||
try {
|
||||
return await api.aiProviders.createOne(payload);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOne(id: string, payload: AIProviderUpdate) {
|
||||
loading.value = true;
|
||||
try {
|
||||
return await api.aiProviders.updateOne(id, payload);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteOne(id: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
return await api.aiProviders.deleteOne(id);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: readonly(loading),
|
||||
getOne,
|
||||
createOne,
|
||||
updateOne,
|
||||
deleteOne,
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,10 @@ const allAnnouncements: Announcement[] = Object.entries(_announcementsUnsorted)
|
||||
.map(([path, mod]) => {
|
||||
const key = path.split("/").at(-1)!.replace(".vue", "");
|
||||
|
||||
const parsed = new Date(key.split("_", 1)[0]!);
|
||||
const dateParts = key.split("_", 1)[0]!.split("-").map(Number);
|
||||
const parsed = dateParts.length === 3
|
||||
? new Date(dateParts[0]!, dateParts[1]! - 1, dateParts[2]!)
|
||||
: new Date(NaN);
|
||||
const date = isNaN(parsed.getTime()) ? undefined : parsed;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref, computed } from "vue";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
import { clearAllStores } from "~/composables/store";
|
||||
import { getTokenCookieOptions } from "~/composables/use-token-cookie";
|
||||
|
||||
interface AuthData {
|
||||
value: UserOut | null;
|
||||
@@ -25,15 +26,12 @@ const authUser = ref<UserOut | null>(null);
|
||||
const authStatus = ref<"loading" | "authenticated" | "unauthenticated">("loading");
|
||||
|
||||
export const useAuthBackend = function (): AuthState {
|
||||
const { $appInfo, $axios } = useNuxtApp();
|
||||
const { $axios } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const tokenName = runtimeConfig.public.AUTH_TOKEN;
|
||||
const tokenCookie = useCookie(tokenName, {
|
||||
maxAge: $appInfo.tokenTime * 60 * 60,
|
||||
secure: $appInfo.production && window?.location?.protocol === "https:",
|
||||
});
|
||||
const tokenCookie = useCookie(tokenName, getTokenCookieOptions());
|
||||
|
||||
function setToken(token: string | null) {
|
||||
tokenCookie.value = token;
|
||||
|
||||
@@ -6,7 +6,12 @@ const loading = ref(false);
|
||||
|
||||
export const useGroupSelf = function () {
|
||||
const api = useUserApi();
|
||||
const auth = useMealieAuth();
|
||||
async function refreshGroupSelf() {
|
||||
if (!auth.user.value) {
|
||||
groupSelfRef.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const { data } = await api.groups.getCurrentUserGroup();
|
||||
groupSelfRef.value = data;
|
||||
@@ -37,6 +42,25 @@ export const useGroupSelf = function () {
|
||||
|
||||
return data || undefined;
|
||||
},
|
||||
async updateAIProviderSettings() {
|
||||
if (!groupSelfRef.value) {
|
||||
await refreshGroupSelf();
|
||||
}
|
||||
if (!groupSelfRef.value?.aiProviderSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.groups.setAIProviderSettings(groupSelfRef.value.aiProviderSettings);
|
||||
|
||||
if (data) {
|
||||
groupSelfRef.value.aiProviderSettings = data;
|
||||
}
|
||||
|
||||
return data || undefined;
|
||||
},
|
||||
async refresh() {
|
||||
await refreshGroupSelf();
|
||||
},
|
||||
};
|
||||
|
||||
const group = actions.get();
|
||||
|
||||
@@ -6,8 +6,13 @@ const loading = ref(false);
|
||||
|
||||
export const useHouseholdSelf = function () {
|
||||
const api = useUserApi();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
async function refreshHouseholdSelf() {
|
||||
if (!auth.user.value) {
|
||||
householdSelfRef.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const { data } = await api.households.getCurrentUserHousehold();
|
||||
householdSelfRef.value = data;
|
||||
|
||||
@@ -3,224 +3,224 @@ export const LOCALES = [
|
||||
{
|
||||
name: "繁體中文 (Chinese traditional)",
|
||||
value: "zh-TW",
|
||||
progress: 72,
|
||||
progress: 97,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "简体中文 (Chinese simplified)",
|
||||
value: "zh-CN",
|
||||
progress: 27,
|
||||
progress: 54,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Tiếng Việt (Vietnamese)",
|
||||
value: "vi-VN",
|
||||
progress: 1,
|
||||
progress: 2,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Українська (Ukrainian)",
|
||||
value: "uk-UA",
|
||||
progress: 60,
|
||||
progress: 85,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Türkçe (Turkish)",
|
||||
value: "tr-TR",
|
||||
progress: 28,
|
||||
progress: 53,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Svenska (Swedish)",
|
||||
value: "sv-SE",
|
||||
progress: 46,
|
||||
progress: 76,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "српски (Serbian)",
|
||||
value: "sr-SP",
|
||||
progress: 72,
|
||||
progress: 99,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Slovenščina (Slovenian)",
|
||||
value: "sl-SI",
|
||||
progress: 30,
|
||||
progress: 57,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Slovenčina (Slovak)",
|
||||
value: "sk-SK",
|
||||
progress: 34,
|
||||
progress: 64,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Pусский (Russian)",
|
||||
value: "ru-RU",
|
||||
progress: 32,
|
||||
progress: 58,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Română (Romanian)",
|
||||
value: "ro-RO",
|
||||
progress: 33,
|
||||
progress: 60,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Português (Portuguese)",
|
||||
value: "pt-PT",
|
||||
progress: 30,
|
||||
progress: 56,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Português do Brasil (Brazilian Portuguese)",
|
||||
value: "pt-BR",
|
||||
progress: 72,
|
||||
progress: 98,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Polski (Polish)",
|
||||
value: "pl-PL",
|
||||
progress: 72,
|
||||
progress: 98,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Norsk (Norwegian)",
|
||||
value: "no-NO",
|
||||
progress: 32,
|
||||
progress: 59,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Nederlands (Dutch)",
|
||||
value: "nl-NL",
|
||||
progress: 70,
|
||||
progress: 98,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Latviešu (Latvian)",
|
||||
value: "lv-LV",
|
||||
progress: 27,
|
||||
progress: 53,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Lietuvių (Lithuanian)",
|
||||
value: "lt-LT",
|
||||
progress: 22,
|
||||
progress: 42,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "한국어 (Korean)",
|
||||
value: "ko-KR",
|
||||
progress: 28,
|
||||
progress: 54,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "日本語 (Japanese)",
|
||||
value: "ja-JP",
|
||||
progress: 26,
|
||||
progress: 50,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Italiano (Italian)",
|
||||
value: "it-IT",
|
||||
progress: 45,
|
||||
progress: 73,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Íslenska (Icelandic)",
|
||||
value: "is-IS",
|
||||
progress: 31,
|
||||
progress: 57,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Magyar (Hungarian)",
|
||||
value: "hu-HU",
|
||||
progress: 34,
|
||||
progress: 62,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Hrvatski (Croatian)",
|
||||
value: "hr-HR",
|
||||
progress: 21,
|
||||
progress: 42,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "עברית (Hebrew)",
|
||||
value: "he-IL",
|
||||
progress: 46,
|
||||
progress: 72,
|
||||
dir: "rtl",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Galego (Galician)",
|
||||
value: "gl-ES",
|
||||
progress: 27,
|
||||
progress: 52,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Français (French)",
|
||||
value: "fr-FR",
|
||||
progress: 55,
|
||||
progress: 83,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Français canadien (Canadian French)",
|
||||
value: "fr-CA",
|
||||
progress: 60,
|
||||
progress: 90,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Belge (Belgian)",
|
||||
value: "fr-BE",
|
||||
progress: 28,
|
||||
progress: 78,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Suomi (Finnish)",
|
||||
value: "fi-FI",
|
||||
progress: 72,
|
||||
progress: 98,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Eesti (Estonian)",
|
||||
value: "et-EE",
|
||||
progress: 32,
|
||||
progress: 58,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Español (Spanish)",
|
||||
value: "es-ES",
|
||||
progress: 35,
|
||||
progress: 64,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
@@ -234,63 +234,63 @@ export const LOCALES = [
|
||||
{
|
||||
name: "British English",
|
||||
value: "en-GB",
|
||||
progress: 32,
|
||||
progress: 36,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "without-unit",
|
||||
},
|
||||
{
|
||||
name: "Ελληνικά (Greek)",
|
||||
value: "el-GR",
|
||||
progress: 33,
|
||||
progress: 58,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Deutsch (German)",
|
||||
value: "de-DE",
|
||||
progress: 72,
|
||||
progress: 99,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Dansk (Danish)",
|
||||
value: "da-DK",
|
||||
progress: 72,
|
||||
progress: 100,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Čeština (Czech)",
|
||||
value: "cs-CZ",
|
||||
progress: 31,
|
||||
progress: 60,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Català (Catalan)",
|
||||
value: "ca-ES",
|
||||
progress: 33,
|
||||
progress: 59,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Български (Bulgarian)",
|
||||
value: "bg-BG",
|
||||
progress: 44,
|
||||
progress: 72,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "العربية (Arabic)",
|
||||
value: "ar-SA",
|
||||
progress: 71,
|
||||
progress: 97,
|
||||
dir: "rtl",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Afrikaans (Afrikaans)",
|
||||
value: "af-ZA",
|
||||
progress: 18,
|
||||
progress: 36,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
|
||||
@@ -73,11 +73,11 @@ function createRecipeExplorerSearchState(groupSlug: ComputedRef<string>): Recipe
|
||||
});
|
||||
|
||||
// Store references
|
||||
const categories = isOwnGroup ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||
const foods = isOwnGroup ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||
const households = isOwnGroup ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
||||
const tags = isOwnGroup ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||
const tools = isOwnGroup ? useToolStore() : usePublicToolStore(groupSlug.value);
|
||||
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||
const households = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
||||
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||
const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
|
||||
|
||||
// Selected items
|
||||
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
|
||||
|
||||
@@ -3,6 +3,11 @@ interface Toast {
|
||||
text: string;
|
||||
title: string | null;
|
||||
color: string;
|
||||
timeout?: number;
|
||||
action?: {
|
||||
onClick: VoidFunction;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const toastAlert = reactive<Toast>({
|
||||
@@ -19,11 +24,13 @@ export const toastLoading = reactive<Toast>({
|
||||
color: "success",
|
||||
});
|
||||
|
||||
function setToast(toast: Toast, text: string, title: string | null, color: string) {
|
||||
function setToast(toast: Toast, text: string, title: string | null, color: string, options?: Partial<Toast>) {
|
||||
toast.open = true;
|
||||
toast.text = text;
|
||||
toast.title = title;
|
||||
toast.color = color;
|
||||
toast.timeout = options?.timeout;
|
||||
toast.action = options?.action;
|
||||
}
|
||||
|
||||
export const loader = {
|
||||
@@ -45,17 +52,17 @@ export const loader = {
|
||||
};
|
||||
|
||||
export const alert = {
|
||||
info(text: string, title: string | null = null) {
|
||||
setToast(toastAlert, text, title, "info");
|
||||
info(text: string, title: string | null = null, options?: Partial<Toast>) {
|
||||
setToast(toastAlert, text, title, "info", options);
|
||||
},
|
||||
success(text: string, title: string | null = null) {
|
||||
setToast(toastAlert, text, title, "success");
|
||||
success(text: string, title: string | null = null, options?: Partial<Toast>) {
|
||||
setToast(toastAlert, text, title, "success", options);
|
||||
},
|
||||
error(text: string, title: string | null = null) {
|
||||
setToast(toastAlert, text, title, "error");
|
||||
error(text: string, title: string | null = null, options?: Partial<Toast>) {
|
||||
setToast(toastAlert, text, title, "error", options);
|
||||
},
|
||||
warning(text: string, title: string | null = null) {
|
||||
setToast(toastAlert, text, title, "warning");
|
||||
warning(text: string, title: string | null = null, options?: Partial<Toast>) {
|
||||
setToast(toastAlert, text, title, "warning", options);
|
||||
},
|
||||
close() {
|
||||
toastAlert.open = false;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getTokenCookieOptions() {
|
||||
const isSecureConnection = useNuxtApp().$appInfo.production && window?.location?.protocol === "https:";
|
||||
return {
|
||||
maxAge: useNuxtApp().$appInfo.tokenTime * 60 * 60,
|
||||
secure: isSecureConnection,
|
||||
sameSite: (isSecureConnection ? "none" : "lax") as "none" | "lax",
|
||||
partitioned: isSecureConnection,
|
||||
};
|
||||
}
|
||||
@@ -73,6 +73,10 @@ export interface UserActivityPreferences {
|
||||
defaultActivity: ActivityKey;
|
||||
}
|
||||
|
||||
export interface UserExperiencePreferences {
|
||||
lockScreen: boolean;
|
||||
}
|
||||
|
||||
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
||||
const fromStorage = useLocalStorage(
|
||||
"meal-planner-preferences",
|
||||
@@ -81,9 +85,7 @@ export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
||||
numberOfDays: 7,
|
||||
},
|
||||
{ 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<UserMealPlanPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -92,15 +94,14 @@ export function useUserPrintPreferences(): Ref<UserPrintPreferences> {
|
||||
const fromStorage = useLocalStorage(
|
||||
"recipe-print-preferences",
|
||||
{
|
||||
imagePosition: "left",
|
||||
imagePosition: "left" as ImagePosition,
|
||||
showDescription: true,
|
||||
showNotes: true,
|
||||
showNutrition: false,
|
||||
expandChildRecipes: false,
|
||||
},
|
||||
{ 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<UserPrintPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -118,9 +119,7 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
|
||||
useMobileCards: false,
|
||||
},
|
||||
{ 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<UserRecipePreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -132,9 +131,7 @@ export function useUserActivityPreferences(): Ref<UserActivityPreferences> {
|
||||
defaultActivity: ActivityKey.RECIPES,
|
||||
},
|
||||
{ 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 Ref<UserActivityPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -146,9 +143,7 @@ export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
|
||||
recipe: "",
|
||||
},
|
||||
{ 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<UserSearchQuery>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -160,9 +155,7 @@ export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
|
||||
viewAllLists: false,
|
||||
},
|
||||
{ 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<UserShoppingListPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -175,9 +168,7 @@ export function useTimelinePreferences(): Ref<UserTimelinePreferences> {
|
||||
types: ["info", "system", "comment"] as TimelineEventType[],
|
||||
},
|
||||
{ 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<UserTimelinePreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -186,12 +177,10 @@ export function useParsingPreferences(): Ref<UserParsingPreferences> {
|
||||
const fromStorage = useLocalStorage(
|
||||
"parsing-preferences",
|
||||
{
|
||||
parser: "nlp",
|
||||
parser: "nlp" as RegisteredParser,
|
||||
},
|
||||
{ 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<UserParsingPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -203,9 +192,7 @@ export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {
|
||||
hideOtherHouseholds: false,
|
||||
},
|
||||
{ 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<UserCookbooksPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -224,9 +211,7 @@ export function useRecipeFinderPreferences(): Ref<UserRecipeFinderPreferences> {
|
||||
includeToolsOnHand: 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<UserRecipeFinderPreferences>;
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
@@ -241,9 +226,19 @@ export function useRecipeCreatePreferences(): Ref<UserRecipeCreatePreferences> {
|
||||
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;
|
||||
}
|
||||
|
||||
export function useUserExperiencePreferences(): Ref<UserExperiencePreferences> {
|
||||
const fromStorage = useLocalStorage(
|
||||
"user-experience-preferences",
|
||||
{
|
||||
lockScreen: true,
|
||||
},
|
||||
{ mergeDefaults: true },
|
||||
);
|
||||
|
||||
return fromStorage;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Beheerpaneel",
|
||||
"delete": "Verwyder",
|
||||
"disabled": "Afkeskakel",
|
||||
"done": "Done",
|
||||
"download": "Laai af",
|
||||
"duplicate": "Dupliseer",
|
||||
"edit": "Wysig",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Dinsdag",
|
||||
"type": "Tipe",
|
||||
"undo": "Undo",
|
||||
"update": "Wysig",
|
||||
"updated": "Opgedateer",
|
||||
"upload": "Laai op",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Wys uitgebreide",
|
||||
"add-field": "Voeg veld by",
|
||||
"date-created": "Datum Geskep",
|
||||
"date-updated": "Datum Opgedateer"
|
||||
"date-updated": "Datum Opgedateer",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Is jy seker jy wil <b>{groupName}<b/> uitvee?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Veranderinge aan hierdie groep sal onmiddellik weerspieël word.",
|
||||
"group-id-value": "Groep-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": "You must select a group before selecting a household",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Household",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Voer in met .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"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.",
|
||||
"create-from-images": "Create from Images",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Hoeveelheid: {0}",
|
||||
"shopping-list": "Inkopielys",
|
||||
"shopping-lists": "Inkopielyste",
|
||||
"add-item": "Add item",
|
||||
"food": "Voedsel",
|
||||
"note": "Nota",
|
||||
"label": "Etiket",
|
||||
@@ -939,7 +977,8 @@
|
||||
"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-delete-checked-items": "Are you sure you want to delete all checked items?",
|
||||
"no-shopping-lists-found": "No Shopping Lists Found"
|
||||
"no-shopping-lists-found": "No Shopping Lists Found",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle resepte",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Die kombinasie van die geselekteerde kosse sal die oorspronklike kos en die nuwe kos kombineer. Die oorspronklike kos sal verwyder word en alle verwysings sal opgedateer word om na die nuwe kos te wys.",
|
||||
"merge-food-example": "Voeg {food1} saam met {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "I'm already set up, just bring me to the homepage",
|
||||
"common-settings-for-new-sites": "Here are some common settings for new sites",
|
||||
"setup-complete": "Setup Complete!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie",
|
||||
"restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.",
|
||||
"manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"min-length": "Must Be At Least {min} Characters",
|
||||
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "لوحة المعلومات",
|
||||
"delete": "حذف",
|
||||
"disabled": "معطَّل",
|
||||
"done": "Done",
|
||||
"download": "تحميل",
|
||||
"duplicate": "استنساخ",
|
||||
"edit": "تعديل",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "الرمز التعريفي",
|
||||
"tuesday": "الثلاثاء",
|
||||
"type": "النوع",
|
||||
"undo": "Undo",
|
||||
"update": "تحديث",
|
||||
"updated": "محدث",
|
||||
"upload": "تحميل",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "إظهار متقدمة",
|
||||
"add-field": "إضافة حقل",
|
||||
"date-created": "تاريخ الإنشاء",
|
||||
"date-updated": "تاريخ التحديث"
|
||||
"date-updated": "تاريخ التحديث",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "هل انت متأكد من رغبتك في حذف <b>{groupName}<b/>؟",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "التغييرات التي ستطرأ على هذه المجموعة ستنعكس على الفور.",
|
||||
"group-id-value": "معرف المجموعة: {0}",
|
||||
"total-households": "مجموع المنزل",
|
||||
"you-must-select-a-group-before-selecting-a-household": "يجب عليك تحديد مجموعة قبل تحديد المنزل"
|
||||
"you-must-select-a-group-before-selecting-a-household": "يجب عليك تحديد مجموعة قبل تحديد المنزل",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "المنزل",
|
||||
@@ -429,7 +466,7 @@
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "صندوق وصفاتي",
|
||||
"description-long": ""
|
||||
"description-long": "Mealie can import recipes from My Recipe Box. Export your recipes in CSV format, then upload the .csv file below."
|
||||
},
|
||||
"recipekeeper": {
|
||||
"title": "مدير الوصفة",
|
||||
@@ -573,11 +610,11 @@
|
||||
"yield-text": "نص الإرجاع",
|
||||
"quantity": "الكَمّيَّة",
|
||||
"choose-unit": "اختر الوحدة",
|
||||
"press-enter-to-create": "",
|
||||
"press-enter-to-create": "Press Enter to Create",
|
||||
"choose-food": "اختيار الطعام",
|
||||
"choose-recipe": "اختر وصفة",
|
||||
"notes": "ملاحظات",
|
||||
"toggle-section": "",
|
||||
"toggle-section": "Toggle Section",
|
||||
"see-original-text": "عرض النص الأصلي",
|
||||
"original-text-with-value": "النص الأصلي: {originalText}",
|
||||
"ingredient-linker": "رابط المكون",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "إنشاء وصفة جديدة من الصفر.",
|
||||
"create-recipes": "إنشاء الوصفات",
|
||||
"import-with-zip": "الاستيراد باستخدام zip.",
|
||||
"create-recipe-from-an-image": "إنشاء وصفة عن طريق صورة",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"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": "قم بقص الصورة وتدويرها بحيث يظهر النص فقط، ويكون في الاتجاه الصحيح.",
|
||||
"create-from-images": "إنشاء عن طريق صور",
|
||||
@@ -885,7 +922,7 @@
|
||||
"application-version-error-text": "الإصدار الحالي الخاص بك ({0}) لا يتطابق مع الإصدار الأخير. انظر في التحديث إلى الإصدار الأحدث ({1}).",
|
||||
"mealie-is-up-to-date": "Malie على آخر تحديث",
|
||||
"secure-site": "موقع آمن",
|
||||
"secure-site-error-text": "",
|
||||
"secure-site-error-text": "Serve via localhost or secure with https. Clipboard and additional browser APIs may not work.",
|
||||
"secure-site-success-text": "يتم الوصول إلى الموقع بواسطة localhost أو peps",
|
||||
"server-side-base-url": "الرابط الأساسي للخادم",
|
||||
"server-side-base-url-error-text": "'BASE_URL' لا يزال القيمة الافتراضية على خادم API. وهذا سيسبب مشاكل مع روابط الإشعارات التي تم إنشاؤها على الخادم لرسائل البريد الإلكتروني، إلخ.",
|
||||
@@ -915,9 +952,10 @@
|
||||
"quantity": "الكَمّيَّة: {0}",
|
||||
"shopping-list": "قائمة التسوق",
|
||||
"shopping-lists": "قوائم التسوق",
|
||||
"add-item": "إضافة عنصر",
|
||||
"food": "الطعام",
|
||||
"note": "ملاحظة",
|
||||
"label": "",
|
||||
"label": "Label",
|
||||
"save-label": "حفظ التصنيف",
|
||||
"linked-item-warning": "هذا العنصر مرتبط بوصفة واحدة أو أكثر. تعديل الوحدات أو الأطعمة سوف يسفر عن نتائج غير متوقعة عند إضافة أو إزالة الوصفة من هذه القائمة.",
|
||||
"toggle-food": "تبديل الطعام",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "هل أنت متأكد من أنك تريد تحديد جميع العناصر؟",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "هل أنت متأكد من أنك تريد إلغاء تحديد جميع العناصر؟",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "هل أنت متأكد أنك تريد حذف جميع العناصر المحددة؟",
|
||||
"no-shopping-lists-found": "لم يتم العثور على قوائم تسوق"
|
||||
"no-shopping-lists-found": "لم يتم العثور على قوائم تسوق",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "جميع الوصفات",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "سيؤدي دمج الأطعمة المختارة إلى دمج الطعام الأصلي والطعام المستهدف في طعام واحد. سيتم حذف الطعام الأصلي، وسيتم تحديث جميع الإشارات إليه لتشير إلى الطعام المستهدف.",
|
||||
"merge-food-example": "دمج {food1} مع {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "أنا بالفعل جاهز، فقط أحضر ني إلى الصفحة الرئيسية",
|
||||
"common-settings-for-new-sites": "فيما يلي بعض الإعدادات الشائعة للمواقع الجديدة",
|
||||
"setup-complete": "تمت الإعدادات!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "إليك بعض الأشياء لمساعدتك على البدء مع ميالي",
|
||||
"restore-from-v1-backup": "لديك نسخة احتياطية من مثيل سابق لـ Mealie v1؟ يمكنك استعادتها هنا.",
|
||||
"manage-profile-or-get-invite-link": "إدارة الملف الشخصي الخاص بك، أو التقط رابط دعوة للمشاركة مع الآخرين."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "لا يسمح باستخدام المسافات",
|
||||
"min-length": "يجب أن يكون على الأقل {min} أحرف",
|
||||
"max-length": "يجب أن لا يتجاوز {max} حرف يجب أن يكون على الأكثر {max}أحرف "
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "وضع علامة مقروء على الكل",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Табло",
|
||||
"delete": "Изтриване",
|
||||
"disabled": "Изключено",
|
||||
"done": "Готово",
|
||||
"download": "Изтегли",
|
||||
"duplicate": "Дублиране",
|
||||
"edit": "Редактирай",
|
||||
@@ -168,10 +169,11 @@
|
||||
"token": "Токен",
|
||||
"tuesday": "Вторник",
|
||||
"type": "Тип",
|
||||
"undo": "Отмяна",
|
||||
"update": "Актуализация",
|
||||
"updated": "Последно обновени",
|
||||
"upload": "Качи",
|
||||
"url": "URL",
|
||||
"url": "URL АДРЕС",
|
||||
"view": "Преглед",
|
||||
"wednesday": "Сряда",
|
||||
"yes": "Да",
|
||||
@@ -182,7 +184,7 @@
|
||||
"start": "Начало",
|
||||
"toggle-view": "Смяна на изгледа",
|
||||
"date": "Дата",
|
||||
"id": "Id",
|
||||
"id": "Идентификатор",
|
||||
"owner": "Собственик",
|
||||
"change-owner": "Промени собственика",
|
||||
"date-added": "Дата на добавяне",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Разширени настройки",
|
||||
"add-field": "Добави поле",
|
||||
"date-created": "Дата на създаване",
|
||||
"date-updated": "Дата на актуализация"
|
||||
"date-updated": "Дата на актуализация",
|
||||
"key": "Ключ",
|
||||
"value": "Стойност"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Сигурни ли сте, че искате да изтриете <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Промените по тази група ще бъдат отразени моментално.",
|
||||
"group-id-value": "ID на Групата: {0}",
|
||||
"total-households": "Общ брой домакинства",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Трябва да изберете група, преди да изберете домакинство"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Трябва да изберете група, преди да изберете домакинство",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "Настройки на доставчика на ИИ",
|
||||
"ai-provider": "Доставчик на изкуствен интелект",
|
||||
"ai-providers": "Доставчици на изкуствен интелект",
|
||||
"ai-provider-settings-description": "Конфигурирайте доставчици на изкуствен интелект, за да активирате функции, задвижвани от изкуствен интелект, като например подобрен анализ на съставки, създаване на рецепти от видеоклипове и други!",
|
||||
"providers": "Доставчици",
|
||||
"create-provider": "Създаване на доставчик",
|
||||
"edit-provider": "Редактиране на доставчик",
|
||||
"default-provider": "Доставчик по подразбиране",
|
||||
"default-provider-description": "Необходимо е за активиране на функции с изкуствен интелект",
|
||||
"audio-provider": "Доставчик на аудио",
|
||||
"audio-provider-description": "Активира функции за аудио транскрипция, като например създаване на рецепти от видеоклипове",
|
||||
"image-provider": "Доставчик на изображения",
|
||||
"image-provider-description": "Активира функции за разпознаване на изображения, като например създаване на рецепти от изображения",
|
||||
"provider-name": "Име на доставчика",
|
||||
"api-key": "API ключ",
|
||||
"api-key-description-create": "API ключът на вашия доставчик за удостоверяване. Ако вашата услуга (напр. Ollama) не използва API ключ, пак трябва да въведете нещо тук.",
|
||||
"api-key-description-edit": "Оставете полето празно, освен ако не искате да го промените.",
|
||||
"base-url": "Основен линк",
|
||||
"base-url-description": "Ако използвате OpenAI, оставете това празно. Трябва да е съвместима OpenAI крайна точка (напр. \"http://localhost:11434/v1\").",
|
||||
"model": "Модел",
|
||||
"model-description": "Кой модел трябва да използва вашият доставчик на изкуствен интелект (напр. „gpt-5“).",
|
||||
"request-timeout-seconds": "Време за изчакване на заявката (секунди)",
|
||||
"provider-created": "Доставчика е създаден",
|
||||
"provider-updated": "Доставчикът е обновен",
|
||||
"provider-deleted": "Доставчикът е изтрит",
|
||||
"provider-create-failed": "Грешка при създаването на доставчик",
|
||||
"provider-update-failed": "Грешка при обновяването на доставчик",
|
||||
"provider-delete-failed": "Грешка при изтриването на доставчик",
|
||||
"request-headers": "Хедъри на заявката",
|
||||
"request-params": "Параметри на заявката",
|
||||
"no-default-provider-warning": "Не сте задали доставчик по подразбиране, така че функциите на изкуствения интелект са деактивирани"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Домакинство",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Създайте нова рецепта от чернова.",
|
||||
"create-recipes": "Създайте рецепти",
|
||||
"import-with-zip": "Импортирай от .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Създаване на рецепта от снимка",
|
||||
"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": "Изрежете и завъртете изображението, така че да се вижда само текстът и той да е в правилната ориентация.",
|
||||
"create-from-images": "Създаване от изображения",
|
||||
@@ -857,9 +894,9 @@
|
||||
"webhooks": {
|
||||
"test-webhooks": "Тестови Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "URL адресите, изброени по-долу, ще получат webhooks, съдържащи данните на рецептите от плана за хранене в планирания ден. В момента Webhooks ще се изпълняват на",
|
||||
"webhook-url": "Webhook URL",
|
||||
"webhooks-caps": "WEBHOOKS",
|
||||
"webhooks": "Webhooks",
|
||||
"webhook-url": "URL адрес на уебхук",
|
||||
"webhooks-caps": "Уебхуук",
|
||||
"webhooks": "Уебхуук",
|
||||
"webhook-name": "Име на webhook",
|
||||
"description": "Дефинираните по-долу webhooks ще бъдат изпълнени, когато е определено хранене за деня. В планираното време webhooks ще бъдат изпратени с данните от рецептата, която е планирана за деня. Имайте предвид, че изпълнението на webhook не е точно. Webhooks се изпълняват на интервал от 5 минути, така че ще бъдат изпълнени в рамките на +/- 5 минути от планираното."
|
||||
},
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Количество: {0}",
|
||||
"shopping-list": "Списък за пазаруване",
|
||||
"shopping-lists": "Списъци за пазаруване",
|
||||
"add-item": "Добавяне на елемент",
|
||||
"food": "Продукт",
|
||||
"note": "Бележка",
|
||||
"label": "Етикет",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Сигурни ли сте, че искате да изберете всички елементи?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Сигурни ли сте, че искате да премахнете отметката от всички елементи?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Сигурни ли сте, че искате да изтриете всички отметнати елементи?",
|
||||
"no-shopping-lists-found": "Не са намерени списъци за пазаруване"
|
||||
"no-shopping-lists-found": "Не са намерени списъци за пазаруване",
|
||||
"item-checked-off": "Неизбран елемент {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Всички рецепти",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Комбинирането на избраните храни ще обедини изходната храна и целевата храна в една единствена храна. Изходната храна ще бъде изтрита и всички препратки към изходната храна ще бъдат актуализирани, за да сочат към целевата храна.",
|
||||
"merge-food-example": "Обединяване на {food1} с {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Вече съм настроен, просто ме отведете до началната страница",
|
||||
"common-settings-for-new-sites": "Ето някои общи настройки за нови сайтове",
|
||||
"setup-complete": "Настройката е завършена!",
|
||||
"ai-providers-description": "По желание конфигурирайте доставчици на изкуствен интелект за вашата група. Доставчиците на изкуствен интелект активират функции като създаване на рецепти от изображения, импортиране на рецепти от видеоклипове и подобрен анализ на съставките. Винаги можете да конфигурирате това по-късно от настройките на вашата група.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Ето няколко неща, които ще Ви помогнат да започнете с Mealie",
|
||||
"restore-from-v1-backup": "Имате резервно копие от предишна инстанция на Mealie v1? Можете да го възстановите тук.",
|
||||
"manage-profile-or-get-invite-link": "Управлявайте собствения си профил или вземете връзка за покана, която да споделите с други."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Не са позволени интервали",
|
||||
"min-length": "Трябва да съдържа поне {min} знака",
|
||||
"max-length": "Трябва да бъде най-много {max} символа|Трябва да бъде най-много {max} символа"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Съобщения",
|
||||
"all-announcements": "Всички съобщения",
|
||||
"mark-all-as-read": "Маркирай всички като прочетени",
|
||||
"show-announcements-from-mealie": "Показване на съобщенията от Mealie",
|
||||
"show-announcements-setting-description": "Искате ли да разрешите на потребителите да виждат съобщения от Mealie? Когато е активирано, потребителите все още могат да се откажат от виждането им в потребителските си настройки"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Tauler de control",
|
||||
"delete": "Suprimeix",
|
||||
"disabled": "Desactivat",
|
||||
"done": "Done",
|
||||
"download": "Descarregar",
|
||||
"duplicate": "Duplica",
|
||||
"edit": "Edita",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Dimarts",
|
||||
"type": "Tipus",
|
||||
"undo": "Undo",
|
||||
"update": "Actualitza",
|
||||
"updated": "S'ha actualitzat",
|
||||
"upload": "Puja",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Mostrar els paràmetres avançats",
|
||||
"add-field": "Afegir camp",
|
||||
"date-created": "Data de creació",
|
||||
"date-updated": "Data d’actualització"
|
||||
"date-updated": "Data d’actualització",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Esteu segur de voler suprimir el grup <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Els canvis en aquest grup s'actualitzaran immediatament.",
|
||||
"group-id-value": "ID del grup: {0}",
|
||||
"total-households": "Llars totals",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Heu de seleccionar un grup abans de seleccionar una llar"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Heu de seleccionar un grup abans de seleccionar una llar",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Llar",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Crea una nova recepta des de zero.",
|
||||
"create-recipes": "Crea Receptes",
|
||||
"import-with-zip": "Importar amb un .zip",
|
||||
"create-recipe-from-an-image": "Crear una recepta a partir d'una imatge",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crear una recepta pujant una imatge d'ella. Mealie intentarà extreure el text de la imatge mitjançant IA i crear-ne la recepta.",
|
||||
"crop-and-rotate-the-image": "Retalla i rota la imatge, per tal que només el text sigui visible, i estigui orientat correctament.",
|
||||
"create-from-images": "Crear una recepta a partir d'una imatge",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Quantitat: {0}",
|
||||
"shopping-list": "Llista de la compra",
|
||||
"shopping-lists": "Llistes de la compra",
|
||||
"add-item": "Add item",
|
||||
"food": "Aliments",
|
||||
"note": "Nota",
|
||||
"label": "Etiqueta",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Estàs segur que vols marcar tots els elements?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Estàs segur que vols desmarcar tots els elements?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Estàs segur que vols eliminar tots els elements marcats?",
|
||||
"no-shopping-lists-found": "No s'han trobat llistes de la compra"
|
||||
"no-shopping-lists-found": "No s'han trobat llistes de la compra",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Receptes",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combinant els aliments seleccionats, es fusionaran els dos aliments en un. El primer aliment serà eliminat i totes les referències s'actualitzaran a l'aliment resultant.",
|
||||
"merge-food-example": "Combinant {food1} i {food2}",
|
||||
"seed-dialog-text": "Afegeix a la base de dades els noms dels aliments en el vostre idioma. Açò crearà més de 200 aliments comuns per a què pugueu organitzar la vostra base de dades. Els noms dels aliments han estat traduïts gràcies a l'esforç de la comunitat.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Ja ho tinc configurat, porta'm a la pàgina principal",
|
||||
"common-settings-for-new-sites": "Aquí hi ha algunes configuracions comunes per noves pàgines",
|
||||
"setup-complete": "Configuració completada!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Aquí hi ha unes quantes coses per ajudar-te a posar Mealie en marxa",
|
||||
"restore-from-v1-backup": "Tens una còpia de seguretat d'una instància prèvia de Mealie v1? Pots restaurar-la aquí.",
|
||||
"manage-profile-or-get-invite-link": "Gestiona el teu propi perfil, o agafa un enllaç d'invitació per compartir amb altres."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No es permeten espais en blanc",
|
||||
"min-length": "Ha de tenir almenys {min} caràcters",
|
||||
"max-length": "Ha de tenir com a màxim {max} caràcter|Ha de tenir com a màxim {max} caràcters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Nástěnka",
|
||||
"delete": "Smazat",
|
||||
"disabled": "Deaktivováno",
|
||||
"done": "Hotovo",
|
||||
"download": "Stáhnout",
|
||||
"duplicate": "Duplikovat",
|
||||
"edit": "Upravit",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Úterý",
|
||||
"type": "Typ",
|
||||
"undo": "Zpět",
|
||||
"update": "Aktualizace",
|
||||
"updated": "Aktualizováno",
|
||||
"upload": "Nahrát",
|
||||
@@ -179,7 +181,7 @@
|
||||
"units": "Jednotky",
|
||||
"back": "Zpět",
|
||||
"next": "Další",
|
||||
"start": "Start",
|
||||
"start": "Spustit",
|
||||
"toggle-view": "Přepnout zobrazení",
|
||||
"date": "Datum",
|
||||
"id": "Id",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Zobrazit pokročilé",
|
||||
"add-field": "Přidat pole",
|
||||
"date-created": "Datum vytvoření",
|
||||
"date-updated": "Datum aktualizace"
|
||||
"date-updated": "Datum aktualizace",
|
||||
"key": "Klíč",
|
||||
"value": "Hodnota"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Jste si jisti, že chcete smazat <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Změny v této skupině budou okamžitě zohledněny.",
|
||||
"group-id-value": "ID skupiny: {0}",
|
||||
"total-households": "Celkem domácností",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Před výběrem domácnosti musíte vybrat skupinu"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Před výběrem domácnosti musíte vybrat skupinu",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "Nastavení AI providera",
|
||||
"ai-provider": "AI provider",
|
||||
"ai-providers": "AI provideři",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Provideři",
|
||||
"create-provider": "Vytvořit providera",
|
||||
"edit-provider": "Upravit providera",
|
||||
"default-provider": "Výchozí provider",
|
||||
"default-provider-description": "Požadováno pro povolení AI funkcí",
|
||||
"audio-provider": "Audio provider",
|
||||
"audio-provider-description": "Povoluje funkce přepisu audia jako vytváření receptů z videí",
|
||||
"image-provider": "Provider obrázků",
|
||||
"image-provider-description": "Povolit funkce rozpoznávání obrázků jako vytváření receptů z obrázků",
|
||||
"provider-name": "Název providera",
|
||||
"api-key": "Klíč API",
|
||||
"api-key-description-create": "API klíč vašeho providera pro ověření. Pokud vaše služba (např. Ollama) nepoužívá API klíč, stále je potřeba sem něco vložit.",
|
||||
"api-key-description-edit": "Vyplňte pouze pokud chcete změnit.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Jaký model by měl váš AI provider použít (např. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Platnost požadavku (sekundy)",
|
||||
"provider-created": "Provider vytvořen",
|
||||
"provider-updated": "Provider aktualizován",
|
||||
"provider-deleted": "Provider odstraněn",
|
||||
"provider-create-failed": "Vytvoření providera se nezdařilo",
|
||||
"provider-update-failed": "Aktualizace providera se nezdařila",
|
||||
"provider-delete-failed": "Odstranění providera se nezdařilo",
|
||||
"request-headers": "Hlavičky požadavků",
|
||||
"request-params": "Parametry požadavků",
|
||||
"no-default-provider-warning": "Nezvolili jste výchozího providera, AI funkce jsou zakázané"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Domácnost",
|
||||
@@ -331,8 +368,8 @@
|
||||
"any-household": "Jakákoliv domácnost",
|
||||
"no-meal-plan-defined-yet": "Dosud nebyl definován žádný jídelníček",
|
||||
"no-meal-planned-for-today": "Pro dnešek není naplánováno žádné jídlo",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-label": "Default Days in the Past",
|
||||
"numberOfDaysPast-hint": "Počet dní v minulosti při načtení stránky",
|
||||
"numberOfDaysPast-label": "Výchozí dny v minulosti",
|
||||
"numberOfDays-hint": "Počet dní při načtení stránky",
|
||||
"numberOfDays-label": "Výchozí dny",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Pouze recepty z těchto kategorií budou použity v jídelníčku",
|
||||
@@ -442,7 +479,7 @@
|
||||
"error-title": "Vypadá to, že se nám nic nepodařilo najít",
|
||||
"from-url": "Přenést recept",
|
||||
"github-issues": "Hlášení chyb na GitHubu",
|
||||
"google-ld-json-info": "Google ld+json Info",
|
||||
"google-ld-json-info": "Informace o Google ld+json",
|
||||
"must-be-a-valid-url": "Musí být validní URL",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Vložte data receptu. Každý řádek bude považován za položku v seznamu",
|
||||
"recipe-markup-specification": "Specifikace Markupu pro recept",
|
||||
@@ -480,7 +517,7 @@
|
||||
"recipe": {
|
||||
"add-key": "Přidat klíč",
|
||||
"add-to-favorites": "Přidat do oblíbených",
|
||||
"api-extras": "API Extras",
|
||||
"api-extras": "API doplňky",
|
||||
"calories": "Kalorie",
|
||||
"calories-suffix": "kalorie",
|
||||
"carbohydrate-content": "Sacharidy",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Vytvořit nový recept od nuly.",
|
||||
"create-recipes": "Vytvořit recepty",
|
||||
"import-with-zip": "Importovat pomocí .zip",
|
||||
"create-recipe-from-an-image": "Vytvořit recept z obrázku",
|
||||
"create-recipe-from-images": "Vytvořit recept z obrázků",
|
||||
"create-recipe-from-an-image-description": "Vytvořte recept nahráním obrázku. Mealie se pokusí z obrázku extrahovat text pomocí AI a vytvořit z něj recept.",
|
||||
"crop-and-rotate-the-image": "Oříznout a otočit obrázek tak, aby byl viditelný pouze text a aby byl ve správné orientaci.",
|
||||
"create-from-images": "Vytvořit z obrázků",
|
||||
@@ -638,8 +675,8 @@
|
||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Vytvořte recept zadáním názvu. Všechny recepty musí mít jedinečná jména.",
|
||||
"new-recipe-names-must-be-unique": "Názvy receptů musí být jedinečné",
|
||||
"scrape-recipe": "Zpracovat recept",
|
||||
"scrape-recipe-description": "Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the recipe from that site and add it to your collection.",
|
||||
"scrape-recipe-description-transcription": "You can also provide the url to a video and Mealie will attempt to transcribe it into a recipe.",
|
||||
"scrape-recipe-description": "Stáhněte recept z URL. Zadejte URL webu, ze kterého chcete recept stáhnout, a Mealie se pokusí recept stáhnout a přidat ho do vaší kolekce.",
|
||||
"scrape-recipe-description-transcription": "Můžete také zadat URL videa a Mealie se pokusí přepsat ho do receptu.",
|
||||
"scrape-recipe-have-a-lot-of-recipes": "Máte spoustu receptů, které chcete zpracovat najednou?",
|
||||
"scrape-recipe-suggest-bulk-importer": "Vyzkoušejte hromadný import",
|
||||
"scrape-recipe-have-raw-html-or-json-data": "Máte surová data HTML nebo JSON?",
|
||||
@@ -655,7 +692,7 @@
|
||||
"import-from-html-or-json": "Importovat z HTML nebo JSON",
|
||||
"import-from-html-or-json-description": "Import jednoho receptu ze surového HTML nebo JSON. To je užitečné, pokud máte recept z webu, který Mealie nedokáže normálně seškrábat, nebo z jiného externího zdroje.",
|
||||
"json-import-format-description-colon": "Chcete-li importovat přes JSON, musí mít platný formát:",
|
||||
"json-editor": "JSON Editor",
|
||||
"json-editor": "JSON editor",
|
||||
"zip-files-must-have-been-exported-from-mealie": "Soubory .zip musí být exportovány z aplikace Mealie",
|
||||
"create-a-recipe-by-uploading-a-scan": "Vytvořte recept nahráním skenu.",
|
||||
"upload-a-png-image-from-a-recipe-book": "Nahrát png obrázek z knihy receptů",
|
||||
@@ -835,7 +872,7 @@
|
||||
},
|
||||
"token": {
|
||||
"active-tokens": "AKTIVNÍ TOKENY",
|
||||
"api-token": "API Token",
|
||||
"api-token": "API token",
|
||||
"api-tokens": "API Tokeny",
|
||||
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Zkopírujte tento token pro použití v externí aplikaci. Tento token nebude znovu zobrazen.",
|
||||
"create-an-api-token": "Vytvořit nový API token",
|
||||
@@ -865,7 +902,7 @@
|
||||
},
|
||||
"bug-report": "Chybové hlášení",
|
||||
"bug-report-information": "Použijte tyto informace k nahlášení chyby. Poskytnutí podrobností vaší instance vývojářům je nejlepší způsob, jak rychle vyřešit vaše problémy.",
|
||||
"tracker": "Tracker",
|
||||
"tracker": "Sledovač",
|
||||
"configuration": "Konfigurace",
|
||||
"docker-volume": "Volume dockeru",
|
||||
"docker-volume-help": "Mealie vyžaduje, aby kontejner prostředí a podpůrné vrstvy sdílely stejný úložný prostor dockeru. Tím se zajistí, že kontejner prostředí bude moci správně přistupovat k obrázkům a informacím uloženým na disku.",
|
||||
@@ -891,17 +928,17 @@
|
||||
"server-side-base-url-error-text": "`BASE_URL` je stále výchozí hodnotou na serveru API. To způsobí problémy s odkazy v oznámení generované na serveru pro e-maily atd.",
|
||||
"server-side-base-url-success-text": "Adresa URL na straně serveru neodpovídá výchozímu nastavení",
|
||||
"ldap-ready": "LDAP připraven",
|
||||
"ldap-not-ready": "LDAP Not Ready",
|
||||
"ldap-not-ready": "LDAP nepřipraveno",
|
||||
"ldap-ready-error-text": "Nejsou nakonfigurovány všechny LDAP hodnoty. To můžete ignorovat, pokud nepoužíváte LDAP autentizaci.",
|
||||
"ldap-ready-success-text": "Všechny požadované proměnné LDAP jsou nastaveny.",
|
||||
"build": "Sestavení",
|
||||
"recipe-scraper-version": "Verze scraperu receptů",
|
||||
"oidc-ready": "OIDC připraveno",
|
||||
"oidc-not-ready": "OIDC Not Ready",
|
||||
"oidc-not-ready": "OIDC nepřipraveno",
|
||||
"oidc-ready-error-text": "Nejsou nakonfigurovány všechny OIDC hodnoty. To můžete ignorovat, pokud nepoužíváte OIDC autentizaci.",
|
||||
"oidc-ready-success-text": "Všechny požadované proměnné OIDC jsou nastaveny.",
|
||||
"openai-ready": "OpenAI připraveno",
|
||||
"openai-not-ready": "OpenAI Not Ready",
|
||||
"openai-not-ready": "OpenAI nepřipraveno",
|
||||
"openai-ready-error-text": "Nejsou nakonfigurovány všechny OpenAI hodnoty. To můžete ignorovat, pokud nepoužíváte OpenAI funkce.",
|
||||
"openai-ready-success-text": "Všechny požadované proměnné OpenAI jsou nastaveny."
|
||||
},
|
||||
@@ -909,12 +946,13 @@
|
||||
"all-lists": "Všechny seznamy",
|
||||
"create-shopping-list": "Vytvořit nákupní seznam",
|
||||
"from-recipe": "Z receptu",
|
||||
"ingredient-of-recipe": "Ingredient of {recipe}",
|
||||
"ingredient-of-recipe": "Ingredience receptu {recipe}",
|
||||
"list-name": "Název seznamu",
|
||||
"new-list": "Nový seznam",
|
||||
"quantity": "Množství: {0}",
|
||||
"shopping-list": "Nákupní seznam",
|
||||
"shopping-lists": "Nákupní seznamy",
|
||||
"add-item": "Přidat položku",
|
||||
"food": "Jídlo",
|
||||
"note": "Poznámka",
|
||||
"label": "Popisek",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Opravdu chcete vybrat všechny položky?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Opravdu chcete zrušit výběr všech položek?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Opravdu chcete odstranit všechny vybrané položky?",
|
||||
"no-shopping-lists-found": "Nebyly nalezeny žádné nákupní seznamy"
|
||||
"no-shopping-lists-found": "Nebyly nalezeny žádné nákupní seznamy",
|
||||
"item-checked-off": "Odškrtnuta položka {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Všechny recepty",
|
||||
@@ -1010,7 +1049,7 @@
|
||||
"current-password": "Současné heslo",
|
||||
"e-mail-must-be-valid": "E-mail musí být platný",
|
||||
"edit-user": "Upravit uživatele",
|
||||
"email": "Email",
|
||||
"email": "E-mail",
|
||||
"error-cannot-delete-super-user": "Chyba! Nelze odstranit superuživatele",
|
||||
"existing-password-does-not-match": "Hesla se neshodují",
|
||||
"full-name": "Jméno a příjmení",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Varování: toto jídlo je použito v {count} receptu(receptech). Odstranění zanechá v receptu(receptech) prázdnou ingrediencí.",
|
||||
"delete-affects-recipes-more": "Zobrazit všechny {count} recepty",
|
||||
"merge-dialog-text": "Zkombinování zvolených potravin způsobí smazání zdrojové potraviny a veškeré odkazy na ni budou přesměrovány do cílové potraviny.",
|
||||
"merge-food-example": "Sloučení {food1} do {food2}",
|
||||
"seed-dialog-text": "Naplňte databázi potravinami z vašeho jazyka. Tímto vytvoříte přes 200 běžných potravin, které můžete použít k organizaci vaší databáze. Potraviny jsou přeloženy skrze komunitní úsilí.",
|
||||
@@ -1142,18 +1183,18 @@
|
||||
"example-unit-plural": "např.: Čajové lžičky",
|
||||
"example-unit-abbreviation-singular": "např.: čl",
|
||||
"example-unit-abbreviation-plural": "např.: čl",
|
||||
"standardization": "Standardization",
|
||||
"standardization-description": "How this unit can be represented as a standard unit. This enables unit conversion features such as merging compatible units in shopping lists.",
|
||||
"standard-unit": "Standard Unit",
|
||||
"standard-quantity": "Standard Quantity",
|
||||
"unit-conversion": "Unit Conversion",
|
||||
"standardization": "Standardizace",
|
||||
"standardization-description": "Jak lze tuto jednotku reprezentovat jako standardní jednotku. Umožňuje funkce konverze jednotek, jako je slučování kompatibilních jednotek v nákupních seznamech.",
|
||||
"standard-unit": "Standardní jednotka",
|
||||
"standard-quantity": "Standardní množství",
|
||||
"unit-conversion": "Konverze jednotek",
|
||||
"standard-unit-labels": {
|
||||
"fluid-ounce": "fluid ounce",
|
||||
"cup": "cup",
|
||||
"ounce": "ounce",
|
||||
"pound": "pound",
|
||||
"milliliter": "milliliter",
|
||||
"liter": "liter",
|
||||
"fluid-ounce": "tekutá unce",
|
||||
"cup": "šálek",
|
||||
"ounce": "unce",
|
||||
"pound": "libra",
|
||||
"milliliter": "mililitr",
|
||||
"liter": "litr",
|
||||
"gram": "gram",
|
||||
"kilogram": "kilogram"
|
||||
}
|
||||
@@ -1192,13 +1233,13 @@
|
||||
"edit-recipe-action": "Upravit akci receptu",
|
||||
"action-type": "Typ akce",
|
||||
"action-types": {
|
||||
"link": "Link",
|
||||
"link": "Odkaz",
|
||||
"post": "Publikovat"
|
||||
}
|
||||
},
|
||||
"create-alias": "Vytvořit alias",
|
||||
"manage-aliases": "Spravovat aliasy",
|
||||
"seed-data": "Seed Data",
|
||||
"seed-data": "Výchozí data",
|
||||
"seed": "Zdroj",
|
||||
"data-management": "Správa dat",
|
||||
"data-management-description": "Vyberte datovou sadu, ve které chcete provést změny.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Již mám nastaveno, vezmi mě na domovskou stránku",
|
||||
"common-settings-for-new-sites": "Zde jsou některá běžná nastavení pro nové stránky",
|
||||
"setup-complete": "Nastavení dokončeno!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Zde je několik věcí, které vám pomohou začít s Mealie",
|
||||
"restore-from-v1-backup": "Máte zálohu z předchozí instance Mealie v1? Můžete ji obnovit zde.",
|
||||
"manage-profile-or-get-invite-link": "Spravujte svůj vlastní profil, nebo přidejte pozvánku ke sdílení s ostatními."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Mezery nejsou povoleny",
|
||||
"min-length": "Musí být alespoň {min} znaků",
|
||||
"max-length": "Musí být maximálně {max} znak|Musí být na většině {max} znaků"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Oznámení",
|
||||
"all-announcements": "Všechna oznámení",
|
||||
"mark-all-as-read": "Označit vše jako přečtené",
|
||||
"show-announcements-from-mealie": "Zobrazit oznámení od Mealie",
|
||||
"show-announcements-setting-description": "Pokud je povoleno, uživatelé uvidí oznámení od Mealie. Mohou to změnit ve svém uživatelském nastavení"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Overblik",
|
||||
"delete": "Slet",
|
||||
"disabled": "Deaktiveret",
|
||||
"done": "Udført",
|
||||
"download": "Hent",
|
||||
"duplicate": "Kopier",
|
||||
"edit": "Rediger",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Nøgle",
|
||||
"tuesday": "Tirsdag",
|
||||
"type": "Type",
|
||||
"undo": "Fortryd",
|
||||
"update": "Gem",
|
||||
"updated": "Ændret",
|
||||
"upload": "Upload",
|
||||
@@ -182,7 +184,7 @@
|
||||
"start": "Start",
|
||||
"toggle-view": "Skift visning",
|
||||
"date": "Dato",
|
||||
"id": "Id",
|
||||
"id": "ID",
|
||||
"owner": "Ejer",
|
||||
"change-owner": "Skift ejer",
|
||||
"date-added": "Oprettelsesdato",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Vis avanceret",
|
||||
"add-field": "Tilføj felt",
|
||||
"date-created": "Oprettet",
|
||||
"date-updated": "Opdateret"
|
||||
"date-updated": "Opdateret",
|
||||
"key": "Nøgle",
|
||||
"value": "Værdi"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Er du sikker på, du vil slette <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Ændringer i denne gruppe vil træde i kraft øjeblikkeligt.",
|
||||
"group-id-value": "Gruppe-ID: {0}",
|
||||
"total-households": "Husstande i Alt",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Du skal vælge en gruppe, før du vælger en husstand"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Du skal vælge en gruppe, før du vælger en husstand",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI-udbyderindstillinger",
|
||||
"ai-provider": "AI-udbyder",
|
||||
"ai-providers": "AI-udbydere",
|
||||
"ai-provider-settings-description": "Konfigurér AI-udbydere for at slå AI-funktioner til, såsom forbedret ingredienshåndtering, at oprette opskrifter fra videoer med mere.",
|
||||
"providers": "Udbydere",
|
||||
"create-provider": "Opret udbyder",
|
||||
"edit-provider": "Redigér udbyder",
|
||||
"default-provider": "Standardudbyder",
|
||||
"default-provider-description": "Påkrævet for at slå AI-funktioner til",
|
||||
"audio-provider": "Lydudbyder",
|
||||
"audio-provider-description": "Slå lydtranskriberingsfunktioner til, såsom at oprette opskrifter fra videoer",
|
||||
"image-provider": "Billedudbyder",
|
||||
"image-provider-description": "Slår billedgenkendelsesfunktioner til, såsom at oprette opskrifter fra billeder",
|
||||
"provider-name": "Udbydernavn",
|
||||
"api-key": "API-nøgle",
|
||||
"api-key-description-create": "Din udbyders API-nøgle til godkendelse. Hvis din udbyder ikke benytter en API-nøgle (eks. Ollama), skal du stadig skrive ét eller andet,",
|
||||
"api-key-description-edit": "Undlad at udfylde dette, medmindre du vil ændre det.",
|
||||
"base-url": "Basis-URL",
|
||||
"base-url-description": "Undlad at udfylde, hvis du benytter OpenAI. Skal være et OpenAI-kompatibelt endpoint (eks. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Hvilken model skal din udbyder benytte (eks. \"gpt-5\")?",
|
||||
"request-timeout-seconds": "Forespørgsels-time-out",
|
||||
"provider-created": "Udbyder oprettet",
|
||||
"provider-updated": "Udbyder opdateret",
|
||||
"provider-deleted": "Udbyder slettet",
|
||||
"provider-create-failed": "Kunne ikke oprette udbyder",
|
||||
"provider-update-failed": "Kunne ikke opdatere udbyder",
|
||||
"provider-delete-failed": "Kunne ikke slette udbyder",
|
||||
"request-headers": "Forespørgsels-headers",
|
||||
"request-params": "Forespørgselsparametre",
|
||||
"no-default-provider-warning": "Du har ikke sat en standardudbyder, så AI-funktioner er slået fra"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Husstand",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Opret ny opskrift fra bunden.",
|
||||
"create-recipes": "Opret opskrift",
|
||||
"import-with-zip": "Importér fra ZIP-fil",
|
||||
"create-recipe-from-an-image": "Opret opskrift fra et billede",
|
||||
"create-recipe-from-images": "Opret opskrift fra billeder",
|
||||
"create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.",
|
||||
"crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.",
|
||||
"create-from-images": "Opret fra billede",
|
||||
@@ -686,9 +723,9 @@
|
||||
"explanation": "For at bruge ingrediensfortolkeren, skal du klikke på knappen 'Fortolk alt' for at starte behandlingen. Når relevante ingredienser er identificeret, kan du gennemgå dem og kontrollere, at de blev korrekt identificeret. Modellens konfidensscore vises til højre for ingrediensens titel. Denne score er et gennemsnit af alle de enkelte scorer og er måske ikke altid helt præcis.",
|
||||
"alerts-explainer": "En advarsel vil blive vist, hvis en identificeret fødevare eller måleenhed ikke findes i databasen.",
|
||||
"select-parser": "Vælg fortolker",
|
||||
"natural-language-processor": "Natural Language Processor",
|
||||
"brute-parser": "Brute Parser",
|
||||
"openai-parser": "OpenAI Parser",
|
||||
"natural-language-processor": "Naturlig sprogbehandler",
|
||||
"brute-parser": "Rå parser",
|
||||
"openai-parser": "OpenAI-parser",
|
||||
"parse-all": "Fortolk alt",
|
||||
"no-unit": "Ingen enhed",
|
||||
"missing-unit": "Opret manglende måleenhed: {unit}",
|
||||
@@ -835,8 +872,8 @@
|
||||
},
|
||||
"token": {
|
||||
"active-tokens": "Aktive tokens",
|
||||
"api-token": "API Token",
|
||||
"api-tokens": "API Tokens",
|
||||
"api-token": "API-token",
|
||||
"api-tokens": "API-tokener",
|
||||
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Kopier denne token ved brug i en anden applikation. Denne token kan ikke ses igen.",
|
||||
"create-an-api-token": "Opret en API token",
|
||||
"token-name": "Tokennavn",
|
||||
@@ -855,9 +892,9 @@
|
||||
"unorganized": "Uorganiseret"
|
||||
},
|
||||
"webhooks": {
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"test-webhooks": "Test webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Webadresserne, der er anført nedenfor, modtager webhooks, der indeholder opskriftsdataene for måltidsplanen på den planlagte dag. \nWebhooks udføres i øjeblikket på ",
|
||||
"webhook-url": "Webhook URL",
|
||||
"webhook-url": "Webhook-URL",
|
||||
"webhooks-caps": "WEBHOOKS",
|
||||
"webhooks": "Webhooks",
|
||||
"webhook-name": "Webhooknavn",
|
||||
@@ -865,9 +902,9 @@
|
||||
},
|
||||
"bug-report": "Fejlrapport",
|
||||
"bug-report-information": "Brug denne information til at rapportere en fejl. At give detaljer om din instans til udviklere er den bedste måde at få dine problemer løst hurtigt.",
|
||||
"tracker": "Tracker",
|
||||
"tracker": "Sporing",
|
||||
"configuration": "Konfiguration",
|
||||
"docker-volume": "Docker Volume",
|
||||
"docker-volume": "Docker-volumen",
|
||||
"docker-volume-help": "Mealie kræver, at frontend og backend containere deler den samme docker mappe. Dette sikrer, at frontend container har adgang til billeder og øvrige lagret på disken.",
|
||||
"volumes-are-misconfigured": "Docker mapper er forkert konfigureret.",
|
||||
"volumes-are-configured-correctly": "Docker mapper er korrekt konfigureret.",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Antal: {0}",
|
||||
"shopping-list": "Indkøbsliste",
|
||||
"shopping-lists": "Indkøbslister",
|
||||
"add-item": "Tilføj vare",
|
||||
"food": "Fødevarer",
|
||||
"note": "Note",
|
||||
"label": "Etiket",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Er du sikker på, at du vil markere alle elementer?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på, at du vil fjerne markeringen af alle elementer?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på, at du vil sletter de valgte elementer?",
|
||||
"no-shopping-lists-found": "Ingen Indkøbslister fundet"
|
||||
"no-shopping-lists-found": "Ingen Indkøbslister fundet",
|
||||
"item-checked-off": "Krydsede {item} af"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle opskrifter",
|
||||
@@ -953,7 +992,7 @@
|
||||
"profile": "Profil",
|
||||
"search": "Søg",
|
||||
"site-settings": "Sideindstillinger",
|
||||
"tags": "Tags",
|
||||
"tags": "Etiketter",
|
||||
"toolbox": "Værktøjskasse",
|
||||
"language": "Sprog",
|
||||
"maintenance": "Vedligeholdelse",
|
||||
@@ -980,7 +1019,7 @@
|
||||
"tag-deletion-failed": "Sletning af tag fejlede",
|
||||
"tag-update-failed": "Opdatering af tag fejlede",
|
||||
"tag-updated": "Tag blev opdateret",
|
||||
"tags": "Tags",
|
||||
"tags": "Etiket",
|
||||
"untagged-count": "Ikke-tagget: {count}",
|
||||
"create-a-tag": "Opret tag",
|
||||
"tag-name": "Tag navn",
|
||||
@@ -1016,7 +1055,7 @@
|
||||
"full-name": "Fulde navn",
|
||||
"generate-password-reset-link": "Generér link til nulstilling af adgangskode",
|
||||
"invite-only": "Kun inviterede",
|
||||
"link-id": "Link ID",
|
||||
"link-id": "Link-ID",
|
||||
"link-name": "Linknavn",
|
||||
"login": "Log på",
|
||||
"login-oidc": "Log ind med",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Advarsel: denne ingrediens er brugt i {count} opskrift(er). Hvis du sletter den, vil det efterlade en tom ingrediens i opskriften/opskrifterne.",
|
||||
"delete-affects-recipes-more": "Se alle {count} opskrifter",
|
||||
"merge-dialog-text": "Ved at kombinere de udvalgte fødevarer vil de to valgte fødevarer blive til en enkelt fødevare. Kildefødevaren vil blive slettet, og alle henvisninger til kildefødevaren vil blive opdateret til at pege på målfødevaren.",
|
||||
"merge-food-example": "Samler {food1} med {food2}",
|
||||
"seed-dialog-text": "Tilføj standardfødevarer på dansk i databasen. Dette vil oprette cirka 2700 af de mest anvendte fødevarer, der kan bruges til at organisere din database. Fødevarer er oversat via en fællesskabsindsats.",
|
||||
@@ -1329,7 +1370,7 @@
|
||||
"action-clean-images-description": "Fjerner alle de billeder, der ikke slutter med .webp",
|
||||
"actions-description": "Vedligeholdelseshandlinger er {destructive_in_bold} og bør bruges med forsigtighed. Udførelse af alle disse handlinger er {irreversible_in_bold}.",
|
||||
"actions-description-destructive": "destruktive",
|
||||
"actions-description-irreversible": "irreversible",
|
||||
"actions-description-irreversible": "uomgørlig",
|
||||
"logs-action-refresh": "Opdater logfiler",
|
||||
"logs-page-title": "Mealie logfiler",
|
||||
"logs-tail-lines-label": "Følg log"
|
||||
@@ -1341,7 +1382,7 @@
|
||||
"ingredients-natural-language-processor-explanation": "Mealie bruger Conditional Random Fields felter (CRF'er) til berarbejdning af ingredienser. Den model, der anvendes til ingredienser er baseret ud fra et datasæt på over 100.000 ingredienser fra et datasæt udarbejdet af New York Times. Bemærk, at da modellen kun er trænet på engelsk, kan du have forskellige resultater, når du bruger modellen på andre sprog. På denne side kan du teste modellen.",
|
||||
"ingredients-natural-language-processor-explanation-2": "Det er ikke perfekt, men giver generelt gode resultater og er et godt udgangspunkt for manuel redigering af ingredienser i individuelle felter. Alternativt kan du også bruge \"Brute\"-metoden, der bruger en mønstermatchende teknik til at identificere ingredienser.",
|
||||
"nlp": "NLP",
|
||||
"brute": "Brute",
|
||||
"brute": "Rå",
|
||||
"openai": "OpenAI",
|
||||
"show-individual-confidence": "Vis individual konfidensscore",
|
||||
"ingredient-text": "Ingredienstekst",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Jeg er allerede oprettet, bare bringe mig til startsiden",
|
||||
"common-settings-for-new-sites": "Her er nogle almindelige indstillinger for nye sites",
|
||||
"setup-complete": "Opsætning færdig!",
|
||||
"ai-providers-description": "Konfigurér valgfrit AI-udbydere for din gruppe. AI-udbydere muliggør handlinger, såsom at oprette opskrifter fra billeder, importere opskrifter fra videoer, og forbedret håndtering af ingredienser. Det er altid muligt at konfigurere dette senere under dine gruppeindstillinger.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Her er et par ting, der kan hjælpe dig i gang med Mealie",
|
||||
"restore-from-v1-backup": "Har du en sikkerhedskopi fra en tidligere udgave af Mealie v1? Du kan gendanne den her.",
|
||||
"manage-profile-or-get-invite-link": "Administrer din egen profil, eller tag et invitationslink til at dele med andre."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Mellemrum er ikke tilladt",
|
||||
"min-length": "Der skal mindst være {min} tegn",
|
||||
"max-length": "Må højst være {max} tegn|Må højst være {max} tegn"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Meddelelser",
|
||||
"all-announcements": "Alle meddelelser",
|
||||
"mark-all-as-read": "Markér alle som læst",
|
||||
"show-announcements-from-mealie": "Vis meddelelser fra Mealie",
|
||||
"show-announcements-setting-description": "Hvorvidt det ønskes, at brugere skal kunne se meddelelser fra Mealie. Hvis dette er slået til, er det stadig muligt for brugere at fravælge det i deres brugerindstillinger."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"category": "Kategorie"
|
||||
},
|
||||
"events": {
|
||||
"apprise-url": "Apprise URL",
|
||||
"apprise-url": "Apprise-URL",
|
||||
"database": "Datenbank",
|
||||
"delete-event": "Ereignis löschen",
|
||||
"event-delete-confirmation": "Bist du dir sicher, dass du dieses Ereignis löschen möchtest?",
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Übersicht",
|
||||
"delete": "Löschen",
|
||||
"disabled": "Deaktiviert",
|
||||
"done": "Erledigt",
|
||||
"download": "Herunterladen",
|
||||
"duplicate": "Duplizieren",
|
||||
"edit": "Bearbeiten",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Dienstag",
|
||||
"type": "Typ",
|
||||
"undo": "Rückgängig",
|
||||
"update": "Aktualisieren",
|
||||
"updated": "Aktualisiert",
|
||||
"upload": "Hochladen",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Erweiterte Optionen anzeigen",
|
||||
"add-field": "Feld Hinzufügen",
|
||||
"date-created": "Erstellungsdatum",
|
||||
"date-updated": "Aktualisiert am"
|
||||
"date-updated": "Aktualisiert am",
|
||||
"key": "Schlüssel",
|
||||
"value": "Wert"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Bist du dir sicher, dass du die Gruppe <b>{groupName}<b/> löschen möchtest?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Änderungen an dieser Gruppe sind sofort wirksam.",
|
||||
"group-id-value": "Gruppen ID: {0}",
|
||||
"total-households": "Haushalte insgesamt",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Du musst eine Gruppe auswählen, bevor du einen Haushalt auswählst"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Du musst eine Gruppe auswählen, bevor du einen Haushalt auswählst",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "KI-Anbieter-Einstellungen",
|
||||
"ai-provider": "KI-Anbieter",
|
||||
"ai-providers": "KI-Anbieter",
|
||||
"ai-provider-settings-description": "Konfiguriere KI-Anbieter, um KI-basierte Funktionen wie das verbesserte Parsen von Zutaten, das Erstellen von Rezepten aus Videos und vieles mehr zu aktivieren!",
|
||||
"providers": "Anbieter",
|
||||
"create-provider": "Anbieter erstellen",
|
||||
"edit-provider": "Anbieter bearbeiten",
|
||||
"default-provider": "Standard-Anbieter",
|
||||
"default-provider-description": "Zum Aktivieren der KI-Funktionen erforderlich",
|
||||
"audio-provider": "Audio-Anbieter",
|
||||
"audio-provider-description": "Aktiviert Audio-Transkriptionsfunktionen, wie das Erstellen von Rezepten aus Videos",
|
||||
"image-provider": "Bildanbieter",
|
||||
"image-provider-description": "Aktiviert Bilderkennungsfunktionen, wie das Erstellen von Rezepten aus Bildern",
|
||||
"provider-name": "Anbietername",
|
||||
"api-key": "API-Key",
|
||||
"api-key-description-create": "Der API-Schlüssel deines Anbieters zur Authentifizierung. Wenn der Dienst (z.B. Ollama) keinen API-Schlüssel verwendet, musst du hier trotzdem etwas eintragen.",
|
||||
"api-key-description-edit": "Lassen Sie dieses Feld leer, wenn Sie es nicht ändern möchten.",
|
||||
"base-url": "Basis-URL",
|
||||
"base-url-description": "Wenn du OpenAI verwendest, lasse dies leer. Muss ein OpenAI-kompatibler Endpunkt sein (z.B. \"http://localhost:11434/v1\").",
|
||||
"model": "Modell",
|
||||
"model-description": "Welches Modell dein KI-Anbieter verwenden soll (z.B. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Anfrage-Timeout (Sekunden)",
|
||||
"provider-created": "Anbieter erstellt",
|
||||
"provider-updated": "Anbieter aktualisiert",
|
||||
"provider-deleted": "Anbieter gelöscht",
|
||||
"provider-create-failed": "Anbieter konnte nicht erstellt werden",
|
||||
"provider-update-failed": "Anbieter konnte nicht aktualisiert werden",
|
||||
"provider-delete-failed": "Anbieter konnte nicht gelöscht werden",
|
||||
"request-headers": "Anfrage-Header",
|
||||
"request-params": "Anfrageparameter",
|
||||
"no-default-provider-warning": "Sie haben keinen Standard-Anbieter gesetzt, daher sind die KI-Funktionen deaktiviert"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Haushalt",
|
||||
@@ -331,7 +368,7 @@
|
||||
"any-household": "Beliebiger Haushalt",
|
||||
"no-meal-plan-defined-yet": "Noch kein Essensplan definiert",
|
||||
"no-meal-planned-for-today": "Kein Essen für heute geplant",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-hint": "Anzahl der Tage in der Vergangenheit beim Laden der Seite",
|
||||
"numberOfDaysPast-label": "Standardtage in der Vergangenheit",
|
||||
"numberOfDays-hint": "Anzahl der Tage beim Laden der Seite",
|
||||
"numberOfDays-label": "Anzuzeigende Tage",
|
||||
@@ -480,7 +517,7 @@
|
||||
"recipe": {
|
||||
"add-key": "Schlüssel hinzufügen",
|
||||
"add-to-favorites": "Zu Favoriten hinzufügen",
|
||||
"api-extras": "API Extras",
|
||||
"api-extras": "API-Extras",
|
||||
"calories": "Kalorien",
|
||||
"calories-suffix": "Kalorien",
|
||||
"carbohydrate-content": "Kohlenhydrate",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Erstelle ein neues Rezept von Grund auf.",
|
||||
"create-recipes": "Rezepte erstellen",
|
||||
"import-with-zip": "Von .zip importieren",
|
||||
"create-recipe-from-an-image": "Rezept von einem Bild erstellen",
|
||||
"create-recipe-from-images": "Rezept von einem Bild erstellen",
|
||||
"create-recipe-from-an-image-description": "Erstelle ein Rezept, indem du ein Bild hochlädst. Mealie wird versuchen, den Text aus dem Bild mit Hilfe von KI zu extrahieren und ein Rezept daraus zu erstellen.",
|
||||
"crop-and-rotate-the-image": "Beschneide und drehe das Bild so, dass nur der Text zu sehen ist und die Ausrichtung stimmt.",
|
||||
"create-from-images": "Aus Bildern erstellen",
|
||||
@@ -634,7 +671,7 @@
|
||||
"please-wait-image-procesing": "Warte bitte, das Bild wird gerade bearbeitet. Dies kann einige Zeit dauern.",
|
||||
"please-wait-images-processing": "Bitte warten, die Bilder werden verarbeitet. Dies kann einige Zeit dauern.",
|
||||
"bulk-url-import": "URL Massenimport",
|
||||
"debug-scraper": "Debug Scraper",
|
||||
"debug-scraper": "Debug-Scraper",
|
||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Erstelle ein Rezept, indem du den Namen angibst. Alle Rezepte müssen eindeutige Namen haben.",
|
||||
"new-recipe-names-must-be-unique": "Neue Rezeptnamen müssen eindeutig sein",
|
||||
"scrape-recipe": "Rezept einlesen",
|
||||
@@ -655,7 +692,7 @@
|
||||
"import-from-html-or-json": "Aus HTML oder JSON importieren",
|
||||
"import-from-html-or-json-description": "Importiere eine einzelne Datei aus Roh-HTML oder JSON. Das ist nützlich, wenn du ein Rezept von einer Seite, die Mealie nicht scrapen kann, oder von einer externen Quelle hast.",
|
||||
"json-import-format-description-colon": "Um mit JSON zu importieren, muss die Datei in einem gültigen Format vorliegen:",
|
||||
"json-editor": "JSON Editor",
|
||||
"json-editor": "JSON-Editor",
|
||||
"zip-files-must-have-been-exported-from-mealie": ".zip Dateien müssen aus Mealie exportiert worden sein",
|
||||
"create-a-recipe-by-uploading-a-scan": "Erstelle ein Rezept durch Hochladen eines Scans.",
|
||||
"upload-a-png-image-from-a-recipe-book": "Lade ein PNG-Bild aus einem Kochbuch hoch",
|
||||
@@ -687,8 +724,8 @@
|
||||
"alerts-explainer": "Es werden Warnungen angezeigt, wenn ein passendes Lebensmittel oder eine Einheit gefunden wurde, aber in der Datenbank nicht vorhanden ist.",
|
||||
"select-parser": "Parser auswählen",
|
||||
"natural-language-processor": "Natürliche Sprachverarbeitung",
|
||||
"brute-parser": "Brute Parser",
|
||||
"openai-parser": "OpenAI Parser",
|
||||
"brute-parser": "Brute-Parser",
|
||||
"openai-parser": "OpenAI-Parser",
|
||||
"parse-all": "Alles parsen",
|
||||
"no-unit": "Keine Einheit",
|
||||
"missing-unit": "Fehlende Einheit erstellen: {unit}",
|
||||
@@ -867,7 +904,7 @@
|
||||
"bug-report-information": "Füge diese Informationen deiner Fehlermeldung hinzu. Die Details zu deiner Installation sind für die Entwickler hilfreich, damit dein Anliegen schnell gelöst werden kann.",
|
||||
"tracker": "Fehler melden",
|
||||
"configuration": "Konfiguration",
|
||||
"docker-volume": "Docker Volume",
|
||||
"docker-volume": "Docker-Volume",
|
||||
"docker-volume-help": "Mealie setzt voraus, dass sich der Frontend-Container und das Backend das gleiche Docker-Volume oder den gleichen Speicher teilen. Dadurch wird sichergestellt, dass der Frontend-Container auf die Bilder und Assets auf der Festplatte zugreifen kann.",
|
||||
"volumes-are-misconfigured": "Volumes sind falsch konfiguriert.",
|
||||
"volumes-are-configured-correctly": "Volumes sind korrekt konfiguriert.",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Menge: {0}",
|
||||
"shopping-list": "Einkaufsliste",
|
||||
"shopping-lists": "Einkaufslisten",
|
||||
"add-item": "Eintrag hinzufügen",
|
||||
"food": "Lebensmittel",
|
||||
"note": "Notiz",
|
||||
"label": "Kategorie",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Bist du sicher, dass du alle Elemente markieren möchtest?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Bist du sicher, dass du die Auswahl aller Elemente aufheben möchtest?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Bist du sicher, dass du alle ausgewählten Elemente löschen möchtest?",
|
||||
"no-shopping-lists-found": "Keine Einkaufslisten gefunden"
|
||||
"no-shopping-lists-found": "Keine Einkaufslisten gefunden",
|
||||
"item-checked-off": "{item} erledigt"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle Rezepte",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "Zeige alle {count} Rezepte",
|
||||
"merge-dialog-text": "Zusammenführen der ausgewählten Lebensmittel führt diese zusammen in ein einzelnes Lebensmittel. Die Ausgangslebensmittel werden gelöscht und alle Verweise werden auf das zusammengeführte Lebensmittel angepasst.",
|
||||
"merge-food-example": "{food1} wird zu {food2} zusammengeführt",
|
||||
"seed-dialog-text": "Füllt die Datenbank mit Lebensmitteln basierend auf deiner Landessprache. Hierdurch werden mehr als 200 gängige Lebensmittel eingetragen, die verwendet werden können, um die Datenbank zu organisieren. Die Lebensmittel werden von der Community übersetzt.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Ich habe schon alles eingerichtet, bring mich zur Startseite",
|
||||
"common-settings-for-new-sites": "Hier sind einige allgemeine Einstellungen für neue Seiten",
|
||||
"setup-complete": "Einrichtung abgeschlossen!",
|
||||
"ai-providers-description": "Optionale Konfiguration von KI-Anbietern für Ihre Gruppe. KI-Anbieter ermöglichen Funktionen wie das Erstellen von Rezepten aus Bildern, das Importieren von Rezepten aus Videos und das verbesserte Parsen von Zutaten. Du kannst dies später immer in Ihren Gruppeneinstellungen konfigurieren.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Hier sind einige Funktionen, die dich beim Start mit Mealie unterstützen",
|
||||
"restore-from-v1-backup": "Hast du ein Backup von einer früheren v1 Instanz von Mealie? Hier kannst du es wiederherstellen.",
|
||||
"manage-profile-or-get-invite-link": "Verwalte dein eigenes Profil oder erstelle einen Einladungslink, den du an andere weitergeben kannst."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Kein Leerzeichen erlaubt",
|
||||
"min-length": "Muss mindestens {min} Zeichen haben",
|
||||
"max-length": "Muss maximal {max} Zeichen haben|Muss maximal {max} Zeichen haben"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Ankündigungen",
|
||||
"all-announcements": "Alle Ankündigungen",
|
||||
"mark-all-as-read": "Alle als gelesen markieren",
|
||||
"show-announcements-from-mealie": "Ankündigung von Mealie anzeigen",
|
||||
"show-announcements-setting-description": "Lege fest, ob Benutzer Ankündigungen von Mealie sehen dürfen. Wenn aktiviert, können Benutzer die Anzeige in ihren Benutzereinstellungen immer noch deaktivieren"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Ταμπλό",
|
||||
"delete": "Διαγραφή",
|
||||
"disabled": "Ανενεργό",
|
||||
"done": "Ολοκληρώθηκε",
|
||||
"download": "Λήψη",
|
||||
"duplicate": "Δημιουργία διπλότυπου",
|
||||
"edit": "Επεξεργασία",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Τρίτη",
|
||||
"type": "Τύπος",
|
||||
"undo": "Αναίρεση",
|
||||
"update": "Ενημέρωση",
|
||||
"updated": "Ενημερώθηκε",
|
||||
"upload": "Ανέβασμα",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Εμφάνιση προχωρημένων επιλογών",
|
||||
"add-field": "Προσθήκη πεδίου",
|
||||
"date-created": "Ημερομηνία δημιουργίας",
|
||||
"date-updated": "Ημερομηνία ενημέρωσης"
|
||||
"date-updated": "Ημερομηνία ενημέρωσης",
|
||||
"key": "Κλειδί",
|
||||
"value": "Τιμή"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Θέλετε σίγουρα να διαγράψετε αυτό τον ασφαλή σύνδεσμο <b>{groupName}<b/>;",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Οι αλλαγές σε αυτή την ομάδα θα αντικατοπτρίζονται αμέσως.",
|
||||
"group-id-value": "ID ομάδας: {0}",
|
||||
"total-households": "Σύνολο νοικοκυριών",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Πρέπει να επιλέξετε μια ομάδα πριν επιλέξετε ένα νοικοκυριό"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Πρέπει να επιλέξετε μια ομάδα πριν επιλέξετε ένα νοικοκυριό",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "Ρυθμίσεις παρόχου τεχνητής νοημοσύνης",
|
||||
"ai-provider": "Πάροχος τεχνητής νοημοσύνης",
|
||||
"ai-providers": "Πάροχοι τεχνητής νοημοσύνης",
|
||||
"ai-provider-settings-description": "Ρυθμίστε τους παρόχους τεχνητής νοημοσύνης για να ενεργοποιήσετε τις λειτουργίες AI, όπως βελτιωμένη ανάλυση συστατικών, δημιουργία συνταγών από βίντεο και πολλά άλλα!",
|
||||
"providers": "Πάροχοι",
|
||||
"create-provider": "Δημιουργία παρόχου",
|
||||
"edit-provider": "Επεξεργασία παρόχου",
|
||||
"default-provider": "Προεπιλεγμένος πάροχος",
|
||||
"default-provider-description": "Απαιτείται για την ενεργοποίηση των χαρακτηριστικών τεχνητής νοημοσύνης",
|
||||
"audio-provider": "Πάροχος ήχου",
|
||||
"audio-provider-description": "Ενεργοποιεί λειτουργίες μεταγραφής ήχου, όπως η δημιουργία συνταγών από βίντεο",
|
||||
"image-provider": "Πάροχος εικόνων",
|
||||
"image-provider-description": "Ενεργοποιεί δυνατότητες αναγνώρισης εικόνας, όπως η δημιουργία συνταγών από εικόνες",
|
||||
"provider-name": "Ονομα παρόχου",
|
||||
"api-key": "Κλειδί API",
|
||||
"api-key-description-create": "Το κλειδί API του παρόχου σας για έλεγχο ταυτότητας. Αν η υπηρεσία σας (π.χ. Ollama) δεν χρησιμοποιεί ένα κλειδί API, πρέπει να βάλετε κάτι εδώ.",
|
||||
"api-key-description-edit": "Αφήστε το κενό εκτός αν θέλετε να το αλλάξετε.",
|
||||
"base-url": "Βασική διεύθυνση URL",
|
||||
"base-url-description": "Αν χρησιμοποιείτε το OpenAI αφήστε αυτό το κενό. Πρέπει να είναι ένα OpenAI συμβατό endpoint (π.χ. \"http://localhost:11434/v1\").",
|
||||
"model": "Μοντέλο",
|
||||
"model-description": "Ποιο μοντέλο θα πρέπει να χρησιμοποιήσει ο πάροχος τεχνητής νοημοσύνης σας (π.χ. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Χρονικό όριο αιτήματος (δευτερόλεπτα)",
|
||||
"provider-created": "Ο πάροχος δημιουργήθηκε",
|
||||
"provider-updated": "Ο πάροχος ενημερώθηκε",
|
||||
"provider-deleted": "Ο πάροχος διαγράφτηκε",
|
||||
"provider-create-failed": "Αποτυχία δημιουργίας παρόχου",
|
||||
"provider-update-failed": "Αποτυχία ενημέρωσης παρόχου",
|
||||
"provider-delete-failed": "Αποτυχία διαγραφής παρόχου",
|
||||
"request-headers": "Κεφαλίδες αιτήματος",
|
||||
"request-params": "Παράμετροι αιτήματος",
|
||||
"no-default-provider-warning": "Δεν έχετε ορίσει προεπιλεγμένο πάροχο, οπότε οι λειτουργίες τεχνητής νοημοσύνης είναι απενεργοποιημένες"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Νοικοκυριό",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Δημιουργήστε μια νέα συνταγή από το μηδέν.",
|
||||
"create-recipes": "Δημιουργία Συνταγών",
|
||||
"import-with-zip": "Εισαγωγή μέσω .zip",
|
||||
"create-recipe-from-an-image": "Δημιουργία συνταγής από μια εικόνα",
|
||||
"create-recipe-from-images": "Δημιουργία συνταγής από εικόνες",
|
||||
"create-recipe-from-an-image-description": "Δημιουργήστε μια συνταγή ανεβάζοντας μια εικόνα της. Το Mealie θα προσπαθήσει να εξάγει το κείμενο από την εικόνα χρησιμοποιώντας τεχνητή νοημοσύνη και να δημιουργήσει μια συνταγή από αυτό.",
|
||||
"crop-and-rotate-the-image": "Περικοπή και περιστροφή της εικόνας, έτσι ώστε να είναι μόνο το κείμενο ορατό και να είναι στο σωστό προσανατολισμό.",
|
||||
"create-from-images": "Δημιουργία από εικόνες",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Ποσότητα: {0}",
|
||||
"shopping-list": "Λίστα για ψώνια",
|
||||
"shopping-lists": "Λίστες για ψώνια",
|
||||
"add-item": "Προσθήκη στοιχείου",
|
||||
"food": "Τρόφιμο",
|
||||
"note": "Σημείωση",
|
||||
"label": "Ετικέτα",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Θέλετε σίγουρα να επιλέξετε όλα τα αντικείμενα;",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Θέλετε σίγουρα να αποεπιλέξετε όλα τα αντικείμενα;",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Θέλετε σίγουρα να διαγράψετε όλα τα επιλεγμένα αντικείμενα;",
|
||||
"no-shopping-lists-found": "Δεν βρέθηκαν λίστες για ψώνια"
|
||||
"no-shopping-lists-found": "Δεν βρέθηκαν λίστες για ψώνια",
|
||||
"item-checked-off": "Το {item} επισημάνθηκε ως ολοκληρωμένο"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Συνταγές όλες",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Προειδοποίηση: αυτό το τρόφιμο χρησιμοποιείται σε {count} συνταγή(ες). Η διαγραφή του θα αφήσει ένα άδειο συστατικό στη συνταγή(ες).",
|
||||
"delete-affects-recipes-more": "Προβολή όλων των {count} συνταγών",
|
||||
"merge-dialog-text": "Ο συνδυασμός των επιλεγμένων τροφίμων θα συγχωνεύσει το αρχικό τρόφιμο και το τρόφιμο στόχος σε ένα μόνο τρόφιμο. Το αρχικό τρόφιμο θα διαγραφεί και όλες οι αναφορές σε αυτό θα ενημερωθούν ώστε να δείχνουν στο τρόφιμο-στόχο.",
|
||||
"merge-food-example": "Συγχώνευση {food1} στο {food2}",
|
||||
"seed-dialog-text": "Τροδοδοτήστε τη βάση δεδομένων με τρόφιμα που βασίζονται στην τοπική σας γλώσσα. Αυτό θα δημιουργήσει 200+ κοινά τρόφιμα που μπορούν να χρησιμοποιηθούν για την οργάνωση της βάσης δεδομένων σας. Τα τρόφιμα μεταφράζονται μέσω μιας προσπάθειας της κοινότητας.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Εχω ήδη κάνει εγκατάσταση, απλά πήγαινέ με στην αρχική σελίδα",
|
||||
"common-settings-for-new-sites": "Εδώ είναι μερικές κοινές ρυθμίσεις για νέους ιστότοπους",
|
||||
"setup-complete": "Η εγκατάσταση ολοκληρώθηκε!",
|
||||
"ai-providers-description": "Προαιρετικά ρυθμίστε τους παρόχους τεχνητής νοημοσύνης για την ομάδα σας. Οι πάροχοι τεχνητής νοημοσύνης ενεργοποιούν λειτουργίες όπως η δημιουργία συνταγών από εικόνες, η εισαγωγή συνταγών από βίντεο και η βελτιωμένη ανάλυση συστατικών. Αυτό μπορείτε πάντα να το διαμορφώσετε αργότερα από τις ρυθμίσεις της ομάδας σας.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Εδώ είναι μερικά πράγματα που θα σας βοηθήσουν να ξεκινήσετε με το Mealie",
|
||||
"restore-from-v1-backup": "Εχετε ένα αντίγραφο ασφαλείας από μια προηγούμενη υπόσταση του Mealie v1; Μπορείτε να το επαναφέρετε εδώ.",
|
||||
"manage-profile-or-get-invite-link": "Διαχειριστείτε το δικό σας προφίλ, ή λάβετε έναν σύνδεσμο πρόσκλησης για να μοιραστείτε με άλλους."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Δεν επιτρέπονται κενοί χαρακτήρες",
|
||||
"min-length": "Πρέπει να αποτελείται από τουλάχιστον {min} χαρακτήρες",
|
||||
"max-length": "Πρέπει να αποτελείται το πολύ από {max} χαρακτήρα|Πρέπει να αποτελείται το πολύ από {max} χαρακτήρες"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Ανακοινώσεις",
|
||||
"all-announcements": "Oλες οι ανακοινώσεις",
|
||||
"mark-all-as-read": "Σήμανση όλων ως αναγνωσμένων",
|
||||
"show-announcements-from-mealie": "Εμφάνιση ανακοινώσεων από το Mealie",
|
||||
"show-announcements-setting-description": "Εάνε θέλετε ή όχι να επιτρέψετε στους χρήστες να βλέπουν ανακοινώσεις από την Mealie. Οταν είναι ενεργοποιημένο οι χρήστες μπορούν ακόμα να απεγγραφούν από αυτές από τις ρυθμίσεις χρήστη τους ώστε να μην τις βλέπουν"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"disabled": "Disabled",
|
||||
"done": "Done",
|
||||
"download": "Download",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Tuesday",
|
||||
"type": "Type",
|
||||
"undo": "Undo",
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"upload": "Upload",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Show Advanced",
|
||||
"add-field": "Add Field",
|
||||
"date-created": "Date Created",
|
||||
"date-updated": "Date Updated"
|
||||
"date-updated": "Date Updated",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
|
||||
@@ -261,7 +265,7 @@
|
||||
"enable-public-access": "Enable Public Access",
|
||||
"enable-public-access-description": "Make group recipes public by default, and allow visitors to view recipes without logging-in",
|
||||
"allow-users-outside-of-your-group-to-see-your-recipes": "Allow users outside your group to see your recipes",
|
||||
"allow-users-outside-of-your-group-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 group or with a pre-generated private link",
|
||||
"allow-users-outside-of-your-group-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorising the user. When disabled, you can only share recipes with users who are in your group or with a pre-generated private link",
|
||||
"show-nutrition-information": "Show nutrition information",
|
||||
"show-nutrition-information-description": "When enabled the nutrition information will be shown on the recipe if available. If there is no nutrition information available the nutrition information will now be shown",
|
||||
"show-recipe-assets": "Show recipe assets",
|
||||
@@ -270,7 +274,7 @@
|
||||
"default-to-landscape-view-description": "When enabled the recipe header section will be shown in landscape view",
|
||||
"disable-users-from-commenting-on-recipes": "Disable users from commenting on recipes",
|
||||
"disable-users-from-commenting-on-recipes-description": "Hides the comment section on the recipe page and disables commenting",
|
||||
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organizing recipe ingredients by units and food",
|
||||
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organising recipe ingredients by units and food",
|
||||
"disable-organizing-recipe-ingredients-by-units-and-food-description": "Hides the Food, Unit, and Amount fields for ingredients and treats ingredients as plain text fields",
|
||||
"general-preferences": "General Preferences",
|
||||
"group-recipe-preferences": "Group Recipe Preferences",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Changes to this group will be reflected immediately.",
|
||||
"group-id-value": "Group 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": "You must select a group before selecting a household",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Household",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"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.",
|
||||
"create-from-images": "Create from Images",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Quantity: {0}",
|
||||
"shopping-list": "Shopping List",
|
||||
"shopping-lists": "Shopping Lists",
|
||||
"add-item": "Add item",
|
||||
"food": "Food",
|
||||
"note": "Note",
|
||||
"label": "Label",
|
||||
@@ -939,7 +977,8 @@
|
||||
"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-delete-checked-items": "Are you sure you want to delete all checked items?",
|
||||
"no-shopping-lists-found": "No Shopping Lists Found"
|
||||
"no-shopping-lists-found": "No Shopping Lists Found",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "All Recipes",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||
"merge-food-example": "Merging {food1} into {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1152,8 +1193,8 @@
|
||||
"cup": "cup",
|
||||
"ounce": "ounce",
|
||||
"pound": "pound",
|
||||
"milliliter": "milliliter",
|
||||
"liter": "liter",
|
||||
"milliliter": "millilitre",
|
||||
"liter": "litre",
|
||||
"gram": "gram",
|
||||
"kilogram": "kilogram"
|
||||
}
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "I'm already set up, just bring me to the homepage",
|
||||
"common-settings-for-new-sites": "Here are some common settings for new sites",
|
||||
"setup-complete": "Setup Complete!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie",
|
||||
"restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.",
|
||||
"manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"min-length": "Must Be At Least {min} Characters",
|
||||
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Tuesday",
|
||||
"type": "Type",
|
||||
"undo": "Undo",
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"upload": "Upload",
|
||||
@@ -222,7 +223,9 @@
|
||||
"show-advanced": "Show Advanced",
|
||||
"add-field": "Add Field",
|
||||
"date-created": "Date Created",
|
||||
"date-updated": "Date Updated"
|
||||
"date-updated": "Date Updated",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
|
||||
@@ -282,7 +285,40 @@
|
||||
"admin-group-management-text": "Changes to this group will be reflected immediately.",
|
||||
"group-id-value": "Group 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": "You must select a group before selecting a household",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Household",
|
||||
@@ -426,7 +462,7 @@
|
||||
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
|
||||
"plantoeat": {
|
||||
"title": "Plan to Eat",
|
||||
"description-long": "Mealie can import recipies from Plan to Eat."
|
||||
"description-long": "Mealie can import recipes from Plan to Eat. Upload a ZIP archive, CSV, or TXT file exported from Plan to Eat."
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "My Recipe Box",
|
||||
@@ -627,7 +663,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from Images",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading images of the recipe text. Mealie will attempt to extract the text from the images using AI and create a new 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.",
|
||||
"create-from-images": "Create from Images",
|
||||
@@ -916,6 +952,7 @@
|
||||
"quantity": "Quantity: {0}",
|
||||
"shopping-list": "Shopping List",
|
||||
"shopping-lists": "Shopping Lists",
|
||||
"add-item": "Add item",
|
||||
"food": "Food",
|
||||
"note": "Note",
|
||||
"label": "Label",
|
||||
@@ -940,7 +977,8 @@
|
||||
"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-delete-checked-items": "Are you sure you want to delete all checked items?",
|
||||
"no-shopping-lists-found": "No Shopping Lists Found"
|
||||
"no-shopping-lists-found": "No Shopping Lists Found",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "All Recipes",
|
||||
@@ -1106,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||
"merge-food-example": "Merging {food1} into {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create ~2700 common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1359,6 +1399,8 @@
|
||||
"already-set-up-bring-to-homepage": "I'm already set up, just bring me to the homepage",
|
||||
"common-settings-for-new-sites": "Here are some common settings for new sites",
|
||||
"setup-complete": "Setup Complete!",
|
||||
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Here are a few things to help you get started with Mealie",
|
||||
"restore-from-v1-backup": "Have a backup from a previous instance of Mealie v1? You can restore it here.",
|
||||
"manage-profile-or-get-invite-link": "Manage your own profile, or grab an invite link to share with others."
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Panel de control",
|
||||
"delete": "Eliminar",
|
||||
"disabled": "Deshabilitado",
|
||||
"done": "Done",
|
||||
"download": "Descargar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Martes",
|
||||
"type": "Tipo",
|
||||
"undo": "Undo",
|
||||
"update": "Actualizar",
|
||||
"updated": "Actualizado",
|
||||
"upload": "Subir",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Mostrar Avanzado",
|
||||
"add-field": "Añadir campo",
|
||||
"date-created": "Fecha de creación",
|
||||
"date-updated": "Fecha de actualización"
|
||||
"date-updated": "Fecha de actualización",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Por favor, confirma que deseas eliminar <b>{groupName}<b/>",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Los cambios en este grupo se reflejarán inmediatamente.",
|
||||
"group-id-value": "Id del Grupo: {0}",
|
||||
"total-households": "Total de Casas",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Debe seleccionar un grupo antes de seleccionar un hogar"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Debe seleccionar un grupo antes de seleccionar un hogar",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Casa",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Crear nueva receta desde cero.",
|
||||
"create-recipes": "Crear Recetas",
|
||||
"import-with-zip": "Importar desde .zip",
|
||||
"create-recipe-from-an-image": "Crear receta a partir de una imagen",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crea una receta cargando una imagen de ella. Mealie intentará extraer el texto de la imagen usando IA y crear una receta de ella.",
|
||||
"crop-and-rotate-the-image": "Recortar y rotar la imagen de manera que sólo el texto sea visible, y esté en la orientación correcta.",
|
||||
"create-from-images": "Crear a partir de imágenes",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Cantidad: {0}",
|
||||
"shopping-list": "Lista de la compra",
|
||||
"shopping-lists": "Listas de la compra",
|
||||
"add-item": "Add item",
|
||||
"food": "Alimentos",
|
||||
"note": "Nota",
|
||||
"label": "Etiqueta",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "¿Seguro que quieres seleccionar todos los elementos?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "¿Seguro que quieres de-seleccionar todos los elementos?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "¿Está seguro que deseas eliminar los elementos seleccionados?",
|
||||
"no-shopping-lists-found": "No hay listas de la compra"
|
||||
"no-shopping-lists-found": "No hay listas de la compra",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Recetas",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combinar los alimentos seleccionados fusionará el alimento origen y destinatario en un solo alimento. El alimento origen será eliminado y todas las referencias a él serán actualizadas para apuntar al nuevo alimento.",
|
||||
"merge-food-example": "Fusionando {food1} con {food2}",
|
||||
"seed-dialog-text": "Rellena la base de datos con alimentos basados en tu idioma local. Esto creará más de 200 alimentos comunes que se pueden usar para organizar tu base de datos. Los alimentos son traducidos a través de un esfuerzo comunitario.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Estoy bien, solo llévame al Inicio",
|
||||
"common-settings-for-new-sites": "Aquí hay algunos ajustes comunes para sitios nuevos",
|
||||
"setup-complete": "¡Configuración completada!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Aquí hay algunas cosas para ayudarte a empezar con Mealie",
|
||||
"restore-from-v1-backup": "¿Tienes una copia de seguridad de Mealie v1? Puedes restaurarla aquí.",
|
||||
"manage-profile-or-get-invite-link": "Gestiona tu perfil, o usa un enlace de invitación para compartir con otros."
|
||||
@@ -1462,7 +1504,7 @@
|
||||
"is-not-like": "no es como"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": ""
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No se permiten espacios en blanco",
|
||||
"min-length": "Debe tener como mínimo {min} caracteres",
|
||||
"max-length": "Debe Ser Como Máximo {max} Carácter|Debe Ser Como Máximo {max} Caracteres"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Töölaud",
|
||||
"delete": "Kustuta",
|
||||
"disabled": "Keelatud",
|
||||
"done": "Done",
|
||||
"download": "Lae alla",
|
||||
"duplicate": "Duplitseeri",
|
||||
"edit": "Muuda",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Identifikaator",
|
||||
"tuesday": "Teisipäev",
|
||||
"type": "Tüüp",
|
||||
"undo": "Undo",
|
||||
"update": "Uuenda",
|
||||
"updated": "Uuendatud",
|
||||
"upload": "Lae üles",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Kuva täpsemad sätted",
|
||||
"add-field": "Lisa väli",
|
||||
"date-created": "Loomise kuupäev",
|
||||
"date-updated": "Üleslaadimise kuupäev"
|
||||
"date-updated": "Üleslaadimise kuupäev",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Kas oled kindel, et tahad kustutada <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Selle grupi muudatused on koheselt nähtavad",
|
||||
"group-id-value": "Grupi ID: {0}",
|
||||
"total-households": "Kokku leibkondi",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Sa pead valima grupi enne leibkonna valimist"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Sa pead valima grupi enne leibkonna valimist",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Leibkond",
|
||||
@@ -569,7 +606,7 @@
|
||||
"failed-to-add-recipe-to-mealplan": "Retsepti lisamine toitumisplaani ebaõnnestus",
|
||||
"failed-to-add-to-list": "Nimekirja lisamine ebaõnnestus",
|
||||
"yield": "Saagikus",
|
||||
"yields-amount-with-text": "",
|
||||
"yields-amount-with-text": "Yields {amount} {text}",
|
||||
"yield-text": "Saaduse tekst",
|
||||
"quantity": "Kogus",
|
||||
"choose-unit": "Vali ühik",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Loo uus retsept algusest",
|
||||
"create-recipes": "Loo retseptid",
|
||||
"import-with-zip": "Impordi .zip failist",
|
||||
"create-recipe-from-an-image": "Retsepti loomine pildist",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Retsepti loomiseks lae üles selle pilt. Mealie üritab ekstraheerida pildil oleva teksti ning luua retsepti sellest kasutades AI-d.",
|
||||
"crop-and-rotate-the-image": "Kärpige ja pöörake pilti nii, et ainult tekst oleks nähtaval ja see oleks suunatud ülespoole.",
|
||||
"create-from-images": "Retsepti loomine pildist",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Kogus: {0}",
|
||||
"shopping-list": "Ostunimekiri",
|
||||
"shopping-lists": "Ostunimekirjad",
|
||||
"add-item": "Add item",
|
||||
"food": "Toit",
|
||||
"note": "Märkus",
|
||||
"label": "Silt",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Kas oled kindel, et tahad valida kõik üksused?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Kas oled kindel, et tahad tühistada kõik valikud?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Kas oled kindel, et tahad kustutada kõik valitud üksused?",
|
||||
"no-shopping-lists-found": "Poenimekirja ei leitud"
|
||||
"no-shopping-lists-found": "Poenimekirja ei leitud",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Kõik retseptid",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Valitud toitude kombineerimine ühendab koostisained ja keskendub ühele roale. Lähtetoidud eemaldatakse ja kõik viited lähtetoidule värskendatakse, et osutada sihttoidule.",
|
||||
"merge-food-example": "{food1} liitmine {food2}-ga",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Ma olen juba vajalikud asjad seadistanud, vii mind pealehele",
|
||||
"common-settings-for-new-sites": "Siin on mõned harilikud sätted uute lehekülgede jaoks",
|
||||
"setup-complete": "Seadistus valmis!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Siin on mõned asjad mis aitavad sul teha algust Mealie-ga",
|
||||
"restore-from-v1-backup": "Kas sul on tagavarakoopia varasemast Mealie v1 instantsist? Sa saad taastada selle siin.",
|
||||
"manage-profile-or-get-invite-link": "Halda oma profiili, või haara kutselink teistega jagamiseks."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"min-length": "Must Be At Least {min} Characters",
|
||||
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"category": "Kategoria"
|
||||
},
|
||||
"events": {
|
||||
"apprise-url": "Apprise URL",
|
||||
"apprise-url": "Apprise-url",
|
||||
"database": "Tietokanta",
|
||||
"delete-event": "Poista tapahtuma",
|
||||
"event-delete-confirmation": "Haluatko varmasti poistaa tämän tapahtuman?",
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Hallintanäkymä",
|
||||
"delete": "Poista",
|
||||
"disabled": "Poistettu käytöstä",
|
||||
"done": "Valmis",
|
||||
"download": "Lataa",
|
||||
"duplicate": "Monista",
|
||||
"edit": "Muokkaa",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Tunniste",
|
||||
"tuesday": "Tiistai",
|
||||
"type": "Tyyppi",
|
||||
"undo": "Peru",
|
||||
"update": "Päivitä",
|
||||
"updated": "Päivitetty",
|
||||
"upload": "Lähetä",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Näytä Lisäasetukset",
|
||||
"add-field": "Lisää Kenttä",
|
||||
"date-created": "Luontipäivä",
|
||||
"date-updated": "Päivitetty"
|
||||
"date-updated": "Päivitetty",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Haluatko varmasti poistaa ryhmän <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Muutokset tähän ryhmään tulevat näkymään välittömästi.",
|
||||
"group-id-value": "Ryhmän tunniste: {0}",
|
||||
"total-households": "Kotitaloudet Yhteensä",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Sinun tulee valita ryhmä ennen kuin valitset kotitalouden"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Sinun tulee valita ryhmä ennen kuin valitset kotitalouden",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Kotitalous",
|
||||
@@ -331,8 +368,8 @@
|
||||
"any-household": "Mikä tahansa kotitalous",
|
||||
"no-meal-plan-defined-yet": "Ateriasuunnitelmaa ei ole vielä määritelty",
|
||||
"no-meal-planned-for-today": "Ei ateriasuunnitelmaa tälle päivälle",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-label": "Default Days in the Past",
|
||||
"numberOfDaysPast-hint": "Menneisyydestä ladattujen päivien määrä",
|
||||
"numberOfDaysPast-label": "Oletusarvo menneiden päivien lataukselle",
|
||||
"numberOfDays-hint": "Sivun latauspäivien lukumäärä",
|
||||
"numberOfDays-label": "Oletuspäivät",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Vain näiden luokkien reseptejä käytetään ateriasuunnitelmissa",
|
||||
@@ -390,7 +427,7 @@
|
||||
"nextcloud": {
|
||||
"description": "Tuo tiedot Nextcloudin Cookbookista",
|
||||
"description-long": "Nextcloud reseptejä voidaan tuoda zip-tiedostosta, joka sisältää Nextcloudin tallennetut tiedot. Katso esimerkkikansiorakenne alla varmistaaksesi, että reseptisi voidaan tuoda.",
|
||||
"title": "Nextcloud Cookbook"
|
||||
"title": "Nextcloud-keittokirja"
|
||||
},
|
||||
"copymethat": {
|
||||
"description-long": "Mealie voi tuoda reseptejä Copy Me That -sovelluksesta. Vie reseptisi HTML-muodossa ja lataa sitten zip-tiedosto.",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Luo resepti alusta.",
|
||||
"create-recipes": "Luo reseptejä",
|
||||
"import-with-zip": "Tuo .zip:llä",
|
||||
"create-recipe-from-an-image": "Luo resepti kuvasta",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Luo resepti tuomalla siitä kuva. Mealie pyrkii poimimaan tekstin kuvasta tekoälyllä ja luomaan siitä reseptin.",
|
||||
"crop-and-rotate-the-image": "Rajaa ja kierrä kuvaa niin, että vain teksti näkyy, ja että se on oikein päin.",
|
||||
"create-from-images": "Luo resepti kuvasta",
|
||||
@@ -700,7 +737,7 @@
|
||||
"confidence-score": "Varmuuspisteet",
|
||||
"ingredient-parser-description": "Ainesosat on haettu onnistuneesti. Ole hyvä ja tarkista ainesosat joista emme ole varmoja.",
|
||||
"ingredient-parser-final-review-description": "Kun kaikki ainesosat on tarkistettu, sinulla on vielä yksi mahdollisuus tarkistaa kaikki ainesosat ennen kuin muokkaat reseptiäsi.",
|
||||
"add-text-as-alias-for-item": "",
|
||||
"add-text-as-alias-for-item": "Lisää \"{text}\" kohteen {item} aliakseksi",
|
||||
"delete-item": "Poista kohde"
|
||||
},
|
||||
"reset-servings-count": "Palauta Annoksien Määrä",
|
||||
@@ -891,17 +928,17 @@
|
||||
"server-side-base-url-error-text": "`BASE_URL` on API-palvelimen oletusarvo. Tämä aiheuttaa ongelmia ilmoitusten linkkien kanssa, jotka on luotu palvelimella sähköposteja varten jne.",
|
||||
"server-side-base-url-success-text": "Palvelimen nettiosoite ei vastaa oletusta",
|
||||
"ldap-ready": "LDAP Valmis",
|
||||
"ldap-not-ready": "LDAP Not Ready",
|
||||
"ldap-not-ready": "LDAP ei valmis",
|
||||
"ldap-ready-error-text": "Kaikkia LDAP-arvoja ei ole määritetty. Tämä voidaan ohittaa, jos et käytä LDAP-todennusta.",
|
||||
"ldap-ready-success-text": "Kaikki vaaditut LDAP-muuttujat on asetettu.",
|
||||
"build": "Koonti",
|
||||
"recipe-scraper-version": "Reseptikaappaimen versio",
|
||||
"oidc-ready": "OIDC valmis",
|
||||
"oidc-not-ready": "OIDC Not Ready",
|
||||
"oidc-not-ready": "OIDC ei ole valmis",
|
||||
"oidc-ready-error-text": "Kaikkia OIDC-arvoja ei ole määritelty. Jos et käytä OIDC-todennusta, voidaan asia jättää huomiotta.",
|
||||
"oidc-ready-success-text": "Kaikki vaaditut OIDC-muuttujat asetettu.",
|
||||
"openai-ready": "OpenAI valmis",
|
||||
"openai-not-ready": "OpenAI Not Ready",
|
||||
"openai-not-ready": "OpenAI ei ole valmis",
|
||||
"openai-ready-error-text": "Kaikkia OpenAI:n arvoja ei ole määritelty. Tämä voidaan sivuuttaa, mikäli et käytä OpenAI:n toimintoja.",
|
||||
"openai-ready-success-text": "Vaadittavat OpenAI-muuttujat ovat asetetut."
|
||||
},
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Määrä: {0}",
|
||||
"shopping-list": "Ostoslista",
|
||||
"shopping-lists": "Ostoslistat",
|
||||
"add-item": "Lisää kohde",
|
||||
"food": "Elintarvikkeet",
|
||||
"note": "Muistiinpano",
|
||||
"label": "Tunnus",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Haluatko varmasti valita kaikki kohteet?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Haluatko varmasti poistaa kaikki valinnat?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Haluatko varmasti poistaa kaikki valitut kohteet?",
|
||||
"no-shopping-lists-found": "Ostoslistoja ei löytynyt"
|
||||
"no-shopping-lists-found": "Ostoslistoja ei löytynyt",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Reseptit",
|
||||
@@ -958,7 +997,7 @@
|
||||
"language": "Kieli",
|
||||
"maintenance": "Ylläpito",
|
||||
"background-tasks": "Taustatehtävät",
|
||||
"parser": "Parser",
|
||||
"parser": "Jäsentäjä",
|
||||
"developer": "Kehittäjä",
|
||||
"cookbook": "Keittokirja",
|
||||
"create-cookbook": "Luo uusi keittokirja"
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Valittujen elintarvikkeiden yhdistäminen yhdistää raaka-aineet ja kohdistaa ne yhteen ainoaan ruokaan. Lähde-elintarvikkeet poistetaan, ja kaikki viittaukset lähtöelintarvikkeeseen saatetaan ajan tasalle niin, että ne osoittavat kohteena olevan elintarvikkeen.",
|
||||
"merge-food-example": "Yhdistä {food1} ja {food2} yhdeksi",
|
||||
"seed-dialog-text": "Lisää tietokantaan paikallisen kielen mukaisia raaka-aineita. Tämä luo yli 200 yleistä raaka-ainetta, joita voidaan käyttää tietokannan järjestämiseen. Raaka-aineiden nimet ovat yhteisön kääntämiä.",
|
||||
@@ -1347,7 +1388,7 @@
|
||||
"ingredient-text": "Ainesosan Teksti",
|
||||
"average-confident": "{0} Luottamus",
|
||||
"try-an-example": "Kokeile esimerkkiä",
|
||||
"parser": "Parser",
|
||||
"parser": "Jäsentäjä",
|
||||
"background-tasks": "Taustatehtävät",
|
||||
"background-tasks-description": "Täältä voit tarkastella kaikkia käynnissä olevia taustatehtäviä ja niiden tilaa",
|
||||
"no-logs-found": "Lokeja Ei Löytynyt",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Olen jo valmis, vie minut kotisivulle",
|
||||
"common-settings-for-new-sites": "Tässä muutamia yleisiä asetuksia uusille sivustoille",
|
||||
"setup-complete": "Asennus valmis.",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Näillä muutamilla asioilla pääset alkuun",
|
||||
"restore-from-v1-backup": "Onko sinulla varmuuskopio aiemmasta Mealie v1 -instanssista? Palauta se tästä.",
|
||||
"manage-profile-or-get-invite-link": "Hallitse profiiliasi tai hanki kutsulinkki muille."
|
||||
@@ -1372,7 +1414,7 @@
|
||||
"profile": {
|
||||
"welcome-user": "👋 Tervetuloa, {0}!",
|
||||
"description": "Hallitse profiiliasi, reseptejäsi ja ryhmäasetuksiasi.",
|
||||
"invite-link": "",
|
||||
"invite-link": "Invite Link",
|
||||
"get-invite-link": "Hanki Kutsulinkki",
|
||||
"get-public-link": "Julkinen linkki",
|
||||
"account-summary": "Tilin Yhteenveto",
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Tekstissä ei saa olla välilyöntejä",
|
||||
"min-length": "Vähimmäispituus on {min} merkkiä",
|
||||
"max-length": "Saa olla enintään {max} merkki|Saa olla enintään {max} merkkiä"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Näytä Mealien ilmoitukset",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"disabled": "Désactivé",
|
||||
"done": "Terminé",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Jeton",
|
||||
"tuesday": "Mardi",
|
||||
"type": "Type",
|
||||
"undo": "Annuler",
|
||||
"update": "Mettre à jour",
|
||||
"updated": "Mis à jour",
|
||||
"upload": "Importer",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Afficher les paramètres avancés",
|
||||
"add-field": "Ajouter un champ",
|
||||
"date-created": "Date de création",
|
||||
"date-updated": "Date de mise à jour"
|
||||
"date-updated": "Date de mise à jour",
|
||||
"key": "Clé",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Voulez-vous vraiment supprimer <b>{groupName}<b/> ?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Les modifications apportées à ce groupe seront immédiatement prises en compte.",
|
||||
"group-id-value": "ID groupe : {0}",
|
||||
"total-households": "Nombre de foyers",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configurez les fournisseurs d'IA pour activer les fonctionnalités alimentées par l'AI, telles que l'analyse améliorée des ingrédients, la création de recettes à partir de vidéos, et plus encore !",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "Vous n'avez pas défini de fournisseur par défaut, donc les fonctionnalités IA sont désactivées"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Foyer",
|
||||
@@ -331,8 +368,8 @@
|
||||
"any-household": "Tout foyer",
|
||||
"no-meal-plan-defined-yet": "Aucun menu planifié",
|
||||
"no-meal-planned-for-today": "Aucun repas prévu pour aujourd’hui",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-label": "Default Days in the Past",
|
||||
"numberOfDaysPast-hint": "Nombre de jours passé à charger",
|
||||
"numberOfDaysPast-label": "Jours par défaut dans le passé",
|
||||
"numberOfDays-hint": "Nombre de jours lors du chargement de la page",
|
||||
"numberOfDays-label": "Jours par défaut",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Seules les recettes appartenant à ces catégories seront utilisées dans les menus",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Créer une recette depuis une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’une image",
|
||||
@@ -891,17 +928,17 @@
|
||||
"server-side-base-url-error-text": "`BASE_URL` est encore la valeur par défaut sur le serveur API. Cela causera des problèmes avec les liens générés par les notifications sur le serveur pour les e-mails, etc.",
|
||||
"server-side-base-url-success-text": "L'URL du côté du serveur ne correspond pas à celle par défaut",
|
||||
"ldap-ready": "Prêt pour LDAP",
|
||||
"ldap-not-ready": "LDAP Not Ready",
|
||||
"ldap-not-ready": "LDAP n'est pas prêt",
|
||||
"ldap-ready-error-text": "Toutes les valeurs LDAP ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n'utilisez pas l'authentification LDAP.",
|
||||
"ldap-ready-success-text": "Les variables LDAP obligatoires sont toutes définies.",
|
||||
"build": "Build",
|
||||
"recipe-scraper-version": "Version du Scraper de recette",
|
||||
"oidc-ready": "Prêt pour OIDC",
|
||||
"oidc-not-ready": "OIDC Not Ready",
|
||||
"oidc-not-ready": "OIDC n'est pas prêt",
|
||||
"oidc-ready-error-text": "Toutes les valeurs OIDC ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n’utilisez pas l’authentification OIDC.",
|
||||
"oidc-ready-success-text": "Les variables OIDC obligatoires sont toutes définies.",
|
||||
"openai-ready": "Prêt pour OpenAI",
|
||||
"openai-not-ready": "OpenAI Not Ready",
|
||||
"openai-not-ready": "OpenAI n'est pas prêt",
|
||||
"openai-ready-error-text": "Toutes les valeurs OpenAI ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n'utilisez pas les fonctionnalités OpenAI.",
|
||||
"openai-ready-success-text": "Les variables OpenAI obligatoires sont toutes définies."
|
||||
},
|
||||
@@ -915,8 +952,9 @@
|
||||
"quantity": "Quantité : {0}",
|
||||
"shopping-list": "Liste de courses",
|
||||
"shopping-lists": "Listes de courses",
|
||||
"add-item": "Ajouter un article",
|
||||
"food": "Aliment",
|
||||
"note": "Note",
|
||||
"note": "Remarque",
|
||||
"label": "Étiquette",
|
||||
"save-label": "Sauvegarder le libellé",
|
||||
"linked-item-warning": "Cet article est lié à une ou plusieurs recettes. Ajuster les unités ou les aliments donnera des résultats inattendus lors de l’ajout ou de la suppression de la recette de cette liste.",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Voulez-vous vraiment sélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Voulez-vous vraiment désélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Voulez-vous vraiment supprimer tous les éléments sélectionnés ?",
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée"
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée",
|
||||
"item-checked-off": "{item} coché"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Recettes",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "La combinaison des aliments sélectionnés fusionnera l’aliment source et l’aliment cible en un seul aliment. L’aliment source sera supprimé et toutes les références à l’aliment source seront mises à jour pour pointer vers l’aliment cible.",
|
||||
"merge-food-example": "Fusion de {food1} dans {food2}",
|
||||
"seed-dialog-text": "Initialisez la base de données avec des aliments basés sur votre langue locale. Cela permettra de créer plus de 200 aliments communs qui pourront être utilisés pour organiser votre base de données. Les aliments sont traduits grâce à un effort communautaire.",
|
||||
@@ -1347,7 +1388,7 @@
|
||||
"ingredient-text": "Texte de l'ingrédient",
|
||||
"average-confident": "Confiant à {0}",
|
||||
"try-an-example": "Essayez avec un exemple",
|
||||
"parser": "Parser",
|
||||
"parser": "Analyseur syntaxique",
|
||||
"background-tasks": "Tâches en arrière plan",
|
||||
"background-tasks-description": "Ici vous pouvez voir toutes les tâches en arrière-plan en cours et leur statut",
|
||||
"no-logs-found": "Pas de journaux trouvés",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "J’ai déjà tout configuré, amenez moi à l’écran d’accueil",
|
||||
"common-settings-for-new-sites": "Voici quelques paramètres courants pour les nouveaux sites",
|
||||
"setup-complete": "Configuration terminée !",
|
||||
"ai-providers-description": "Vous pouvez configurer un fournisseur d'IA pour votre groupe. Les fournisseurs d'IA permettent d'activer des fonctionnalités telles que la création de recettes depuis des images, l'importation de recettes depuis des vidéos ainsi qu'un traitement amélioré des ingrédients. Vous pourrez toujours configurer cela plus tard dans les paramètres de votre groupe.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Voici quelques trucs pour vous aider à commencer avec Mealie",
|
||||
"restore-from-v1-backup": "Vous avez une sauvegarde d’une précédente instance de Mealie v1 ? Vous pouvez la restaurer ici.",
|
||||
"manage-profile-or-get-invite-link": "Gérez votre propre profil, ou récupérez un lien d’invitation à partager avec d’autres."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Aucun espace n'est autorisé",
|
||||
"min-length": "Doit contenir au moins {min} caractères",
|
||||
"max-length": "Doit comporter au maximum {max} caractère|Doit comporter au maximum {max} caractères"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Annonces",
|
||||
"all-announcements": "Toutes les annonces",
|
||||
"mark-all-as-read": "Marquer tout comme lu",
|
||||
"show-announcements-from-mealie": "Afficher les annonces de Mealie",
|
||||
"show-announcements-setting-description": "Si vous voulez autoriser ou non les utilisateurs à voir les annonces de Mealie. Lorsque cette option est activée, les utilisateurs peuvent toujours refuser de les voir dans leurs paramètres utilisateur"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"log-lines": "Lignes du journal",
|
||||
"not-demo": "Non démo",
|
||||
"portfolio": "Portfolio",
|
||||
"production": "Production",
|
||||
"production": "Réalisation",
|
||||
"support": "Soutenir",
|
||||
"version": "Version",
|
||||
"unknown-version": "inconnu",
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"disabled": "Désactivé",
|
||||
"done": "Terminé",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Jeton",
|
||||
"tuesday": "Mardi",
|
||||
"type": "Type",
|
||||
"undo": "Annuler",
|
||||
"update": "Mettre à jour",
|
||||
"updated": "Mis à jour",
|
||||
"upload": "Importer",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Afficher les paramètres avancés",
|
||||
"add-field": "Ajouter un champ",
|
||||
"date-created": "Date de création",
|
||||
"date-updated": "Date de mise à jour"
|
||||
"date-updated": "Date de mise à jour",
|
||||
"key": "Clé",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Êtes-vous certain de vouloir supprimer <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Les modifications apportées à ce groupe seront immédiatement prises en compte.",
|
||||
"group-id-value": "ID groupe : {0}",
|
||||
"total-households": "Nombre de foyers",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configurez les fournisseurs d'IA pour activer les fonctionnalités alimentées par l'AI, telles que l'analyse améliorée des ingrédients, la création de recettes à partir de vidéos, et plus encore !",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "Vous n'avez pas défini de fournisseur par défaut, donc les fonctionnalités IA sont désactivées"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Foyer",
|
||||
@@ -331,8 +368,8 @@
|
||||
"any-household": "Tout foyer",
|
||||
"no-meal-plan-defined-yet": "Aucun menu planifié",
|
||||
"no-meal-planned-for-today": "Aucun repas prévu pour aujourd'hui",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-label": "Default Days in the Past",
|
||||
"numberOfDaysPast-hint": "Nombre de jours passé à charger",
|
||||
"numberOfDaysPast-label": "Jours par défaut dans le passé",
|
||||
"numberOfDays-hint": "Nombre de jours lors du chargement de la page",
|
||||
"numberOfDays-label": "Jours par défaut",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Seules les recettes appartenant à ces catégories seront utilisées dans les menus",
|
||||
@@ -390,7 +427,7 @@
|
||||
"nextcloud": {
|
||||
"description": "Importer des recettes depuis un livre de recettes Nextcloud existant",
|
||||
"description-long": "Les recettes Nextcloud peuvent être importées depuis un fichier zip qui contient les données stockées dans Nextcloud. Consultez la structure de dossiers d'exemple ci-dessous pour vous assurer que vos recettes peuvent être importées.",
|
||||
"title": "Nextcloud Cookbook"
|
||||
"title": "Livre de recettes Nextcloud (Cookbook)"
|
||||
},
|
||||
"copymethat": {
|
||||
"description-long": "Mealie peut importer des recettes à partir de Copy Me That. Exportez vos recettes au format HTML, puis téléverser-le .zip ci-dessous.",
|
||||
@@ -513,7 +550,7 @@
|
||||
"milligrams": "milligrammes",
|
||||
"new-key-name": "Nouveau nom de clé",
|
||||
"no-white-space-allowed": "Aucun espace blanc autorisé",
|
||||
"note": "Note",
|
||||
"note": "Remarque",
|
||||
"nutrition": "Valeurs nutritionnelles",
|
||||
"object-key": "Clé d'objet",
|
||||
"object-value": "Valeur d'objet",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette à partir de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Créer une recette depuis une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléversant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
@@ -891,17 +928,17 @@
|
||||
"server-side-base-url-error-text": "`BASE_URL` est encore la valeur par défaut sur le serveur API. Cela causera des problèmes avec les liens générés par les notifications sur le serveur pour les e-mails, etc.",
|
||||
"server-side-base-url-success-text": "L'URL du côté du serveur ne correspond pas à celle par défaut",
|
||||
"ldap-ready": "Prêt pour LDAP",
|
||||
"ldap-not-ready": "LDAP Not Ready",
|
||||
"ldap-not-ready": "LDAP n'est pas prêt",
|
||||
"ldap-ready-error-text": "Toutes les valeurs LDAP ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n'utilisez pas l'authentification LDAP.",
|
||||
"ldap-ready-success-text": "Les variables LDAP obligatoires sont toutes définies.",
|
||||
"build": "Build",
|
||||
"recipe-scraper-version": "Version du Scraper de recette",
|
||||
"oidc-ready": "Prêt pour OIDC",
|
||||
"oidc-not-ready": "OIDC Not Ready",
|
||||
"oidc-not-ready": "OIDC n'est pas prêt",
|
||||
"oidc-ready-error-text": "Toutes les valeurs OIDC ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n’utilisez pas l’authentification OIDC.",
|
||||
"oidc-ready-success-text": "Les variables OIDC obligatoires sont toutes définies.",
|
||||
"openai-ready": "Prêt pour OpenAI",
|
||||
"openai-not-ready": "OpenAI Not Ready",
|
||||
"openai-not-ready": "OpenAI n'est pas prêt",
|
||||
"openai-ready-error-text": "Toutes les valeurs OpenAI ne sont pas configurées. Vous pouvez ignorer cet avertissement si vous n'utilisez pas les fonctionnalités OpenAI.",
|
||||
"openai-ready-success-text": "Les variables OpenAI obligatoires sont toutes définies."
|
||||
},
|
||||
@@ -915,8 +952,9 @@
|
||||
"quantity": "Quantité : {0}",
|
||||
"shopping-list": "Liste d'épicerie",
|
||||
"shopping-lists": "Listes d'épicerie",
|
||||
"add-item": "Ajouter un article",
|
||||
"food": "Aliments",
|
||||
"note": "Note",
|
||||
"note": "Remarque",
|
||||
"label": "Étiquette",
|
||||
"save-label": "Sauvegarder le libellé",
|
||||
"linked-item-warning": "Cet article est lié à une ou plusieurs recettes. Ajuster les unités ou les aliments donnera des résultats inattendus lors de l’ajout ou de la suppression de la recette de cette liste.",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Voulez-vous vraiment sélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Voulez-vous vraiment désélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Voulez-vous vraiment supprimer tous les éléments sélectionnés ?",
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée"
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée",
|
||||
"item-checked-off": "{item} coché"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Les recettes",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "La combinaison des aliments sélectionnés fusionnera l’aliment source et l’aliment cible en un seul aliment. L’aliment source sera supprimé et toutes les références à l’aliment source seront mises à jour pour pointer vers l’aliment cible.",
|
||||
"merge-food-example": "Fusion de {food1} dans {food2}",
|
||||
"seed-dialog-text": "Initialisez la base de données avec des aliments basés sur votre langue locale. Cela permettra de créer plus de 200 aliments communs qui pourront être utilisés pour organiser votre base de données. Les aliments sont traduits grâce à un effort communautaire.",
|
||||
@@ -1347,7 +1388,7 @@
|
||||
"ingredient-text": "Texte de l'ingrédient",
|
||||
"average-confident": "Confiant à {0}",
|
||||
"try-an-example": "Essayez avec un exemple",
|
||||
"parser": "Parser",
|
||||
"parser": "Analyseur syntaxique",
|
||||
"background-tasks": "Tâches en arrière plan",
|
||||
"background-tasks-description": "Ici vous pouvez voir toutes les tâches en arrière-plan en cours et leur statut",
|
||||
"no-logs-found": "Pas de journaux trouvés",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "J’ai déjà tout configuré, amenez moi à l’écran d’accueil",
|
||||
"common-settings-for-new-sites": "Voici quelques paramètres communs pour les nouveaux sites",
|
||||
"setup-complete": "Configuration terminée!",
|
||||
"ai-providers-description": "Vous pouvez configurer un fournisseur d'IA pour votre groupe. Les fournisseurs d'IA permettent d'activer des fonctionnalités telles que la création de recettes depuis des images, l'importation de recettes depuis des vidéos ainsi qu'un traitement amélioré des ingrédients. Vous pourrez toujours configurer cela plus tard dans les paramètres de votre groupe.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Voici quelques trucs pour vous aider à commencer avec Mealie",
|
||||
"restore-from-v1-backup": "Vous avez une sauvegarde d’une précédente instance de Mealie v1 ? Vous pouvez la restaurer ici.",
|
||||
"manage-profile-or-get-invite-link": "Gérez votre propre profil, ou récupérez un lien d’invitation à partager avec d’autres."
|
||||
@@ -1470,7 +1512,14 @@
|
||||
"invalid-email": "L’e-mail doit être valide",
|
||||
"invalid-url": "Doit être une URL valide",
|
||||
"no-whitespace": "Aucun espace n'est autorisé",
|
||||
"min-length": "",
|
||||
"min-length": "Doit contenir au moins {min} caractères",
|
||||
"max-length": "Doit comporter au maximum {max} caractère|Doit comporter au maximum {max} caractères"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Annonces",
|
||||
"all-announcements": "Toutes les annonces",
|
||||
"mark-all-as-read": "Marquer tout comme lu",
|
||||
"show-announcements-from-mealie": "Afficher les annonces de Mealie",
|
||||
"show-announcements-setting-description": "Si vous voulez autoriser ou non les utilisateurs à voir les annonces de Mealie. Lorsque cette option est activée, les utilisateurs peuvent toujours refuser de les voir dans leurs paramètres utilisateur"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"log-lines": "Lignes de log",
|
||||
"not-demo": "Non",
|
||||
"portfolio": "Portfolio",
|
||||
"production": "Production",
|
||||
"production": "Réalisation",
|
||||
"support": "Soutenir",
|
||||
"version": "Version",
|
||||
"unknown-version": "inconnu",
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"disabled": "Désactivé",
|
||||
"done": "Terminé",
|
||||
"download": "Télécharger",
|
||||
"duplicate": "Dupliquer",
|
||||
"edit": "Modifier",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Jeton",
|
||||
"tuesday": "Mardi",
|
||||
"type": "Type",
|
||||
"undo": "Annuler",
|
||||
"update": "Mettre à jour",
|
||||
"updated": "Mis à jour",
|
||||
"upload": "Importer",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Afficher les paramètres avancés",
|
||||
"add-field": "Ajouter un champ",
|
||||
"date-created": "Date de création",
|
||||
"date-updated": "Date de mise à jour"
|
||||
"date-updated": "Date de mise à jour",
|
||||
"key": "Clé",
|
||||
"value": "Valeur"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Voulez-vous vraiment supprimer <b>{groupName}<b/> ?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Les modifications apportées à ce groupe seront immédiatement prises en compte.",
|
||||
"group-id-value": "ID groupe : {0}",
|
||||
"total-households": "Nombre de foyers",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Vous devez sélectionner un groupe avant de sélectionner un foyer",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "Paramètres du fournisseur d'IA",
|
||||
"ai-provider": "Fournisseur d'IA",
|
||||
"ai-providers": "Fournisseurs d'IA",
|
||||
"ai-provider-settings-description": "Configurez les fournisseurs d'IA pour activer les fonctionnalités alimentées par l'AI, telles que l'analyse améliorée des ingrédients, la création de recettes à partir de vidéos, et plus encore !",
|
||||
"providers": "Fournisseurs",
|
||||
"create-provider": "Créer un fournisseur",
|
||||
"edit-provider": "Éditer le fournisseur",
|
||||
"default-provider": "Fournisseur par défaut",
|
||||
"default-provider-description": "Requis pour activer les fonctionnalités IA",
|
||||
"audio-provider": "Fournisseur audio",
|
||||
"audio-provider-description": "Active les fonctionnalités de transcription audio, comme la création de recettes à partir de vidéos",
|
||||
"image-provider": "Fournisseur d'images",
|
||||
"image-provider-description": "Active les fonctionnalités de reconnaissance d'image, comme la création de recettes à partir d'images",
|
||||
"provider-name": "Nom du fournisseur",
|
||||
"api-key": "Clé API",
|
||||
"api-key-description-create": "La clé API de votre fournisseur pour l'authentification. Si votre service (par exemple Ollama) n'utilise pas une clé API, vous devez malgré tout toujours mettre quelque chose ici.",
|
||||
"api-key-description-edit": "Laissez ce champ vide à moins que vous vouliez le modifier.",
|
||||
"base-url": "URL de base",
|
||||
"base-url-description": "Si vous utilisez OpenAI laissez ce champ vide. Doit être un point de terminaison compatible OpenAI (par exemple \"http://localhost:11434/v1\").",
|
||||
"model": "Modèle",
|
||||
"model-description": "Quel modèle doit utiliser votre fournisseur d'IA (par exemple \"gpt-5\").",
|
||||
"request-timeout-seconds": "Délai d'attente de la requête (secondes)",
|
||||
"provider-created": "Fournisseur créé",
|
||||
"provider-updated": "Fournisseur mis à jour",
|
||||
"provider-deleted": "Fournisseur supprimé",
|
||||
"provider-create-failed": "Échec de la création du fournisseur",
|
||||
"provider-update-failed": "Échec de la mise à jour du fournisseur",
|
||||
"provider-delete-failed": "Échec de la suppression du fournisseur",
|
||||
"request-headers": "En-têtes de la requête",
|
||||
"request-params": "Paramètres de la requête",
|
||||
"no-default-provider-warning": "Vous n'avez pas défini de fournisseur par défaut, donc les fonctionnalités IA sont désactivées"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Foyer",
|
||||
@@ -390,7 +427,7 @@
|
||||
"nextcloud": {
|
||||
"description": "Importer des recettes depuis Nextcloud Cookbook",
|
||||
"description-long": "Les recettes Nextcloud peuvent être importées depuis un fichier zip qui contient les données stockées dans Nextcloud. Consultez la structure de dossiers d'exemple ci-dessous pour vous assurer que vos recettes peuvent être importées.",
|
||||
"title": "Nextcloud Cookbook"
|
||||
"title": "Livre de recettes Nextcloud (Cookbook)"
|
||||
},
|
||||
"copymethat": {
|
||||
"description-long": "Mealie peut importer des recettes à partir de Copy Me That. Exportez vos recettes au format HTML, puis téléchargez le .zip ci-dessous.",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Créer une recette depuis une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
@@ -915,8 +952,9 @@
|
||||
"quantity": "Quantité : {0}",
|
||||
"shopping-list": "Liste de courses",
|
||||
"shopping-lists": "Listes de courses",
|
||||
"add-item": "Ajouter un article",
|
||||
"food": "Aliment",
|
||||
"note": "Note",
|
||||
"note": "Remarque",
|
||||
"label": "Étiquette",
|
||||
"save-label": "Sauvegarder le libellé",
|
||||
"linked-item-warning": "Cet article est lié à une ou plusieurs recettes. Ajuster les unités ou les aliments donnera des résultats inattendus lors de l’ajout ou de la suppression de la recette de cette liste.",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Voulez-vous vraiment sélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Voulez-vous vraiment désélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Voulez-vous vraiment supprimer tous les éléments sélectionnés ?",
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée"
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée",
|
||||
"item-checked-off": "{item} coché"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Recettes",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Attention : cet aliment est utilisé dans {count} recette(s). Le supprimer laissera un ingrédient vide dans la ou les recette(s).",
|
||||
"delete-affects-recipes-more": "Visualiser les {count} recettes",
|
||||
"merge-dialog-text": "La combinaison des aliments sélectionnés fusionnera l’aliment source et l’aliment cible en un seul aliment. L’aliment source sera supprimé et toutes les références à l’aliment source seront mises à jour pour pointer vers l’aliment cible.",
|
||||
"merge-food-example": "Fusion de {food1} dans {food2}",
|
||||
"seed-dialog-text": "Initialisez la base de données avec des aliments basés sur votre langue locale. Cela permettra de créer plus de 200 aliments communs qui pourront être utilisés pour organiser votre base de données. Les aliments sont traduits grâce à un effort communautaire.",
|
||||
@@ -1347,7 +1388,7 @@
|
||||
"ingredient-text": "Texte de l'ingrédient",
|
||||
"average-confident": "Confiant à {0}",
|
||||
"try-an-example": "Essayez avec un exemple",
|
||||
"parser": "Parser",
|
||||
"parser": "Analyseur syntaxique",
|
||||
"background-tasks": "Tâches en arrière-plan",
|
||||
"background-tasks-description": "Ici vous pouvez voir toutes les tâches en arrière-plan en cours et leur statut",
|
||||
"no-logs-found": "Pas de journaux trouvés",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "J’ai déjà tout configuré, amenez-moi à l’écran d’accueil",
|
||||
"common-settings-for-new-sites": "Voici quelques paramètres courants pour les nouveaux sites",
|
||||
"setup-complete": "Configuration terminée !",
|
||||
"ai-providers-description": "Vous pouvez configurer un fournisseur d'IA pour votre groupe. Les fournisseurs d'IA permettent d'activer des fonctionnalités telles que la création de recettes depuis des images, l'importation de recettes depuis des vidéos ainsi qu'un traitement amélioré des ingrédients. Vous pourrez toujours configurer cela plus tard dans les paramètres de votre groupe.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Voici quelques trucs pour vous aider à commencer avec Mealie",
|
||||
"restore-from-v1-backup": "Vous avez une sauvegarde d’une précédente instance de Mealie v1 ? Vous pouvez la restaurer ici.",
|
||||
"manage-profile-or-get-invite-link": "Gérez votre propre profil, ou récupérez un lien d’invitation à partager avec d’autres."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "Aucun espace n'est autorisé",
|
||||
"min-length": "Doit contenir au moins {min} caractères",
|
||||
"max-length": "Doit comporter au maximum {max} caractère|Doit comporter au maximum {max} caractères"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Annonces",
|
||||
"all-announcements": "Toutes les annonces",
|
||||
"mark-all-as-read": "Marquer tout comme lu",
|
||||
"show-announcements-from-mealie": "Afficher les annonces de Mealie",
|
||||
"show-announcements-setting-description": "Si vous voulez autoriser ou non les utilisateurs à voir les annonces de Mealie. Lorsque cette option est activée, les utilisateurs peuvent toujours refuser de les voir dans leurs paramètres utilisateur"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "Panel",
|
||||
"delete": "Eliminar",
|
||||
"disabled": "Desactivado",
|
||||
"done": "Done",
|
||||
"download": "Descargar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "Identificador",
|
||||
"tuesday": "Martes",
|
||||
"type": "Tipo",
|
||||
"undo": "Undo",
|
||||
"update": "Actualizar",
|
||||
"updated": "Actualizado",
|
||||
"upload": "Subir",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "Mostrar Avanzadas",
|
||||
"add-field": "Adicionar Campo",
|
||||
"date-created": "Data de Creación",
|
||||
"date-updated": "Data de Atualización"
|
||||
"date-updated": "Data de Atualización",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Estás seguro de que queres eliminar <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "Os cambios neste grupo reflectiranse inmediatamente.",
|
||||
"group-id-value": "Id do grupo: {0}",
|
||||
"total-households": "Casas Totais",
|
||||
"you-must-select-a-group-before-selecting-a-household": "Tes que selecionar un grupo antes de selecionar unha casa"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Tes que selecionar un grupo antes de selecionar unha casa",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "Casa",
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "Crear unha receita en branco.",
|
||||
"create-recipes": "Crear Receitas",
|
||||
"import-with-zip": "Importar con .zip",
|
||||
"create-recipe-from-an-image": "Crear receita a partir dunha imaxen",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Cree unha receita cargando unha imaxen da mesma. O Mealie tentará extrair o texto da imaxen utilizando IA e creará unha receita a partir da mesma.",
|
||||
"crop-and-rotate-the-image": "Recorte e vire a imaxen de modo a que só o texto sexa visível e na orientación correta.",
|
||||
"create-from-images": "Crear a partir de imaxens",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "Cantidade: {0}",
|
||||
"shopping-list": "Lista de Compras",
|
||||
"shopping-lists": "Listas de Compras",
|
||||
"add-item": "Add item",
|
||||
"food": "Comida",
|
||||
"note": "Nota",
|
||||
"label": "Rótulo",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "Ten a certeza de que pretende selecionar todos os itens?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Ten a certeza de que pretende desmarcar todos os itens?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Ten a certeza de que pretende eliminar todos os itens selecionados?",
|
||||
"no-shopping-lists-found": "Nengunha Lista de Compras Encontrada"
|
||||
"no-shopping-lists-found": "Nengunha Lista de Compras Encontrada",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Todas as Receitas",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combinar os alimentos selecionados xuntará o alimento de orixen e o alimento de destino nun único alimento. O alimento de orixen será eliminado, e todas as referencias a este, serán atualizadas para apuntar para o alimento de destino.",
|
||||
"merge-food-example": "A xuntar {food1} con {food2}",
|
||||
"seed-dialog-text": "Semente a base de datos con alimentos baseados no seu idioma local. Isto creará mais de 200 alimentos comuns que poden ser utilizados para organizar a sua base de datos. Os alimentos son traducidos através dun esforzo comunitario.",
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "Xa estou listo, levame para a páxina inicial",
|
||||
"common-settings-for-new-sites": "Aqui están algunhas configuracións comuns para sites novos",
|
||||
"setup-complete": "Configuración Concluída!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "Aqui están algunhas cousas para axudar a comezar co Mealie",
|
||||
"restore-from-v1-backup": "Ten unha copia de seguranza dunha instancia do Mealie v1? Pode restaurala aqui.",
|
||||
"manage-profile-or-get-invite-link": "Xestione o seu proprio perfil ou obteña unha ligazón de convite para compartir con outras persoas."
|
||||
@@ -1472,5 +1514,12 @@
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"min-length": "Must Be At Least {min} Characters",
|
||||
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"dashboard": "לוח הבקרה",
|
||||
"delete": "מחיקה",
|
||||
"disabled": "כבוי",
|
||||
"done": "Done",
|
||||
"download": "הורדה",
|
||||
"duplicate": "שכפול",
|
||||
"edit": "עריכה",
|
||||
@@ -168,6 +169,7 @@
|
||||
"token": "טוקן",
|
||||
"tuesday": "שלישי",
|
||||
"type": "סוג",
|
||||
"undo": "Undo",
|
||||
"update": "עדכון",
|
||||
"updated": "עודכן",
|
||||
"upload": "העלאה",
|
||||
@@ -221,7 +223,9 @@
|
||||
"show-advanced": "הצג הגדרות מתקדמות",
|
||||
"add-field": "הוסף שדה",
|
||||
"date-created": "תאריך יצירה",
|
||||
"date-updated": "תאריך עדכון"
|
||||
"date-updated": "תאריך עדכון",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "למחוק את <b>{groupName}<b/>?",
|
||||
@@ -281,7 +285,40 @@
|
||||
"admin-group-management-text": "שינויים לקבוצה זו ישתקפו מיידית.",
|
||||
"group-id-value": "מזהה קבוצה: {0}",
|
||||
"total-households": "סך כל משקי בית",
|
||||
"you-must-select-a-group-before-selecting-a-household": "חובה לבחור קבוצה לפני בחירת משק בית"
|
||||
"you-must-select-a-group-before-selecting-a-household": "חובה לבחור קבוצה לפני בחירת משק בית",
|
||||
"ai-provider-settings": {
|
||||
"ai-provider-settings": "AI Provider Settings",
|
||||
"ai-provider": "AI Provider",
|
||||
"ai-providers": "AI Providers",
|
||||
"ai-provider-settings-description": "Configure AI providers to enable AI-powered features, such as enhanced ingredient parsing, creating recipes from videos, and more!",
|
||||
"providers": "Providers",
|
||||
"create-provider": "Create Provider",
|
||||
"edit-provider": "Edit Provider",
|
||||
"default-provider": "Default Provider",
|
||||
"default-provider-description": "Required to enable AI features",
|
||||
"audio-provider": "Audio Provider",
|
||||
"audio-provider-description": "Enables audio transcription features, such as creating recipes from videos",
|
||||
"image-provider": "Image Provider",
|
||||
"image-provider-description": "Enables image recognition features, such as creating recipes from images",
|
||||
"provider-name": "Provider Name",
|
||||
"api-key": "API Key",
|
||||
"api-key-description-create": "Your provider's API key for authentication. If your service (e.g. Ollama) doesn't use an API key, you still have to put something here.",
|
||||
"api-key-description-edit": "Leave this blank unless you want to change it.",
|
||||
"base-url": "Base URL",
|
||||
"base-url-description": "If you're using OpenAI leave this blank. Must be an OpenAI-compatible endpoint (e.g. \"http://localhost:11434/v1\").",
|
||||
"model": "Model",
|
||||
"model-description": "Which model your AI provider should use (e.g. \"gpt-5\").",
|
||||
"request-timeout-seconds": "Request Timeout (seconds)",
|
||||
"provider-created": "Provider created",
|
||||
"provider-updated": "Provider updated",
|
||||
"provider-deleted": "Provider deleted",
|
||||
"provider-create-failed": "Failed to create provider",
|
||||
"provider-update-failed": "Failed to update provider",
|
||||
"provider-delete-failed": "Failed to delete provider",
|
||||
"request-headers": "Request Headers",
|
||||
"request-params": "Request Parameters",
|
||||
"no-default-provider-warning": "You have not set a default provider, so AI features are disabled"
|
||||
}
|
||||
},
|
||||
"household": {
|
||||
"household": "משק בית",
|
||||
@@ -424,7 +461,7 @@
|
||||
"paprika-text": "Mealie יכולה לייבא מתכונים מאפליקציית Paprika. יש לייצא את המתכונים מהאפליקציה, לשנות את סיומת הקובץ ל- zip ולהעלות אותו כאן.",
|
||||
"mealie-text": "Mealie יכול לייבא מתכונים מגרסאות ישנות של Mealie מתחת ל- v1.0. ייצא את המתכונים מהסביבה הישנה והעלה את קובץ ה- zip למטה. ניתן לייבא רק מתכונים מתהליך הייצוא.",
|
||||
"plantoeat": {
|
||||
"title": "Plan to Eat",
|
||||
"title": "בתכנון לאכול",
|
||||
"description-long": "Mealie יכולה לייבא מתכונים מהאתר- Plan to Eat."
|
||||
},
|
||||
"myrecipebox": {
|
||||
@@ -626,7 +663,7 @@
|
||||
"create-recipe-description": "יצירת מתכון חדש מאפס.",
|
||||
"create-recipes": "יצירת מתכונים",
|
||||
"import-with-zip": "ייבא באמצעות zip",
|
||||
"create-recipe-from-an-image": "יצירת מתכון מתמונה",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "יצירת מתכון ע\"י העלאת תמונה שלו. Mealie תנסה לחלץ את הטקסט מהתמונה באמצעות AI ותייצר ממנו מתכון.",
|
||||
"crop-and-rotate-the-image": "נא לחתוך ולסובב את התמונה כך שרואים רק את הטקסט, והוא בכיוון הנכון.",
|
||||
"create-from-images": "יצירה מתמונה",
|
||||
@@ -644,10 +681,10 @@
|
||||
"scrape-recipe-suggest-bulk-importer": "נסה את יכולת קריאת רשימה",
|
||||
"scrape-recipe-have-raw-html-or-json-data": "יש לך מידע גולמי ב-HTML או JSON?",
|
||||
"scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי",
|
||||
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||
"scrape-recipe-website-being-blocked": "האתר חסום?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||
"import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "ייבא קטגוריות מקוריות",
|
||||
"stay-in-edit-mode": "השאר במצב עריכה",
|
||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||
"import-from-zip": "ייבא מקובץ",
|
||||
@@ -693,15 +730,15 @@
|
||||
"no-unit": "ללא יחידות",
|
||||
"missing-unit": "יצירת יחידה חסרה: {unit}",
|
||||
"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",
|
||||
"this-unit-could-not-be-parsed-automatically": "יחידת המידה הזאת לא יכלה להיות מחולצת אוטומטית",
|
||||
"this-food-could-not-be-parsed-automatically": "המאכל הזה לא יכל להיות מחולץ אוטומטית",
|
||||
"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"
|
||||
"delete-item": "מחק פריט"
|
||||
},
|
||||
"reset-servings-count": "איפוס מספר המנות",
|
||||
"not-linked-ingredients": "מרכיבים נוספים",
|
||||
@@ -915,6 +952,7 @@
|
||||
"quantity": "כמות: {0}",
|
||||
"shopping-list": "רשימת קניות",
|
||||
"shopping-lists": "רשימות קניות",
|
||||
"add-item": "Add item",
|
||||
"food": "אוכל",
|
||||
"note": "הערה",
|
||||
"label": "תווית",
|
||||
@@ -939,7 +977,8 @@
|
||||
"are-you-sure-you-want-to-check-all-items": "לסמן את כל הפריטים?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "לבטל את סימון כל הפריטים?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "למחוק את כל הפריטים המסומנים?",
|
||||
"no-shopping-lists-found": "לא נמצאה רשימת קניות"
|
||||
"no-shopping-lists-found": "לא נמצאה רשימת קניות",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "כל המתכונים",
|
||||
@@ -1094,7 +1133,7 @@
|
||||
"forgot-password-text": "נא לספק כתובת דוא\"ל. אנו נשלח לך הודעת דוא\"ל לצורך איפוס הסיסמה שלך.",
|
||||
"changes-reflected-immediately": "שינויים למשתמש זה ישתקפו מיידית.",
|
||||
"default-activity": "Default Activity",
|
||||
"default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device"
|
||||
"default-activity-hint": "בחר את עמוד ברירת המחדל בשביל המכשיר הזה"
|
||||
},
|
||||
"language-dialog": {
|
||||
"translated": "תורגם",
|
||||
@@ -1105,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "צירוף של המאכלים שנבחרו יאחד את אוכל המקור ואוכל היעד לפריט אוכל אחד. אוכל המקור ימחק וכל ההפניות / הייחוסים אליו יעודכנו ויופנו לאוכל היעד.",
|
||||
"merge-food-example": "ממזג את {food1} לתוך {food2}",
|
||||
"seed-dialog-text": "אכלס את מסד הנתונים עם אוכל בהתבסס על השפה המקומית שלך. הפעולה תיצור +200 מאכלים נפוצים שיכולים לשמש לארגון מסד הנתונים. מאכלים מתורגמים על ידי מאמצי הקהילה.",
|
||||
@@ -1153,8 +1194,8 @@
|
||||
"ounce": "ounce",
|
||||
"pound": "pound",
|
||||
"milliliter": "מיליליטר",
|
||||
"liter": "liter",
|
||||
"gram": "gram",
|
||||
"liter": "ליטר",
|
||||
"gram": "גרם",
|
||||
"kilogram": "kilogram"
|
||||
}
|
||||
},
|
||||
@@ -1192,7 +1233,7 @@
|
||||
"edit-recipe-action": "עריכת פעולת-מתכון",
|
||||
"action-type": "סוג פעולה",
|
||||
"action-types": {
|
||||
"link": "Link",
|
||||
"link": "לינק",
|
||||
"post": "Post"
|
||||
}
|
||||
},
|
||||
@@ -1358,6 +1399,7 @@
|
||||
"already-set-up-bring-to-homepage": "כבר הגדרתי הכל, תעבירו אותי לעמוד הבית",
|
||||
"common-settings-for-new-sites": "הגדרות נפוצות לאתרים חדשים יופיעו כאן",
|
||||
"setup-complete": "ההגדרה הושלמה!",
|
||||
"ai-providers-description": "Optionally configure AI providers for your group. AI providers enable features like creating recipes from images, importing recipes from videos, and enhanced ingredient parsing. You can always configure this later from your group settings.",
|
||||
"here-are-a-few-things-to-help-you-get-started": "כמה דברים שיעזרו לך להתחיל להשתמש ב-Mealie",
|
||||
"restore-from-v1-backup": "יש לך גיבוי משרת Mealie v1? ניתן לשחזר אותו כאן.",
|
||||
"manage-profile-or-get-invite-link": "ניתן לנהל את הפרופיל שלך, או לשתף את לינק ההזמנה לאחרים."
|
||||
@@ -1466,11 +1508,18 @@
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
"required": "This Field is Required",
|
||||
"required": "חובה למלא שדה זה",
|
||||
"invalid-email": "Email Must Be Valid",
|
||||
"invalid-url": "Must Be A Valid URL",
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"min-length": "Must Be At Least {min} Characters",
|
||||
"max-length": "Must Be At Most {max} Character|Must Be At Most {max} Characters"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user