diff --git a/docker/entry.sh b/docker/entry.sh index cccc2ba9d..91217e864 100644 --- a/docker/entry.sh +++ b/docker/entry.sh @@ -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. diff --git a/docs/docs/documentation/community-guide/ios-shortcut.md b/docs/docs/documentation/community-guide/ios-shortcut.md index 62e52063f..3f963ce6d 100644 --- a/docs/docs/documentation/community-guide/ios-shortcut.md +++ b/docs/docs/documentation/community-guide/ios-shortcut.md @@ -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. \ No newline at end of file + 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. diff --git a/docs/docs/documentation/getting-started/faq.md b/docs/docs/documentation/getting-started/faq.md index 6f6728b90..c03bcb367 100644 --- a/docs/docs/documentation/getting-started/faq.md +++ b/docs/docs/documentation/getting-started/faq.md @@ -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. diff --git a/docs/docs/documentation/getting-started/features.md b/docs/docs/documentation/getting-started/features.md index 57beb87c0..a25db8f82 100644 --- a/docs/docs/documentation/getting-started/features.md +++ b/docs/docs/documentation/getting-started/features.md @@ -11,7 +11,7 @@ Mealie offers several ways to create recipes: - **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/open-ai.md) greatly expands the ways you can create recipes: +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. diff --git a/docs/docs/documentation/getting-started/installation/ai-providers.md b/docs/docs/documentation/getting-started/installation/ai-providers.md new file mode 100644 index 000000000..e3aebf047 --- /dev/null +++ b/docs/docs/documentation/getting-started/installation/ai-providers.md @@ -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/ai-providers.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) diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md index 4e7ef368c..27dae3885 100644 --- a/docs/docs/documentation/getting-started/installation/backend-config.md +++ b/docs/docs/documentation/getting-started/installation/backend-config.md @@ -120,22 +120,10 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md) :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[†][secrets] | 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[†][secrets] | 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
: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
: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
: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
: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
: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
: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 @@ -315,7 +303,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 diff --git a/docs/docs/documentation/getting-started/installation/installation-checklist.md b/docs/docs/documentation/getting-started/installation/installation-checklist.md index 4153f705f..86d89e60b 100644 --- a/docs/docs/documentation/getting-started/installation/installation-checklist.md +++ b/docs/docs/documentation/getting-started/installation/installation-checklist.md @@ -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.17.0` +2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.18.0` 3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access. 4. Restart the container diff --git a/docs/docs/documentation/getting-started/installation/open-ai.md b/docs/docs/documentation/getting-started/installation/open-ai.md deleted file mode 100644 index bbaae51d7..000000000 --- a/docs/docs/documentation/getting-started/installation/open-ai.md +++ /dev/null @@ -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) diff --git a/docs/docs/documentation/getting-started/installation/postgres.md b/docs/docs/documentation/getting-started/installation/postgres.md index 75cb06eab..e57fdbdbb 100644 --- a/docs/docs/documentation/getting-started/installation/postgres.md +++ b/docs/docs/documentation/getting-started/installation/postgres.md @@ -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.17.0 # (3) + image: ghcr.io/mealie-recipes/mealie:v3.18.0 # (3) container_name: mealie restart: always ports: diff --git a/docs/docs/documentation/getting-started/installation/sqlite.md b/docs/docs/documentation/getting-started/installation/sqlite.md index c244e0ca4..653ff7755 100644 --- a/docs/docs/documentation/getting-started/installation/sqlite.md +++ b/docs/docs/documentation/getting-started/installation/sqlite.md @@ -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.17.0 # (3) + image: ghcr.io/mealie-recipes/mealie:v3.18.0 # (3) container_name: mealie restart: always ports: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 52ca05cc0..16042494f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -76,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" diff --git a/frontend/app/components/Domain/Announcement/Announcements/2026-05-21_1_ai-providers.vue b/frontend/app/components/Domain/Announcement/Announcements/2026-05-21_1_ai-providers.vue new file mode 100644 index 000000000..c0d746eec --- /dev/null +++ b/frontend/app/components/Domain/Announcement/Announcements/2026-05-21_1_ai-providers.vue @@ -0,0 +1,63 @@ + + + + + + + diff --git a/frontend/app/components/Domain/Group/GroupAIProviderDialog.vue b/frontend/app/components/Domain/Group/GroupAIProviderDialog.vue new file mode 100644 index 000000000..53797dd18 --- /dev/null +++ b/frontend/app/components/Domain/Group/GroupAIProviderDialog.vue @@ -0,0 +1,204 @@ + + + diff --git a/frontend/app/components/Domain/Group/GroupAIProviderSettingsEditor.vue b/frontend/app/components/Domain/Group/GroupAIProviderSettingsEditor.vue new file mode 100644 index 000000000..1b679aa6c --- /dev/null +++ b/frontend/app/components/Domain/Group/GroupAIProviderSettingsEditor.vue @@ -0,0 +1,170 @@ + + + diff --git a/frontend/app/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageParseDialog.vue b/frontend/app/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageParseDialog.vue index b5cf74dfa..20fe48c02 100644 --- a/frontend/app/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageParseDialog.vue +++ b/frontend/app/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageParseDialog.vue @@ -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): 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, }, ]; }); diff --git a/frontend/app/components/Domain/User/UserProfileLinkCard.vue b/frontend/app/components/Domain/User/UserProfileLinkCard.vue index 01d97529e..ffdbe5851 100644 --- a/frontend/app/components/Domain/User/UserProfileLinkCard.vue +++ b/frontend/app/components/Domain/User/UserProfileLinkCard.vue @@ -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" >
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 || ""); @@ -131,7 +133,7 @@ const cookbooks = computed(() => { return []; }); -const showImageImport = computed(() => $appInfo.enableOpenaiImageServices); +const showImageImport = computed(() => group.value?.aiProviderSettings?.imageProviderEnabled); const sidebar = ref(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, diff --git a/frontend/app/components/global/BaseCardSectionTitle.vue b/frontend/app/components/global/BaseCardSectionTitle.vue index 520922ed5..4743e561d 100644 --- a/frontend/app/components/global/BaseCardSectionTitle.vue +++ b/frontend/app/components/global/BaseCardSectionTitle.vue @@ -7,7 +7,8 @@ 'mt-8': section, }" > - + + {{ title }} + diff --git a/frontend/app/composables/use-ai-providers.ts b/frontend/app/composables/use-ai-providers.ts new file mode 100644 index 000000000..4eaabc896 --- /dev/null +++ b/frontend/app/composables/use-ai-providers.ts @@ -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, + }; +} diff --git a/frontend/app/composables/use-announcements.ts b/frontend/app/composables/use-announcements.ts index 92faa219f..696e1b127 100644 --- a/frontend/app/composables/use-announcements.ts +++ b/frontend/app/composables/use-announcements.ts @@ -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 { diff --git a/frontend/app/composables/use-groups.ts b/frontend/app/composables/use-groups.ts index 3904abcb8..9b9326255 100644 --- a/frontend/app/composables/use-groups.ts +++ b/frontend/app/composables/use-groups.ts @@ -42,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(); diff --git a/frontend/app/composables/use-locales/available-locales.ts b/frontend/app/composables/use-locales/available-locales.ts index c3b5cedac..226e39aed 100644 --- a/frontend/app/composables/use-locales/available-locales.ts +++ b/frontend/app/composables/use-locales/available-locales.ts @@ -3,7 +3,7 @@ export const LOCALES = [ { name: "繁體中文 (Chinese traditional)", value: "zh-TW", - progress: 98, + progress: 97, dir: "ltr", pluralFoodHandling: "never", }, @@ -24,21 +24,21 @@ export const LOCALES = [ { name: "Українська (Ukrainian)", value: "uk-UA", - progress: 86, + progress: 85, dir: "ltr", pluralFoodHandling: "always", }, { name: "Türkçe (Turkish)", value: "tr-TR", - progress: 54, + progress: 53, dir: "ltr", pluralFoodHandling: "never", }, { name: "Svenska (Swedish)", value: "sv-SE", - progress: 74, + progress: 75, dir: "ltr", pluralFoodHandling: "always", }, @@ -52,42 +52,42 @@ export const LOCALES = [ { name: "Slovenščina (Slovenian)", value: "sl-SI", - progress: 57, + progress: 56, dir: "ltr", pluralFoodHandling: "always", }, { name: "Slovenčina (Slovak)", value: "sk-SK", - progress: 61, + progress: 60, dir: "ltr", pluralFoodHandling: "always", }, { name: "Pусский (Russian)", value: "ru-RU", - progress: 59, + progress: 58, dir: "ltr", pluralFoodHandling: "always", }, { name: "Română (Romanian)", value: "ro-RO", - progress: 60, + progress: 59, dir: "ltr", pluralFoodHandling: "always", }, { name: "Português (Portuguese)", value: "pt-PT", - progress: 57, + progress: 56, dir: "ltr", pluralFoodHandling: "always", }, { name: "Português do Brasil (Brazilian Portuguese)", value: "pt-BR", - progress: 99, + progress: 98, dir: "ltr", pluralFoodHandling: "always", }, @@ -129,7 +129,7 @@ export const LOCALES = [ { name: "한국어 (Korean)", value: "ko-KR", - progress: 55, + progress: 54, dir: "ltr", pluralFoodHandling: "never", }, @@ -143,35 +143,35 @@ export const LOCALES = [ { name: "Italiano (Italian)", value: "it-IT", - progress: 73, + progress: 72, dir: "ltr", pluralFoodHandling: "always", }, { name: "Íslenska (Icelandic)", value: "is-IS", - progress: 57, + progress: 56, dir: "ltr", pluralFoodHandling: "always", }, { name: "Magyar (Hungarian)", value: "hu-HU", - progress: 61, + progress: 62, dir: "ltr", pluralFoodHandling: "always", }, { name: "Hrvatski (Croatian)", value: "hr-HR", - progress: 42, + progress: 41, dir: "ltr", pluralFoodHandling: "always", }, { name: "עברית (Hebrew)", value: "he-IL", - progress: 73, + progress: 72, dir: "rtl", pluralFoodHandling: "always", }, @@ -192,7 +192,7 @@ export const LOCALES = [ { name: "Français canadien (Canadian French)", value: "fr-CA", - progress: 90, + progress: 89, dir: "ltr", pluralFoodHandling: "always", }, @@ -206,7 +206,7 @@ export const LOCALES = [ { name: "Suomi (Finnish)", value: "fi-FI", - progress: 99, + progress: 98, dir: "ltr", pluralFoodHandling: "always", }, @@ -241,21 +241,21 @@ export const LOCALES = [ { name: "Ελληνικά (Greek)", value: "el-GR", - progress: 57, + progress: 58, dir: "ltr", pluralFoodHandling: "always", }, { name: "Deutsch (German)", value: "de-DE", - progress: 99, + progress: 98, dir: "ltr", pluralFoodHandling: "always", }, { name: "Dansk (Danish)", value: "da-DK", - progress: 100, + progress: 99, dir: "ltr", pluralFoodHandling: "always", }, @@ -269,7 +269,7 @@ export const LOCALES = [ { name: "Català (Catalan)", value: "ca-ES", - progress: 60, + progress: 59, dir: "ltr", pluralFoodHandling: "always", }, @@ -283,14 +283,14 @@ export const LOCALES = [ { name: "العربية (Arabic)", value: "ar-SA", - progress: 98, + progress: 97, dir: "rtl", pluralFoodHandling: "always", }, { name: "Afrikaans (Afrikaans)", value: "af-ZA", - progress: 37, + progress: 36, dir: "ltr", pluralFoodHandling: "always", }, diff --git a/frontend/app/lang/messages/af-ZA.json b/frontend/app/lang/messages/af-ZA.json index 7bc32094e..f2427068f 100644 --- a/frontend/app/lang/messages/af-ZA.json +++ b/frontend/app/lang/messages/af-ZA.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/ar-SA.json b/frontend/app/lang/messages/ar-SA.json index f93a68baf..f34929477 100644 --- a/frontend/app/lang/messages/ar-SA.json +++ b/frontend/app/lang/messages/ar-SA.json @@ -628,7 +628,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": "إنشاء عن طريق صور", diff --git a/frontend/app/lang/messages/bg-BG.json b/frontend/app/lang/messages/bg-BG.json index 64a3cfdb9..84e004be6 100644 --- a/frontend/app/lang/messages/bg-BG.json +++ b/frontend/app/lang/messages/bg-BG.json @@ -628,7 +628,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 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": "Създаване от изображения", diff --git a/frontend/app/lang/messages/ca-ES.json b/frontend/app/lang/messages/ca-ES.json index cb700daa2..230db387d 100644 --- a/frontend/app/lang/messages/ca-ES.json +++ b/frontend/app/lang/messages/ca-ES.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/cs-CZ.json b/frontend/app/lang/messages/cs-CZ.json index f6037af47..5ea11f6fa 100644 --- a/frontend/app/lang/messages/cs-CZ.json +++ b/frontend/app/lang/messages/cs-CZ.json @@ -628,7 +628,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": "Create Recipe from Images", "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ů", diff --git a/frontend/app/lang/messages/da-DK.json b/frontend/app/lang/messages/da-DK.json index b9866c762..65e5634d0 100644 --- a/frontend/app/lang/messages/da-DK.json +++ b/frontend/app/lang/messages/da-DK.json @@ -628,7 +628,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": "Create Recipe from Images", "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", diff --git a/frontend/app/lang/messages/de-DE.json b/frontend/app/lang/messages/de-DE.json index b4fbfd1b0..abd3afda6 100644 --- a/frontend/app/lang/messages/de-DE.json +++ b/frontend/app/lang/messages/de-DE.json @@ -169,7 +169,7 @@ "token": "Token", "tuesday": "Dienstag", "type": "Typ", - "undo": "Undo", + "undo": "Rückgängig", "update": "Aktualisieren", "updated": "Aktualisiert", "upload": "Hochladen", @@ -628,7 +628,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": "Create Recipe from Images", "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", @@ -917,7 +917,7 @@ "quantity": "Menge: {0}", "shopping-list": "Einkaufsliste", "shopping-lists": "Einkaufslisten", - "add-item": "Add item", + "add-item": "Eintrag hinzufügen", "food": "Lebensmittel", "note": "Notiz", "label": "Kategorie", @@ -1478,10 +1478,10 @@ "max-length": "Muss maximal {max} Zeichen haben|Muss maximal {max} Zeichen haben" }, "announcements": { - "announcements": "Announcements", - "all-announcements": "All announcements", + "announcements": "Ankündigungen", + "all-announcements": "Alle Ankündigungen", "mark-all-as-read": "Alle als gelesen markieren", - "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" + "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" } } diff --git a/frontend/app/lang/messages/el-GR.json b/frontend/app/lang/messages/el-GR.json index c485f488e..59c399e7d 100644 --- a/frontend/app/lang/messages/el-GR.json +++ b/frontend/app/lang/messages/el-GR.json @@ -628,7 +628,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": "Δημιουργία από εικόνες", diff --git a/frontend/app/lang/messages/en-GB.json b/frontend/app/lang/messages/en-GB.json index 5e958408f..6d5c00df8 100644 --- a/frontend/app/lang/messages/en-GB.json +++ b/frontend/app/lang/messages/en-GB.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/en-US.json b/frontend/app/lang/messages/en-US.json index 80707e356..f120dc8fd 100644 --- a/frontend/app/lang/messages/en-US.json +++ b/frontend/app/lang/messages/en-US.json @@ -223,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 {groupName}?", @@ -283,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", @@ -628,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", @@ -1362,6 +1397,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." diff --git a/frontend/app/lang/messages/es-ES.json b/frontend/app/lang/messages/es-ES.json index 8b82e5dee..5d9df76c8 100644 --- a/frontend/app/lang/messages/es-ES.json +++ b/frontend/app/lang/messages/es-ES.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/et-EE.json b/frontend/app/lang/messages/et-EE.json index 1d4162baf..a0ab3120e 100644 --- a/frontend/app/lang/messages/et-EE.json +++ b/frontend/app/lang/messages/et-EE.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/fi-FI.json b/frontend/app/lang/messages/fi-FI.json index e70e21540..e5bf93e4c 100644 --- a/frontend/app/lang/messages/fi-FI.json +++ b/frontend/app/lang/messages/fi-FI.json @@ -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,7 +98,7 @@ "dashboard": "Hallintanäkymä", "delete": "Poista", "disabled": "Poistettu käytöstä", - "done": "Done", + "done": "Valmis", "download": "Lataa", "duplicate": "Monista", "edit": "Muokkaa", @@ -169,7 +169,7 @@ "token": "Tunniste", "tuesday": "Tiistai", "type": "Tyyppi", - "undo": "Undo", + "undo": "Peru", "update": "Päivitä", "updated": "Päivitetty", "upload": "Lähetä", @@ -333,8 +333,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", @@ -392,7 +392,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.", @@ -628,7 +628,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", @@ -702,7 +702,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}", + "add-text-as-alias-for-item": "Lisää \"{text}\" kohteen {item} aliakseksi", "delete-item": "Poista kohde" }, "reset-servings-count": "Palauta Annoksien Määrä", @@ -893,17 +893,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." }, @@ -917,7 +917,7 @@ "quantity": "Määrä: {0}", "shopping-list": "Ostoslista", "shopping-lists": "Ostoslistat", - "add-item": "Add item", + "add-item": "Lisää kohde", "food": "Elintarvikkeet", "note": "Muistiinpano", "label": "Tunnus", @@ -962,7 +962,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" @@ -1351,7 +1351,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", @@ -1481,7 +1481,7 @@ "announcements": "Announcements", "all-announcements": "All announcements", "mark-all-as-read": "Mark All as Read", - "show-announcements-from-mealie": "Show announcements from Mealie", + "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" } } diff --git a/frontend/app/lang/messages/fr-BE.json b/frontend/app/lang/messages/fr-BE.json index 414b780fa..88476cbc7 100644 --- a/frontend/app/lang/messages/fr-BE.json +++ b/frontend/app/lang/messages/fr-BE.json @@ -628,7 +628,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": "Create Recipe from Images", "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", diff --git a/frontend/app/lang/messages/fr-CA.json b/frontend/app/lang/messages/fr-CA.json index 67e99a45a..2992e4c36 100644 --- a/frontend/app/lang/messages/fr-CA.json +++ b/frontend/app/lang/messages/fr-CA.json @@ -628,7 +628,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": "Create Recipe from Images", "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", diff --git a/frontend/app/lang/messages/fr-FR.json b/frontend/app/lang/messages/fr-FR.json index 39cdfdd17..8a6c68323 100644 --- a/frontend/app/lang/messages/fr-FR.json +++ b/frontend/app/lang/messages/fr-FR.json @@ -169,7 +169,7 @@ "token": "Jeton", "tuesday": "Mardi", "type": "Type", - "undo": "Undo", + "undo": "Annuler", "update": "Mettre à jour", "updated": "Mis à jour", "upload": "Importer", @@ -628,7 +628,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", @@ -943,7 +943,7 @@ "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", - "item-checked-off": "Checked off {item}" + "item-checked-off": "{item} coché" }, "sidebar": { "all-recipes": "Recettes", diff --git a/frontend/app/lang/messages/gl-ES.json b/frontend/app/lang/messages/gl-ES.json index 4e716ee37..9046c8adb 100644 --- a/frontend/app/lang/messages/gl-ES.json +++ b/frontend/app/lang/messages/gl-ES.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/he-IL.json b/frontend/app/lang/messages/he-IL.json index 6813e63ff..3b8d1264a 100644 --- a/frontend/app/lang/messages/he-IL.json +++ b/frontend/app/lang/messages/he-IL.json @@ -628,7 +628,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": "יצירה מתמונה", diff --git a/frontend/app/lang/messages/hr-HR.json b/frontend/app/lang/messages/hr-HR.json index 12bfc4c40..6016a89d4 100644 --- a/frontend/app/lang/messages/hr-HR.json +++ b/frontend/app/lang/messages/hr-HR.json @@ -628,7 +628,7 @@ "create-recipe-description": "Izradi novi recept od početka", "create-recipes": "Kreiraj recept", "import-with-zip": "Učitaj pomoću .zip-a", - "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": "Obreži i rotiraj sliku tako da bude vidljiv samo tekst i da bude u ispravnoj orijentaciji.", "create-from-images": "Izradi na temelju fotografije", diff --git a/frontend/app/lang/messages/hu-HU.json b/frontend/app/lang/messages/hu-HU.json index 9713d8341..60d30903b 100644 --- a/frontend/app/lang/messages/hu-HU.json +++ b/frontend/app/lang/messages/hu-HU.json @@ -628,7 +628,7 @@ "create-recipe-description": "Adj hozzá egy új receptet a nulláról kezdve.", "create-recipes": "Receptek létrehozása", "import-with-zip": "Importálás .zip formátummal", - "create-recipe-from-an-image": "Recept készítése képről", + "create-recipe-from-images": "Recept létrehozása képek alapján", "create-recipe-from-an-image-description": "Hozzon létre egy receptet egy kép feltöltésével. A Mealie megpróbálja a kép szövegét mesterséges intelligencia segítségével kinyerni, és létrehozni belőle a receptet.", "crop-and-rotate-the-image": "Vágja ki és forgassa el a képet úgy, hogy csak a szöveg legyen látható, és megfelelő tájolásban legyen.", "create-from-images": "Létrehozás képekről", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Biztos, hogy minden elem kijelölését visszavonja?", "are-you-sure-you-want-to-delete-checked-items": "Biztosan törölni akarja az összes bejelölt elemet?", "no-shopping-lists-found": "Nem találhatók bevásárlólisták", - "item-checked-off": "Checked off {item}" + "item-checked-off": "{item} leellenőrzve" }, "sidebar": { "all-recipes": "Minden recept", diff --git a/frontend/app/lang/messages/is-IS.json b/frontend/app/lang/messages/is-IS.json index e29758d75..1e2f61a9d 100644 --- a/frontend/app/lang/messages/is-IS.json +++ b/frontend/app/lang/messages/is-IS.json @@ -333,8 +333,8 @@ "any-household": "Öll heimili", "no-meal-plan-defined-yet": "Ekkert matarplan hefur verið skilgreint", "no-meal-planned-for-today": "Ekkert matarplan skipulagt í dag", - "numberOfDaysPast-hint": "Number of days in the past on page load", - "numberOfDaysPast-label": "Default Days in the Past", + "numberOfDaysPast-hint": "Fjöldi liðina daga við síðuhleðslu", + "numberOfDaysPast-label": "Sjálfgefnir liðnir dagar", "numberOfDays-hint": "Fjöldi daga við síðuhleðslu", "numberOfDays-label": "Sjálfgefnir dagar", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Aðeins uppskriftir í þessum flokkum verða notaðir í matarplan", @@ -628,7 +628,7 @@ "create-recipe-description": "Stofna nýja uppskrift frá grunni.", "create-recipes": "Stofna uppskriftir", "import-with-zip": "Hlaða inn með .zip", - "create-recipe-from-an-image": "Stofna uppskrift út frá mynd", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Stofna uppskrift með því hlaða inn myndum af uppskriftartextanum. Mealie mun reyna að vinna texta úr myndunum með gervigreind og stofna nýja uppskrift út frá textanum.", "crop-and-rotate-the-image": "Sníða og snúa mynd svo bara textinn sé sýnilegur og að myndin snúi rétt.", "create-from-images": "Stofna uppskrift frá mynd", @@ -640,8 +640,8 @@ "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Stofnaðu uppskrift með því að gefa henni nafn, allar uppskriftir þurfa að hafa einstakt nafn.", "new-recipe-names-must-be-unique": "Nöfn uppskrifta þurfa að vera einstök", "scrape-recipe": "Vinna uppskrift", - "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": "Sækja uppskrift af vefslóð. Settu inn vefslóð fyrir síðuna þar sem þú vilt sækja uppskrift og Mealie mun reyna að vinna uppskriftina þaðan og bæta henni við safnið þitt.", + "scrape-recipe-description-transcription": "Þú getur einnig sett inn slóð á video og Mealie mun reyna að umrita það yfir í uppskrift.", "scrape-recipe-have-a-lot-of-recipes": "Ertu með margar uppskriftir sem þú villt setja inn í einu?", "scrape-recipe-suggest-bulk-importer": "Prófaðu að setja inn margar uppskriftir í einu", "scrape-recipe-have-raw-html-or-json-data": "Ertu með hrá HTML eða JSON gögn?", @@ -893,17 +893,17 @@ "server-side-base-url-error-text": "'BASE_URL' er enn sjálfgefið gildi á API netþjóns. Þetta getur valdið vandræðum með tilkynninga tengla sem netþjónninn býr til fyrir tölvupósta og annað.", "server-side-base-url-success-text": "Slóð netþjóns samsvarar ekki sjálfgefnu gildi", "ldap-ready": "LDAP klár", - "ldap-not-ready": "LDAP Not Ready", + "ldap-not-ready": "LDAP er ekki tilbúið", "ldap-ready-error-text": "Ekki öll LDAP-gildi eru stillt. Þetta má hunsa ef þú notar ekki LDAP-auðkenningu.", "ldap-ready-success-text": "Öll nauðsynleg LDAP-gildi eru stillt.", "build": "Build", "recipe-scraper-version": "Recipe Scraper útgáfa", "oidc-ready": "OIDC klár", - "oidc-not-ready": "OIDC Not Ready", + "oidc-not-ready": "OIDC er ekki tilbúið", "oidc-ready-error-text": "Ekki öll OIDC gildi eru stillt. Þetta má hunsa ef þú notar ekki OIDC-auðkenningu.", "oidc-ready-success-text": "Öll nauðsynleg OIDC-gildi eru stillt.", "openai-ready": "OpenAI klár", - "openai-not-ready": "OpenAI Not Ready", + "openai-not-ready": "OpenAI er ekki tilbúið", "openai-ready-error-text": "Ekki öll OpenAI gildi eru stillt. Þetta má hunsa ef þú notar ekki OpenAI.", "openai-ready-success-text": "Öll nauðsynleg OpenAI-gildi eru stillt." }, @@ -917,7 +917,7 @@ "quantity": "Fjöldi: {0}", "shopping-list": "Innkaupalisti", "shopping-lists": "Innkaupalistar", - "add-item": "Add item", + "add-item": "Bæta við vöru", "food": "Matvara", "note": "Minnispunktur", "label": "Merkimiði", diff --git a/frontend/app/lang/messages/it-IT.json b/frontend/app/lang/messages/it-IT.json index 54605da15..d4e84b20c 100644 --- a/frontend/app/lang/messages/it-IT.json +++ b/frontend/app/lang/messages/it-IT.json @@ -628,7 +628,7 @@ "create-recipe-description": "Crea una nuova ricetta da zero.", "create-recipes": "Crea Ricette", "import-with-zip": "Importa da .zip", - "create-recipe-from-an-image": "Crea ricetta da un'immagine", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Crea una ricetta caricando un'immagine di essa. Mealie tenterà di estrarre il testo dall'immagine usando l'IA e creare una ricetta da esso.", "crop-and-rotate-the-image": "Ritaglia e ruota l'immagine in modo che solo il testo sia visibile e che sia orientato correttamente.", "create-from-images": "Crea da immagini", diff --git a/frontend/app/lang/messages/ja-JP.json b/frontend/app/lang/messages/ja-JP.json index 1044e7c8e..2777b9dc9 100644 --- a/frontend/app/lang/messages/ja-JP.json +++ b/frontend/app/lang/messages/ja-JP.json @@ -628,7 +628,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": "Create from Images", diff --git a/frontend/app/lang/messages/ko-KR.json b/frontend/app/lang/messages/ko-KR.json index 946c03f9e..533aa17c6 100644 --- a/frontend/app/lang/messages/ko-KR.json +++ b/frontend/app/lang/messages/ko-KR.json @@ -628,7 +628,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": "이미지에서 생성", diff --git a/frontend/app/lang/messages/lt-LT.json b/frontend/app/lang/messages/lt-LT.json index 41cd0b886..9da91e3f6 100644 --- a/frontend/app/lang/messages/lt-LT.json +++ b/frontend/app/lang/messages/lt-LT.json @@ -628,7 +628,7 @@ "create-recipe-description": "Create a new recipe from scratch.", "create-recipes": "Create Recipes", "import-with-zip": "Įkelti naudojant .zip failus", - "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": "Kurti iš vaizdų", diff --git a/frontend/app/lang/messages/lv-LV.json b/frontend/app/lang/messages/lv-LV.json index ca1ad5a7b..7c515dc7f 100644 --- a/frontend/app/lang/messages/lv-LV.json +++ b/frontend/app/lang/messages/lv-LV.json @@ -628,7 +628,7 @@ "create-recipe-description": "Izveidojiet jaunu recepti no nulles.", "create-recipes": "Izveidojiet receptes", "import-with-zip": "Importēt ar .zip", - "create-recipe-from-an-image": "Izveidojiet recepti no attēla", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Izveidojiet recepti, augšupielādējot tās attēlu. Mealie mēģinās iegūt tekstu no attēla, izmantojot AI, un no tā izveidot recepti.", "crop-and-rotate-the-image": "Apgrieziet un pagrieziet attēlu tā, lai būtu redzams tikai teksts un tas būtu pareizajā orientācijā.", "create-from-images": "Create from Images", diff --git a/frontend/app/lang/messages/nl-NL.json b/frontend/app/lang/messages/nl-NL.json index 7f94c7d3b..21bbe251b 100644 --- a/frontend/app/lang/messages/nl-NL.json +++ b/frontend/app/lang/messages/nl-NL.json @@ -51,7 +51,7 @@ "category": "Categorie" }, "events": { - "apprise-url": "Apprise URL", + "apprise-url": "Kennisgevings-url", "database": "Database", "delete-event": "Gebeurtenis verwijderen", "event-delete-confirmation": "Weet je zeker dat je deze gebeurtenis wilt verwijderen?", @@ -98,7 +98,7 @@ "dashboard": "Dashboard", "delete": "Verwijderen", "disabled": "Uitgeschakeld", - "done": "Done", + "done": "Gereed", "download": "Downloaden", "duplicate": "Dupliceren", "edit": "Bewerken", @@ -169,7 +169,7 @@ "token": "Token", "tuesday": "dinsdag", "type": "Soort", - "undo": "Undo", + "undo": "Ongedaan maken", "update": "Bijwerken", "updated": "Bijgewerkt", "upload": "Uploaden", @@ -333,8 +333,8 @@ "any-household": "Elk huishouden", "no-meal-plan-defined-yet": "Nog geen maaltijdplan opgesteld", "no-meal-planned-for-today": "Geen maaltijd gepland voor vandaag", - "numberOfDaysPast-hint": "Number of days in the past on page load", - "numberOfDaysPast-label": "Default Days in the Past", + "numberOfDaysPast-hint": "Aantal dagen in het verleden bij laden pagina", + "numberOfDaysPast-label": "Standaard dagen in het verleden", "numberOfDays-hint": "Aantal dagen bij laden van de pagina", "numberOfDays-label": "Standaard aantal dagen", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Alleen recepten met deze categorieën zullen worden gebruikt in maaltijdplannen", @@ -443,7 +443,7 @@ "error-details": "Alleen websites met ld+json of microdata kunnen worden geïmporteerd door Mealie. De meeste grote receptenwebsites ondersteunen deze gegevensstructuur. Als je site niet kan worden geïmporteerd, maar er zijn json-gegevens in de log, maak dan een github issue aan met de URL en gegevens.", "error-title": "Het lijkt erop dat we niets konden vinden", "from-url": "Recept importeren", - "github-issues": "GitHub Issues", + "github-issues": "GitHubproblemen", "google-ld-json-info": "Google ld+json Info", "must-be-a-valid-url": "Moet een geldige URL zijn", "paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Plak je receptgegevens. Elke regel wordt behandeld als een item in een lijst", @@ -628,7 +628,7 @@ "create-recipe-description": "Maak een nieuw recept.", "create-recipes": "Recepten aanmaken", "import-with-zip": "Importeer met .zip", - "create-recipe-from-an-image": "Maak recept van de tekst op een afbeelding", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Maak een recept door een afbeelding ervan te uploaden. Mealie probeert de tekst met behulp van AI uit de afbeelding te halen en er een recept uit te maken.", "crop-and-rotate-the-image": "Snijd de afbeelding bij zodat alleen tekst zichtbaar is. En draai t plaatje zodat het leesbaar is.", "create-from-images": "Maak recept van een afbeelding", @@ -893,17 +893,17 @@ "server-side-base-url-error-text": "`BASE_URL` is nog steeds de standaard waarde op de API Server. Dit geeft problemen met notificatielinks in e-mails etc.", "server-side-base-url-success-text": "Server-side URL komt niet overeen met de standaard", "ldap-ready": "LDAP klaar", - "ldap-not-ready": "LDAP Not Ready", + "ldap-not-ready": "LDAP niet gereed", "ldap-ready-error-text": "Niet alle LDAP-waarden zijn geconfigureerd. Dit kan worden genegeerd als je geen LDAP-authenticatie gebruikt.", "ldap-ready-success-text": "Vereiste LDAP variabelen zijn helemaal ingesteld.", "build": "Build", "recipe-scraper-version": "Versie van de receptenscraper", "oidc-ready": "OIDC klaar", - "oidc-not-ready": "OIDC Not Ready", + "oidc-not-ready": "OIDC niet gereed", "oidc-ready-error-text": "Niet alle OIDC-waarden zijn geconfigureerd. Dit kan worden genegeerd als je geen OIDC-authenticatie gebruikt.", "oidc-ready-success-text": "Vereiste OIDC-variabelen zijn allemaal ingesteld.", "openai-ready": "OpenAI staat klaar", - "openai-not-ready": "OpenAI Not Ready", + "openai-not-ready": "OpenAI niet gereed", "openai-ready-error-text": "Niet alle tekstvakken voor OpenAI zijn ingevuld. Als je geen OpenAI gebruikt kun je dit leeg laten.", "openai-ready-success-text": "Verplichte tekstvakken voor OpenAI zijn ingevuld." }, @@ -917,7 +917,7 @@ "quantity": "Hoeveelheid: {0}", "shopping-list": "Boodschappenlijst", "shopping-lists": "Boodschappenlijsten", - "add-item": "Add item", + "add-item": "Item toevoegen", "food": "Levensmiddelen", "note": "Notitie", "label": "Label", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Weet je zeker dat je alle items wilt deselecteren?", "are-you-sure-you-want-to-delete-checked-items": "Weet je zeker dat je de geselecteerde items wilt verwijderen?", "no-shopping-lists-found": "Geen boodschappenlijsten gevonden", - "item-checked-off": "Checked off {item}" + "item-checked-off": "Uitgevinkt {item}" }, "sidebar": { "all-recipes": "Alle Recepten", @@ -1283,7 +1283,7 @@ "split-by-block": "Splits per tekstblok", "flatten": "Plat maken ongeacht originele opmaak", "help": { - "help": "Help", + "help": "Hulp", "mouse-modes": "Muismodus", "selection-mode": "Selectiemodus (standaard)", "selection-mode-desc": "De selectiemodus is de hoofdmodus die gebruikt kan worden om gegevens in te voeren:", @@ -1478,10 +1478,10 @@ "max-length": "Moet maximaal {max} tekens bevatten|Moet maximaal {max} tekens bevatten" }, "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" + "announcements": "Aankondigingen", + "all-announcements": "Alle aankondigingen", + "mark-all-as-read": "Alles markeren als gelezen", + "show-announcements-from-mealie": "Aankondigingen van Mealie weergeven", + "show-announcements-setting-description": "Of je gebruikers wel of niet meldingen van Mealie wilt laten zien. Wanneer ingeschakeld kunnen gebruikers nog steeds afzien van het bekijken van hen in hun gebruikersinstellingen" } } diff --git a/frontend/app/lang/messages/no-NO.json b/frontend/app/lang/messages/no-NO.json index c3119afd4..da076fccd 100644 --- a/frontend/app/lang/messages/no-NO.json +++ b/frontend/app/lang/messages/no-NO.json @@ -98,7 +98,7 @@ "dashboard": "Kontrollpanel", "delete": "Slett", "disabled": "Deaktivert", - "done": "Done", + "done": "Ferdig", "download": "Last ned", "duplicate": "Dupliser", "edit": "Rediger", @@ -169,7 +169,7 @@ "token": "Token", "tuesday": "Tirsdag", "type": "Type", - "undo": "Undo", + "undo": "Angre", "update": "Oppdater", "updated": "Oppdatert", "upload": "Last opp", @@ -334,7 +334,7 @@ "no-meal-plan-defined-yet": "Ingen måltidsplan er definert ennå", "no-meal-planned-for-today": "Ingen måltid planlagt i dag", "numberOfDaysPast-hint": "Number of days in the past on page load", - "numberOfDaysPast-label": "Default Days in the Past", + "numberOfDaysPast-label": "Standard antall dager tilbake", "numberOfDays-hint": "Antall dager på sideinnlasting", "numberOfDays-label": "Standard antall dager", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Kun oppskrifter med disse kategoriene vil bli brukt i måltidsplaner", @@ -392,7 +392,7 @@ "nextcloud": { "description": "Overfør data fra en Nextcloud Cookbook-instans", "description-long": "Oppskrifter fra Nextcloud kan importeres fra en zip-fil som inneholder dataene lagret i Nextcloud. Se eksempelet på mappestrukture nedenfor for å sikre at oppskriftene kan importeres.", - "title": "Nextcloud Cookbook" + "title": "Nextcloud kokebok" }, "copymethat": { "description-long": "Mealie kan importere oppskrifter fra Copy Me That. Eksporter oppskrifter i HTML-format, last deretter opp .zip-filen under.", @@ -628,7 +628,7 @@ "create-recipe-description": "Opprett en ny oppskrift fra bunnen av.", "create-recipes": "Opprett oppskrifter", "import-with-zip": "Importer fra .zip-fil", - "create-recipe-from-an-image": "Opprett oppskrift fra et bilde", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Opprett en oppskrift ved å laste opp et bilde av den. Mealie vil forsøke å hente ut teksten fra bildet ved bruk av AI, og lage en ny oppskrift.", "crop-and-rotate-the-image": "Beskjær og roter bildet slik at bare teksten er synlig, og at det er i riktig retning.", "create-from-images": "Opprett fra bilde", @@ -917,7 +917,7 @@ "quantity": "Antall: {0}", "shopping-list": "Handleliste", "shopping-lists": "Handlelister", - "add-item": "Add item", + "add-item": "Legg til produkt", "food": "Matvare", "note": "Notat", "label": "Etikett", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på at du vil fjerne valg av alle elementer?", "are-you-sure-you-want-to-delete-checked-items": "Er du sikker på at du vil slette alle valgte elementer?", "no-shopping-lists-found": "Ingen handlelister funnet", - "item-checked-off": "Checked off {item}" + "item-checked-off": "Avkrysset av {item}" }, "sidebar": { "all-recipes": "Alle oppskrifter", @@ -1478,10 +1478,10 @@ "max-length": "Må være minst minst {max} tegn må bestå av maks {max} tegn" }, "announcements": { - "announcements": "Announcements", - "all-announcements": "All announcements", - "mark-all-as-read": "Mark All as Read", - "show-announcements-from-mealie": "Show announcements from Mealie", + "announcements": "Kunngjøringer", + "all-announcements": "Alle kunngjøringer", + "mark-all-as-read": "Marker alle som lest", + "show-announcements-from-mealie": "Vis kunngjøringer fra 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" } } diff --git a/frontend/app/lang/messages/pl-PL.json b/frontend/app/lang/messages/pl-PL.json index 4eb82df5b..f29529dc2 100644 --- a/frontend/app/lang/messages/pl-PL.json +++ b/frontend/app/lang/messages/pl-PL.json @@ -51,7 +51,7 @@ "category": "Kategoria" }, "events": { - "apprise-url": "Apprise URL", + "apprise-url": "URL Apprise", "database": "Baza danych", "delete-event": "Usuń wydarzenie", "event-delete-confirmation": "Czy na pewno chcesz usunąć to zdarzenie?", @@ -98,7 +98,7 @@ "dashboard": "Panel główny", "delete": "Usuń", "disabled": "Wyłączone", - "done": "Done", + "done": "Gotowe", "download": "Pobierz", "duplicate": "Duplikuj", "edit": "Edytuj", @@ -169,7 +169,7 @@ "token": "Token", "tuesday": "Wtorek", "type": "Typ", - "undo": "Undo", + "undo": "Cofnij", "update": "Zaktualizuj", "updated": "Zaktualizowano", "upload": "Prześlij", @@ -628,7 +628,7 @@ "create-recipe-description": "Utwórz nowy przepis od zera.", "create-recipes": "Utwórz przepisy", "import-with-zip": "Importuj z pliku .zip", - "create-recipe-from-an-image": "Utwórz przepis z obrazów", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Utwórz przepis poprzez przesłanie obrazów tekstu przepisu. Mealie spróbuje wyodrębnić tekst z obrazów za pomocą AI i utworzyć z niego przepis.", "crop-and-rotate-the-image": "Przytnij i obróć obraz, tak aby był w odpowiedniej orientacji i był widoczny tylko tekst.", "create-from-images": "Utwórz przepis z obrazów", @@ -917,7 +917,7 @@ "quantity": "Ilość: {0}", "shopping-list": "Lista zakupów", "shopping-lists": "Listy zakupów", - "add-item": "Add item", + "add-item": "Dodaj element", "food": "Jedzenie", "note": "Notatka", "label": "Etykieta", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Czy na pewno chcesz odznaczyć wszystkie elementy?", "are-you-sure-you-want-to-delete-checked-items": "Czy jesteś pewien, że chcesz usunąć wszystkie zaznaczone elementy?", "no-shopping-lists-found": "Nie znaleziono list zakupów", - "item-checked-off": "Checked off {item}" + "item-checked-off": "Zaznaczono {item}" }, "sidebar": { "all-recipes": "Wszystkie", @@ -1478,10 +1478,10 @@ "max-length": "Może zawierać co najwyżej {max} znak|Może zawierać co najwyżej {max} znaki|Może zawierać co najwyżej {max} znaków" }, "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" + "announcements": "Ogłoszenia", + "all-announcements": "Wszystkie ogłoszenia", + "mark-all-as-read": "Oznacz wszystkie jako przeczytane", + "show-announcements-from-mealie": "Pokazuj ogłoszenia z Mealie", + "show-announcements-setting-description": "Czy chcesz by użytkownicy widzieli ogłoszenia z Mealie? Użytkownicy będą w dalszym ciągu mogli wyłączyć ogłoszenia w swoich ustawieniach użytkownika" } } diff --git a/frontend/app/lang/messages/pt-BR.json b/frontend/app/lang/messages/pt-BR.json index 3fbcf6556..d628a7723 100644 --- a/frontend/app/lang/messages/pt-BR.json +++ b/frontend/app/lang/messages/pt-BR.json @@ -628,7 +628,7 @@ "create-recipe-description": "Criar uma receita do zero.", "create-recipes": "Criar Receitas", "import-with-zip": "Importar a partir de .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": "Corte e gire a imagem para que apenas o texto esteja visível e esteja na posição correta.", "create-from-images": "Criar a partir de imagens", diff --git a/frontend/app/lang/messages/pt-PT.json b/frontend/app/lang/messages/pt-PT.json index 8d611ee83..e328b062c 100644 --- a/frontend/app/lang/messages/pt-PT.json +++ b/frontend/app/lang/messages/pt-PT.json @@ -628,7 +628,7 @@ "create-recipe-description": "Criar uma receita em branco.", "create-recipes": "Criar Receitas", "import-with-zip": "Importar com .zip", - "create-recipe-from-an-image": "Criar receita a partir de uma imagem", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Crie uma receita carregando uma imagem da mesma. O Mealie tentará extrair o texto da imagem utilizando IA e criará uma receita a partir da mesma.", "crop-and-rotate-the-image": "Recorte e rode a imagem de modo a que apenas o texto seja visível e esteja na orientação correta.", "create-from-images": "Criar a partir de Imagens", diff --git a/frontend/app/lang/messages/ro-RO.json b/frontend/app/lang/messages/ro-RO.json index fe8aba712..2455e267d 100644 --- a/frontend/app/lang/messages/ro-RO.json +++ b/frontend/app/lang/messages/ro-RO.json @@ -628,7 +628,7 @@ "create-recipe-description": "Creează o rețetă nouă de la zero.", "create-recipes": "Crează rețetă", "import-with-zip": "Importă cu .zip", - "create-recipe-from-an-image": "Creează o rețetă dintr-o imagine", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Creează o rețetă prin încărcarea unei imagini a acesteia. Mealie va încerca să extragă textul din imagine folosind AI și să creeze o rețetă din el.", "crop-and-rotate-the-image": "Decupați și rotiți imaginea astfel încât numai textul să fie vizibil, iar orientarea să fie corectă.", "create-from-images": "Creează din Imagini", diff --git a/frontend/app/lang/messages/ru-RU.json b/frontend/app/lang/messages/ru-RU.json index 1b1fe63f6..447f9baa0 100644 --- a/frontend/app/lang/messages/ru-RU.json +++ b/frontend/app/lang/messages/ru-RU.json @@ -628,7 +628,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 попытается извлечь текст из изображения с помощью ИИ и создать рецепт из него.", "crop-and-rotate-the-image": "Обрежьте и поверните изображение так, чтобы виден был только текст в нужной ориентации.", "create-from-images": "Создать из изображений", diff --git a/frontend/app/lang/messages/sk-SK.json b/frontend/app/lang/messages/sk-SK.json index d3386cb0b..7c50261b1 100644 --- a/frontend/app/lang/messages/sk-SK.json +++ b/frontend/app/lang/messages/sk-SK.json @@ -628,7 +628,7 @@ "create-recipe-description": "Vytvoriť nový recept od začiatku.", "create-recipes": "Vytvoriť recept", "import-with-zip": "Importovať .zip súbor", - "create-recipe-from-an-image": "Vytvoriť recept z obrázka", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Vytvoriť recept nahraním fotografie jedla. Mealie sa pokúsi previesť obrázok na text pomocou AI a vytvorí k nemu recept.", "crop-and-rotate-the-image": "Orežte a otočte obrázok tak, aby bol viditeľný iba text a aby mal správnu orientáciu.", "create-from-images": "Vytvoriť z obrázka", diff --git a/frontend/app/lang/messages/sl-SI.json b/frontend/app/lang/messages/sl-SI.json index 7ec0ac51b..e780a48d4 100644 --- a/frontend/app/lang/messages/sl-SI.json +++ b/frontend/app/lang/messages/sl-SI.json @@ -628,7 +628,7 @@ "create-recipe-description": "Ustvari nov recept.", "create-recipes": "Ustvari recepte", "import-with-zip": "Uvozi z .zip", - "create-recipe-from-an-image": "Ustvari recept iz slike", + "create-recipe-from-images": "Ustvari recept iz slik", "create-recipe-from-an-image-description": "Ustvarite recept tako, da naložite njegovo sliko. Mealie bo s pomočjo umetne inteligence poskušal izluščiti besedilo iz slike in iz njega ustvariti recept.", "crop-and-rotate-the-image": "Obrežite in zasukajte sliko, tako da bo vidno samo besedilo in da bo v pravilnem položaju.", "create-from-images": "Ustvari iz slik", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Ali res ne želite izbrati vseh elementov?", "are-you-sure-you-want-to-delete-checked-items": "Ali ste prepričani, da želite izbrisati vse izbrane elemente?", "no-shopping-lists-found": "Ni nakupovalnih seznamov", - "item-checked-off": "Checked off {item}" + "item-checked-off": "Odkljukano {item}" }, "sidebar": { "all-recipes": "Vsi recepti", diff --git a/frontend/app/lang/messages/sr-SP.json b/frontend/app/lang/messages/sr-SP.json index 4c2f6d03f..5ba89d67f 100644 --- a/frontend/app/lang/messages/sr-SP.json +++ b/frontend/app/lang/messages/sr-SP.json @@ -628,7 +628,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 ће покушати да издвоји текст из слике користећи АИ и од њега направи рецепт.", "crop-and-rotate-the-image": "Исеците и ротирајте слику тако да је видљив само текст и да је у исправној оријентацији.", "create-from-images": "Креирај из слика", diff --git a/frontend/app/lang/messages/sv-SE.json b/frontend/app/lang/messages/sv-SE.json index c6e3eed44..555ebc39d 100644 --- a/frontend/app/lang/messages/sv-SE.json +++ b/frontend/app/lang/messages/sv-SE.json @@ -169,7 +169,7 @@ "token": "Token", "tuesday": "Tisdag", "type": "Typ", - "undo": "Undo", + "undo": "Ångra", "update": "Uppdatera", "updated": "Uppdaterad", "upload": "Ladda upp", @@ -333,8 +333,8 @@ "any-household": "Valfritt hushåll", "no-meal-plan-defined-yet": "Ingen måltidsplan definierad ännu", "no-meal-planned-for-today": "Ingen måltidsplan för idag", - "numberOfDaysPast-hint": "Number of days in the past on page load", - "numberOfDaysPast-label": "Default Days in the Past", + "numberOfDaysPast-hint": "Antal förflutna dagar vid sidhämtning", + "numberOfDaysPast-label": "Förvalda förflutna dagar", "numberOfDays-hint": "Antal dagar vid sidhämtning", "numberOfDays-label": "Förvalda dagar", "only-recipes-with-these-categories-will-be-used-in-meal-plans": "Endast recept med dessa kategorier kommer att användas i måltidsplaner", @@ -628,7 +628,7 @@ "create-recipe-description": "Skapa nytt recept från grunden.", "create-recipes": "Skapa recept", "import-with-zip": "Importera från .zip", - "create-recipe-from-an-image": "Skapa recept från en bild", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Skapa ett recept genom att ladda upp en bild på det. Mealie kommer att försöka extrahera texten från bilden med hjälp av AI och skapa ett recept från det.", "crop-and-rotate-the-image": "Beskär och rotera bilden så att endast texten är synlig och den är åt rätt håll.", "create-from-images": "Skapa från bild", @@ -812,7 +812,7 @@ "settings-updated": "Inställningar uppdaterade", "site-settings": "Systeminställningar", "theme": { - "accent": "Accent", + "accent": "Accentfärg", "dark": "Mörkt", "default-to-system": "Standard", "error": "Fel", @@ -893,17 +893,17 @@ "server-side-base-url-error-text": "`BASE_URL` är fortfarande standardvärdet på API-servern. Detta kommer att orsaka problem med meddelanden som genereras på servern för e-postmeddelanden, etc.", "server-side-base-url-success-text": "Serversidans URL matchar inte standard", "ldap-ready": "LDAP Redo", - "ldap-not-ready": "LDAP Not Ready", + "ldap-not-ready": "LDAP ej tillgängligt", "ldap-ready-error-text": "Alla LDAP-värden är inte konfigurerade. Detta kan ignoreras om du inte använder LDAP-autentisering.", "ldap-ready-success-text": "Alla obligatoriska LDAP-variabler är satta.", "build": "Bygge", "recipe-scraper-version": "Version av Recept-scraper", "oidc-ready": "OIDC Klar", - "oidc-not-ready": "OIDC Not Ready", + "oidc-not-ready": "OIDC ej tillgängligt", "oidc-ready-error-text": "Alla OIDC-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OIDC-autentisering.", "oidc-ready-success-text": "Alla obligatoriska OIDC-variabler är satta.", "openai-ready": "OpenAI redo", - "openai-not-ready": "OpenAI Not Ready", + "openai-not-ready": "OpenAI ej tillgängligt", "openai-ready-error-text": "Alla OpenAI-värden är inte konfigurerade. Detta kan ignoreras om du inte använder OpenAI-funktioner.", "openai-ready-success-text": "Alla obligatoriska OpenAI-variabler är satta." }, @@ -917,7 +917,7 @@ "quantity": "Antal {0}", "shopping-list": "Inköpslista", "shopping-lists": "Inköpslistor", - "add-item": "Add item", + "add-item": "Lägg till vara", "food": "Mat", "note": "Anteckning", "label": "Etikett", @@ -943,7 +943,7 @@ "are-you-sure-you-want-to-uncheck-all-items": "Är du säker på att du vill avmarkera alla objekt?", "are-you-sure-you-want-to-delete-checked-items": "Är du säker på att du vill ta bort alla markerade objekt?", "no-shopping-lists-found": "Inga inköpslistor hittades", - "item-checked-off": "Checked off {item}" + "item-checked-off": "Kryssat av {item}" }, "sidebar": { "all-recipes": "Recept", @@ -1478,10 +1478,10 @@ "max-length": "Måste Vara Som Mest {max} Tecken|Måste Vara Som Mest {max} Tecken" }, "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" + "announcements": "Meddelanden", + "all-announcements": "Alla meddelanden", + "mark-all-as-read": "Markera alla som lästa", + "show-announcements-from-mealie": "Visa meddelanden från Mealie", + "show-announcements-setting-description": "Om du vill tillåta användare att se meddelanden från Mealie eller inte. När funktionen är aktiverad kan användarna fortfarande välja att inte se dem i sina användarinställningar" } } diff --git a/frontend/app/lang/messages/tr-TR.json b/frontend/app/lang/messages/tr-TR.json index af76f5571..96d8ec1be 100644 --- a/frontend/app/lang/messages/tr-TR.json +++ b/frontend/app/lang/messages/tr-TR.json @@ -628,7 +628,7 @@ "create-recipe-description": "Sıfırdan yeni bir tarif oluşturun.", "create-recipes": "Tarif Oluştur", "import-with-zip": ".zip ile içe aktar", - "create-recipe-from-an-image": "Görüntüden yemek tarifi oluştur", + "create-recipe-from-images": "Create Recipe from Images", "create-recipe-from-an-image-description": "Bir görsel yükleyerek yemek tarifi oluşturun. Mealie, yapay zekâ kullanarak görseldeki bilgileri çekip yemek tarifini oluşturmaya çalışacaktır.", "crop-and-rotate-the-image": "Resmi sadece yazılar gözükecek şekilde kesin ve döndürün, ayrıca doğru yönde durduğuna emin olun.", "create-from-images": "Resimden Oluştur", diff --git a/frontend/app/lang/messages/uk-UA.json b/frontend/app/lang/messages/uk-UA.json index 384020a4c..260e17425 100644 --- a/frontend/app/lang/messages/uk-UA.json +++ b/frontend/app/lang/messages/uk-UA.json @@ -628,7 +628,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 спробує витягти текст з зображення за допомогою ШІ та створити рецепт з нього.", "crop-and-rotate-the-image": "Обріжте і поверніть зображення так, щоб було видно лише текст в правильній орієнтації.", "create-from-images": "Створити з зображень", @@ -911,7 +911,7 @@ "all-lists": "Всі списки", "create-shopping-list": "Сторити список покупок", "from-recipe": "З рецепту", - "ingredient-of-recipe": "Ingredient of {recipe}", + "ingredient-of-recipe": "Інгредієнт з {recipe}", "list-name": "Назва списку", "new-list": "Новий список", "quantity": "Кількість: {0}", diff --git a/frontend/app/lang/messages/vi-VN.json b/frontend/app/lang/messages/vi-VN.json index 0f621de7e..cff252b2d 100644 --- a/frontend/app/lang/messages/vi-VN.json +++ b/frontend/app/lang/messages/vi-VN.json @@ -628,7 +628,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", diff --git a/frontend/app/lang/messages/zh-CN.json b/frontend/app/lang/messages/zh-CN.json index 066368220..f89c2e963 100644 --- a/frontend/app/lang/messages/zh-CN.json +++ b/frontend/app/lang/messages/zh-CN.json @@ -628,7 +628,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将尝试使用人工智能从图像中提取文本,并从中创建一个新的食谱。", "crop-and-rotate-the-image": "裁剪并旋转图片,使仅文字可见且方向正确。", "create-from-images": "从图片创建", diff --git a/frontend/app/lang/messages/zh-TW.json b/frontend/app/lang/messages/zh-TW.json index 749cef389..8a23eaa20 100644 --- a/frontend/app/lang/messages/zh-TW.json +++ b/frontend/app/lang/messages/zh-TW.json @@ -628,7 +628,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": "從圖片建立", diff --git a/frontend/app/lib/api/admin/admin-ai-providers.ts b/frontend/app/lib/api/admin/admin-ai-providers.ts new file mode 100644 index 000000000..7cf7102ff --- /dev/null +++ b/frontend/app/lib/api/admin/admin-ai-providers.ts @@ -0,0 +1,27 @@ +import { BaseAPI } from "../base/base-clients"; +import type { AIProviderCreate, AIProviderOut, AIProviderUpdate } from "~/lib/api/types/group"; + +const prefix = "/api/admin"; + +const routes = { + providers: (groupId: string) => `${prefix}/groups/${groupId}/ai-providers/providers`, + providersId: (groupId: string, providerId: string) => `${prefix}/groups/${groupId}/ai-providers/providers/${providerId}`, +}; + +export class AdminAIProvidersApi extends BaseAPI { + async createProvider(groupId: string, payload: AIProviderCreate) { + return await this.requests.post(routes.providers(groupId), payload); + } + + async getProvider(groupId: string, providerId: string) { + return await this.requests.get(routes.providersId(groupId, providerId)); + } + + async updateProvider(groupId: string, providerId: string, payload: AIProviderUpdate) { + return await this.requests.put(routes.providersId(groupId, providerId), payload); + } + + async deleteProvider(groupId: string, providerId: string) { + return await this.requests.delete(routes.providersId(groupId, providerId)); + } +} diff --git a/frontend/app/lib/api/admin/admin-debug.ts b/frontend/app/lib/api/admin/admin-debug.ts index af6babfab..038f59fcf 100644 --- a/frontend/app/lib/api/admin/admin-debug.ts +++ b/frontend/app/lib/api/admin/admin-debug.ts @@ -4,11 +4,11 @@ import type { DebugResponse } from "~/lib/api/types/admin"; const prefix = "/api"; const routes = { - openai: `${prefix}/admin/debug/openai`, + openai: providerId => `${prefix}/admin/debug/openai/${providerId}`, }; export class AdminDebugAPI extends BaseAPI { - async debugOpenAI(fileObject: Blob | File | undefined = undefined, fileName = "") { + async debugOpenAI(providerId: string, fileObject: Blob | File | undefined = undefined, fileName = "") { let formData: FormData | null = null; if (fileObject) { formData = new FormData(); @@ -16,6 +16,6 @@ export class AdminDebugAPI extends BaseAPI { formData.append("extension", fileName.split(".").pop() ?? ""); } - return await this.requests.post(routes.openai, formData); + return await this.requests.post(routes.openai(providerId), formData); } } diff --git a/frontend/app/lib/api/client-admin.ts b/frontend/app/lib/api/client-admin.ts index 1352edba9..5d45e8324 100644 --- a/frontend/app/lib/api/client-admin.ts +++ b/frontend/app/lib/api/client-admin.ts @@ -6,6 +6,7 @@ import { AdminBackupsApi } from "./admin/admin-backups"; import { AdminMaintenanceApi } from "./admin/admin-maintenance"; import { AdminAnalyticsApi } from "./admin/admin-analytics"; import { AdminDebugAPI } from "./admin/admin-debug"; +import { AdminAIProvidersApi } from "./admin/admin-ai-providers"; import type { ApiRequestInstance } from "~/lib/api/types/non-generated"; export class AdminAPI { @@ -17,6 +18,7 @@ export class AdminAPI { public maintenance: AdminMaintenanceApi; public analytics: AdminAnalyticsApi; public debug: AdminDebugAPI; + public aiProviders: AdminAIProvidersApi; constructor(requests: ApiRequestInstance) { this.about = new AdminAboutAPI(requests); @@ -27,6 +29,7 @@ export class AdminAPI { this.maintenance = new AdminMaintenanceApi(requests); this.analytics = new AdminAnalyticsApi(requests); this.debug = new AdminDebugAPI(requests); + this.aiProviders = new AdminAIProvidersApi(requests); Object.freeze(this); } diff --git a/frontend/app/lib/api/client-user.ts b/frontend/app/lib/api/client-user.ts index 7d786f6f2..74ba694d6 100644 --- a/frontend/app/lib/api/client-user.ts +++ b/frontend/app/lib/api/client-user.ts @@ -24,6 +24,7 @@ import { MultiPurposeLabelsApi } from "./user/group-multiple-purpose-labels"; import { GroupEventNotifierApi } from "./user/group-event-notifier"; import { MealPlanRulesApi } from "./user/group-mealplan-rules"; import { GroupDataSeederApi } from "./user/group-seeder"; +import { AIProvidersAPI } from "./user/group-ai-providers"; import type { ApiRequestInstance } from "~/lib/api/types/non-generated"; export class UserApiClient { @@ -53,6 +54,7 @@ export class UserApiClient { public groupEventNotifier: GroupEventNotifierApi; public upload: UploadFile; public seeders: GroupDataSeederApi; + public aiProviders: AIProvidersAPI; constructor(requests: ApiRequestInstance) { // Recipes @@ -80,6 +82,7 @@ export class UserApiClient { this.shopping = new ShoppingApi(requests); this.multiPurposeLabels = new MultiPurposeLabelsApi(requests); this.seeders = new GroupDataSeederApi(requests); + this.aiProviders = new AIProvidersAPI(requests); // Admin this.backups = new BackupAPI(requests); diff --git a/frontend/app/lib/api/types/admin.ts b/frontend/app/lib/api/types/admin.ts index a5658d56f..173fa8a74 100644 --- a/frontend/app/lib/api/types/admin.ts +++ b/frontend/app/lib/api/types/admin.ts @@ -16,9 +16,6 @@ export interface AdminAboutInfo { enableOidc: boolean; oidcRedirect: boolean; oidcProviderName: string; - enableOpenai: boolean; - enableOpenaiImageServices: boolean; - enableOpenaiTranscriptionServices: boolean; tokenTime: number; versionLatest: string; apiPort: number; @@ -50,9 +47,6 @@ export interface AppInfo { enableOidc: boolean; oidcRedirect: boolean; oidcProviderName: string; - enableOpenai: boolean; - enableOpenaiImageServices: boolean; - enableOpenaiTranscriptionServices: boolean; tokenTime: number; } export interface AppStartupInfo { @@ -95,7 +89,6 @@ export interface CheckAppConfig { emailReady: boolean; ldapReady: boolean; oidcReady: boolean; - enableOpenai: boolean; baseUrlSet: boolean; isUpToDate: boolean; } diff --git a/frontend/app/lib/api/types/group.ts b/frontend/app/lib/api/types/group.ts index 498ea009e..9a45d0394 100644 --- a/frontend/app/lib/api/types/group.ts +++ b/frontend/app/lib/api/types/group.ts @@ -17,6 +17,77 @@ export type SupportedMigrations = | "recipekeeper" | "cookn"; +export interface AIProviderCreate { + name: string; + baseUrl?: string | null; + model: string; + timeout?: number; + requestHeaders?: { + [k: string]: string; + }; + requestParams?: { + [k: string]: string; + }; +} +export interface AIProviderOut { + name: string; + baseUrl?: string | null; + model: string; + timeout?: number; + requestHeaders?: { + [k: string]: string; + }; + requestParams?: { + [k: string]: string; + }; + id: string; +} +export interface AIProviderSave { + name: string; + baseUrl?: string | null; + model: string; + timeout?: number; + requestHeaders?: { + [k: string]: string; + }; + requestParams?: { + [k: string]: string; + }; + settingsId: string; +} +export interface AIProviderSettingsCreate { + groupId: string; +} +export interface AIProviderSettingsOut { + defaultProviderId: string | null; + audioProviderId: string | null; + imageProviderId: string | null; + providers: AIProviderSummary[]; + aiEnabled: boolean; + audioProviderEnabled: boolean; + imageProviderEnabled: boolean; +} +export interface AIProviderSummary { + id: string; + name: string; +} +export interface AIProviderSettingsUpdate { + defaultProviderId: string | null; + audioProviderId: string | null; + imageProviderId: string | null; +} +export interface AIProviderUpdate { + name: string; + baseUrl?: string | null; + model: string; + timeout?: number; + requestHeaders?: { + [k: string]: string; + }; + requestParams?: { + [k: string]: string; + }; +} export interface CreateGroupPreferences { privateGroup?: boolean; showAnnouncements?: boolean; @@ -29,6 +100,7 @@ export interface GroupAdminUpdate { id: string; name: string; preferences?: UpdateGroupPreferences | null; + aiProviderSettings?: AIProviderSettingsUpdate | null; } export interface UpdateGroupPreferences { privateGroup?: boolean; diff --git a/frontend/app/lib/api/types/user.ts b/frontend/app/lib/api/types/user.ts index a5c3adaa1..c1dc7a706 100644 --- a/frontend/app/lib/api/types/user.ts +++ b/frontend/app/lib/api/types/user.ts @@ -59,6 +59,7 @@ export interface GroupInDB { households?: GroupHouseholdSummary[] | null; users?: UserSummary[] | null; preferences?: ReadGroupPreferences | null; + aiProviderSettings?: AIProviderSettingsOut | null; } export interface CategoryBase { name: string; @@ -89,11 +90,25 @@ export interface ReadGroupPreferences { groupId: string; id: string; } +export interface AIProviderSettingsOut { + defaultProviderId: string | null; + audioProviderId: string | null; + imageProviderId: string | null; + providers: AIProviderSummary[]; + aiEnabled: boolean; + audioProviderEnabled: boolean; + imageProviderEnabled: boolean; +} +export interface AIProviderSummary { + id: string; + name: string; +} export interface GroupSummary { name: string; id: string; slug: string; preferences?: ReadGroupPreferences | null; + aiProviderSettings?: AIProviderSettingsOut | null; } export interface LongLiveTokenCreateResponse { name: string; diff --git a/frontend/app/lib/api/user/group-ai-providers.ts b/frontend/app/lib/api/user/group-ai-providers.ts new file mode 100644 index 000000000..4fe85621e --- /dev/null +++ b/frontend/app/lib/api/user/group-ai-providers.ts @@ -0,0 +1,27 @@ +import { BaseAPI } from "../base/base-clients"; +import type { AIProviderCreate, AIProviderOut, AIProviderUpdate } from "~/lib/api/types/group"; + +const prefix = "/api/groups/ai-providers"; + +const routes = { + providers: `${prefix}/providers`, + providersId: (id: string) => `${prefix}/providers/${id}`, +}; + +export class AIProvidersAPI extends BaseAPI { + async getOne(id: string) { + return await this.requests.get(routes.providersId(id)); + } + + async createOne(payload: AIProviderCreate) { + return await this.requests.post(routes.providers, payload); + } + + async updateOne(id: string, payload: AIProviderUpdate) { + return await this.requests.put(routes.providersId(id), payload); + } + + async deleteOne(id: string) { + return await this.requests.delete(routes.providersId(id)); + } +} diff --git a/frontend/app/lib/api/user/groups.ts b/frontend/app/lib/api/user/groups.ts index bd3611d9c..c1a75ddbf 100644 --- a/frontend/app/lib/api/user/groups.ts +++ b/frontend/app/lib/api/user/groups.ts @@ -3,6 +3,8 @@ import type { PaginationData } from "../types/non-generated"; import type { QueryValue } from "../base/route"; import type { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user"; import type { + AIProviderSettingsUpdate, + AIProviderSettingsOut, GroupAdminUpdate, GroupStorage, ReadGroupPreferences, @@ -15,6 +17,7 @@ const routes = { groups: `${prefix}/admin/groups`, groupsSelf: `${prefix}/groups/self`, preferences: `${prefix}/groups/preferences`, + aiProviderSettings: `${prefix}/groups/ai-providers/settings`, storage: `${prefix}/groups/storage`, members: `${prefix}/groups/members`, groupsId: (id: string | number) => `${prefix}/admin/groups/${id}`, @@ -29,15 +32,15 @@ export class GroupAPI extends BaseCRUDAPI(routes.groupsSelf); } - async getPreferences() { - return await this.requests.get(routes.preferences); - } - async setPreferences(payload: UpdateGroupPreferences) { // TODO: This should probably be a patch request, which isn't offered by the API currently return await this.requests.put(routes.preferences, payload); } + async setAIProviderSettings(payload: AIProviderSettingsUpdate) { + return await this.requests.put(routes.aiProviderSettings, payload); + } + async fetchMembers(page = 1, perPage = -1, params = {} as Record) { return await this.requests.get>(routes.members, { page, perPage, ...params }); } diff --git a/frontend/app/lib/api/user/households.ts b/frontend/app/lib/api/user/households.ts index 363af940d..62b6ba8c0 100644 --- a/frontend/app/lib/api/user/households.ts +++ b/frontend/app/lib/api/user/households.ts @@ -43,10 +43,6 @@ export class HouseholdAPI extends BaseCRUDAPIReadOnly { return await this.requests.get(routes.householdsSelfRecipesSlug(recipeSlug)); } - async getPreferences() { - return await this.requests.get(routes.preferences); - } - async setPreferences(payload: UpdateHouseholdPreferences) { // TODO: This should probably be a patch request, which isn't offered by the API currently return await this.requests.put(routes.preferences, payload); diff --git a/frontend/app/pages/admin/debug/openai.vue b/frontend/app/pages/admin/debug/openai.vue index 3b1388df3..1d69f09ba 100644 --- a/frontend/app/pages/admin/debug/openai.vue +++ b/frontend/app/pages/admin/debug/openai.vue @@ -6,7 +6,7 @@
@@ -17,6 +17,36 @@
+ + + + + + + + import { useAdminApi } from "~/composables/api"; +import { useGroups } from "~/composables/use-groups"; import { alert } from "~/composables/use-toast"; +import type { AIProviderSummary } from "~/lib/api/types/group"; definePageMeta({ layout: "admin", @@ -106,10 +139,24 @@ const uploadedImage = ref(); const uploadedImageName = ref(""); const uploadedImagePreviewUrl = ref(); -function uploadImage(fileObject: File) { - uploadedImage.value = fileObject; - uploadedImageName.value = fileObject.name; - uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject); +// Group + provider selection +const { groups } = useGroups(); +const selectedGroupId = ref(null); +const groupProviders = ref([]); +const selectedProviderId = ref(null); + +watch(selectedGroupId, (id) => { + groupProviders.value = []; + selectedProviderId.value = null; + if (!id) return; + const group = groups.value?.find(g => g.id === id); + groupProviders.value = group?.aiProviderSettings?.providers ?? []; +}); + +function uploadImage(fileObject: unknown) { + uploadedImage.value = fileObject as File; + uploadedImageName.value = (fileObject as File).name; + uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject as File); } function clearImage() { @@ -119,10 +166,15 @@ function clearImage() { } async function testOpenAI() { + if (!selectedProviderId.value) { + alert.error("Please select a provider"); + return; + } + response.value = ""; loading.value = true; - const { data } = await api.debug.debugOpenAI(uploadedImage.value); + const { data } = await api.debug.debugOpenAI(selectedProviderId.value, uploadedImage.value); loading.value = false; if (!data) { diff --git a/frontend/app/pages/admin/manage/groups/[id].vue b/frontend/app/pages/admin/manage/groups/[id].vue index 781f8c7d0..61b7a7643 100644 --- a/frontend/app/pages/admin/manage/groups/[id].vue +++ b/frontend/app/pages/admin/manage/groups/[id].vue @@ -33,6 +33,13 @@ v-if="group.preferences" v-model="group.preferences" /> +
@@ -50,8 +57,10 @@ diff --git a/frontend/app/pages/admin/setup.vue b/frontend/app/pages/admin/setup.vue index 795795ecb..87bf27336 100644 --- a/frontend/app/pages/admin/setup.vue +++ b/frontend/app/pages/admin/setup.vue @@ -45,6 +45,14 @@ :title="$t('settings.site-settings')" /> + + + + + + + {{ $t('group.ai-provider-settings.ai-providers') }} + + + {{ $t('group.ai-provider-settings.ai-providers-description') }} + + + + + + + + @@ -252,7 +297,11 @@ import { useLocales } from "~/composables/use-locales"; import { alert } from "~/composables/use-toast"; import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form"; import { useCommonSettingsForm } from "~/composables/use-setup/common-settings-form"; +import { useGroupSelf } from "~/composables/use-groups"; +import { useAIProviders } from "~/composables/use-ai-providers"; import UserRegistrationForm from "~/components/Domain/User/UserRegistrationForm.vue"; +import GroupAIProviderSettingsEditor from "~/components/Domain/Group/GroupAIProviderSettingsEditor.vue"; +import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group"; definePageMeta({ layout: "blank", @@ -267,6 +316,42 @@ const userApi = useUserApi(); const adminApi = useAdminApi(); const groupSlug = computed(() => auth.user.value?.groupSlug); + +const { group, actions: groupActions } = useGroupSelf(); +const { createOne, updateOne, deleteOne } = useAIProviders(); + +async function handleCreateProvider(data: AIProviderCreate) { + const result = await createOne(data); + if (result.data) { + await groupActions.refresh(); + alert.success(i18n.t("group.ai-provider-settings.provider-created")); + } + else { + alert.error(i18n.t("group.ai-provider-settings.provider-create-failed")); + } +} + +async function handleUpdateProvider(id: string, data: AIProviderUpdate) { + const result = await updateOne(id, data); + if (result.data) { + await groupActions.refresh(); + alert.success(i18n.t("group.ai-provider-settings.provider-updated")); + } + else { + alert.error(i18n.t("group.ai-provider-settings.provider-update-failed")); + } +} + +async function handleDeleteProvider(id: string) { + const result = await deleteOne(id); + if (result.data) { + await groupActions.refresh(); + alert.success(i18n.t("group.ai-provider-settings.provider-deleted")); + } + else { + alert.error(i18n.t("group.ai-provider-settings.provider-delete-failed")); + } +} const { locale } = useLocales(); const router = useRouter(); const isSubmitting = ref(false); @@ -281,8 +366,9 @@ enum Pages { LANDING = 1, USER_INFO = 2, PAGE_2 = 3, - CONFIRM = 4, - END = 5, + AI_PROVIDERS = 4, + CONFIRM = 5, + END = 6, } function getStepperColor(currentPage: Pages, page: Pages) { @@ -475,6 +561,7 @@ async function submitAll() { const tasks = [ submitRegistration(), submitCommonSettings(), + groupActions.updateAIProviderSettings(), ]; await Promise.all(tasks); diff --git a/frontend/app/pages/admin/site-settings.vue b/frontend/app/pages/admin/site-settings.vue index 791e6838e..db4425893 100644 --- a/frontend/app/pages/admin/site-settings.vue +++ b/frontend/app/pages/admin/site-settings.vue @@ -284,7 +284,6 @@ const appConfig = ref({ isUpToDate: false, ldapReady: false, oidcReady: false, - enableOpenai: false, }); function isLocalHostOrHttps() { return window.location.hostname === "localhost" || window.location.protocol === "https:"; @@ -351,15 +350,6 @@ const simpleChecks = computed(() => { color: appConfig.value.oidcReady ? goodColor : warningColor, icon: appConfig.value.oidcReady ? goodIcon : warningIcon, }, - { - id: "openai-ready", - text: appConfig.value.enableOpenai ? i18n.t("settings.openai-ready") : i18n.t("settings.openai-not-ready"), - status: appConfig.value.enableOpenai, - errorText: i18n.t("settings.openai-ready-error-text"), - successText: i18n.t("settings.openai-ready-success-text"), - color: appConfig.value.enableOpenai ? goodColor : warningColor, - icon: appConfig.value.enableOpenai ? goodIcon : warningIcon, - }, ]; return data; }); diff --git a/frontend/app/pages/g/[groupSlug]/r/create.vue b/frontend/app/pages/g/[groupSlug]/r/create.vue index 04feb0c49..ab9e8b83c 100644 --- a/frontend/app/pages/g/[groupSlug]/r/create.vue +++ b/frontend/app/pages/g/[groupSlug]/r/create.vue @@ -45,6 +45,7 @@