Merge branch 'mealie-next' into fix/warn-on-edit-nav

This commit is contained in:
boc-the-git 2024-02-21 20:51:50 +11:00 committed by GitHub
commit 618c567392
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
232 changed files with 3870 additions and 2042 deletions

View File

@ -49,7 +49,9 @@
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && task setup",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
// "features": {
// "git": "latest"
// }
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2"
}
}
}

View File

@ -4,6 +4,12 @@ on:
push:
branches:
- mealie-next
paths-ignore:
- '*.md'
- '.devcontainer/**'
# I'm not excluding .github as changes in there might be to workflows etc
- '.vscode/**'
- 'docs/**'
concurrency:
group: nightly-${{ github.ref }}
@ -22,7 +28,13 @@ jobs:
permissions:
contents: read
packages: write
# The id-token write permission is needed to connect to Depot.dev
# as part of the partial-builder.yml action. It needs to be declared
# in the parent action, as noted here:
# https://github.com/orgs/community/discussions/76409#discussioncomment-8131390
id-token: write
name: Build Tagged Release
if: github.repository == 'mealie-recipes/mealie'
uses: ./.github/workflows/partial-builder.yml
needs:
- frontend-tests
@ -35,6 +47,7 @@ jobs:
notify-discord:
name: Notify Discord
if: github.repository == 'mealie-recipes/mealie'
needs:
- build-release
runs-on: ubuntu-latest

View File

@ -35,19 +35,16 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Override __init__.py
run: |
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
- name: Build and push Docker image
uses: docker/build-push-action@v5
- uses: depot/setup-action@v1
- name: Build and push Docker image, via Depot.dev
uses: depot/build-push-action@v1
with:
project: srzjb6mhzm
file: ./docker/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
@ -58,6 +55,3 @@ jobs:
${{ inputs.tags }}
build-args: |
COMMIT=${{ github.sha }}
# https://docs.docker.com/build/ci/github-actions/cache/#github-cache
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -17,6 +17,11 @@ jobs:
permissions:
contents: read
packages: write
# The id-token write permission is needed to connect to Depot.dev
# as part of the partial-builder.yml action. It needs to be declared
# in the parent action, as noted here:
# https://github.com/orgs/community/discussions/76409#discussioncomment-8131390
id-token: write
name: Build Tagged Release
uses: ./.github/workflows/partial-builder.yml
needs:
@ -49,6 +54,9 @@ jobs:
needs:
- build-release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
@ -58,11 +66,12 @@ jobs:
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/sqlite.md
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/postgres.md
- name: Commit updates
uses: test-room-7/action-update-file@v1
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
file-path: |
docs/docs/documentation/getting-started/installation/sqlite.md
docs/docs/documentation/getting-started/installation/postgres.md
commit-msg: "Change image tag, for release ${{ github.event.release.tag_name }}"
github-token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
delete-branch: true
base: mealie-next
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
body: "Auto-generated by `.github/workflows/release.yml`, on publish of release ${{ github.event.release.tag_name }}"

View File

@ -71,12 +71,9 @@ Distributed under the AGPL License. See `LICENSE` for more information.
Huge thanks to all the sponsors of this project on [Github Sponsors](https://github.com/sponsors/hay-kot) and Buy Me a Coffee. Without you, this project would surely not be possible.
Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sites! Another big thanks to JetBrains for providing their IDEs for development.
Thanks to Depot for providing build instances for our Docker image builds.
<div align='center'>
<img height="100" src="docs/docs/assets/img/sponsors-linode.svg" />
<img height="100" src="docs/docs/assets/img/sponsors-jetbrains.png" />
</div>
[![Built with Depot](https://depot.dev/badges/built-with-depot.svg)](https://depot.dev?utm_source=Mealie)

View File

@ -14,7 +14,9 @@ env:
SMTP_HOST: localhost
SMTP_PORT: 1025
SMTP_FROM_NAME: MealieDev
SMTP_FROM_EMAIL: mealie@example.com
SMTP_AUTH_STRATEGY: NONE
BASE_URL: http://localhost:3000
LANG: en-US
# loads .env file if it exists

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 230 90" width="2500" height="978"><path d="M93.8 27.8l5.8-1.4v28c0 3.1.9 4.9 2.7 5.5-.9 1.7-2.4 2.6-4.6 2.6-2.6 0-4-1.8-4-5.5V27.8zM108.4 62V41.8h-3.2V37h9.1v25h-5.9zm3-34.6c.9 0 1.7.3 2.4 1s1 1.5 1 2.4c0 .9-.3 1.7-1 2.4s-1.5 1-2.4 1c-.9 0-1.7-.3-2.4-1s-1-1.5-1-2.4c0-.9.3-1.7 1-2.4s1.5-1 2.4-1zM137.1 62V47.6c0-2.1-.4-3.7-1.2-4.6-.8-1-2.1-1.5-4-1.5-.9 0-1.8.2-2.7.7-1 .5-1.7 1.1-2.3 1.8v18h-5.8V37.1h4.2l1.1 2.3c1.6-1.9 3.9-2.8 7-2.8 3 0 5.3.9 7 2.7 1.7 1.8 2.6 4.3 2.6 7.4V62h-5.9zM147.5 49.5c0-3.8 1.1-6.9 3.3-9.3 2.2-2.4 5.1-3.6 8.7-3.6 3.8 0 6.7 1.1 8.8 3.4 2.1 2.3 3.1 5.4 3.1 9.4s-1.1 7.1-3.2 9.5c-2.1 2.3-5 3.5-8.8 3.5-3.8 0-6.7-1.2-8.8-3.5-2-2.4-3.1-5.5-3.1-9.4zm6.1 0c0 5.5 2 8.2 5.9 8.2 1.8 0 3.2-.7 4.3-2.1 1.1-1.4 1.6-3.5 1.6-6.1 0-5.4-2-8.1-5.9-8.1-1.8 0-3.3.7-4.3 2.1-1.1 1.4-1.6 3.4-1.6 6zM192.1 62v-1.5c-.5.5-1.3 1-2.4 1.4-1.1.4-2.3.6-3.6.6-3.5 0-6.2-1.1-8.2-3.3-2-2.2-3-5.3-3-9.2 0-3.9 1.1-7.1 3.4-9.6s5.1-3.7 8.6-3.7c1.9 0 3.6.4 5.2 1.2v-10l5.8-1.4V62h-5.8zm0-19c-1.2-1-2.5-1.5-3.9-1.5-2.3 0-4.1.7-5.4 2.1-1.3 1.4-1.9 3.5-1.9 6.1 0 5.2 2.5 7.8 7.5 7.8.6 0 1.2-.2 2.1-.5.8-.3 1.3-.7 1.6-1V43zM226 51.3h-17.8c.1 2 .8 3.5 2 4.6 1.3 1.1 2.9 1.7 5.1 1.7 2.6 0 4.7-.7 6-2.1l2.3 4.4c-2 1.7-5.1 2.5-9.2 2.5-3.8 0-6.8-1.1-9-3.3-2.2-2.2-3.3-5.3-3.3-9.3 0-3.9 1.2-7.1 3.6-9.5 2.4-2.4 5.3-3.6 8.7-3.6 3.6 0 6.5 1.1 8.7 3.2 2.2 2.2 3.3 4.9 3.3 8.2.1.7-.1 1.7-.4 3.2zm-17.6-4.4h12.2c-.4-3.6-2.4-5.5-6-5.5-3.3.1-5.4 1.9-6.2 5.5z"/><g><path fill="#004712" d="M65.9 47.4l-1 11.5-3.3-2.3.4-5.8v-.1-.1l-.1-.1-.1-.1-7.1-4.7.1-5.1 11.1 6.8zM48.5 59.9L43.4 56v.9c0 .2-.1.4-.2.5L39.4 60l4.2 3.4.1.1v.2l.2 4 4.7 3.9-.1-11.7zm-32.1 5l2.4 11.5 9.9 10.5L27 75.3 16.4 64.9zm9.3 1.7l-2.4-16.1-12-10 3.2 15.6 11.2 10.5zm-3.8-26l-3.3-22.8L4.8 9.2l4.5 21.5 12.6 9.9z"/><path fill="#00B259" d="M75.7 41.2l-1.5 10.9-8.2 6.6 1-11.2 8.7-6.3zM49.6 59.9l.1 11.8 10.5-8.4.7-11.5-11.3 8.1zm-6.8 4.8L28 75.3l1.8 12.2 13.4-10.7-.4-12.1zm-.4-8l-.7-16-17.3 9.9 2.4 16.6 15.6-10.5zm-1.1-25.3l-.9-21.6-20.8 8L23 41l18.3-9.6z"/><path d="M76.9 40c0-.1 0-.1 0 0v-.2s0-.1-.1-.1c0 0-.1 0-.1-.1l-12-6.7c-.2-.1-.4-.1-.5 0L54 39.1h-.1v.6l-.1 5.4-4.1-2.7c-.2-.1-.4-.1-.6 0L43 45.8l-.3-6v-.1-.1-.1-.1-.1h-.1l-6.2-4.1 5.8-3c.2-.1.3-.3.3-.5L41.4 9v-.1s0-.1-.1-.1c0 0 0-.1-.1-.1L25.5 1.1c-.1-.1-.2-.1-.3-.1L3.9 7.6s-.1 0-.1.1c0 0-.1 0-.1.1v.6l4.7 22.9c0 .1.1.2.2.3l6.4 5-4.7 2.2s-.1 0-.1.1c0 0 0 .1-.1.1v.2l3.6 17.2c0 .1.1.2.2.3l4.5 4.2-3 1.8-.1.1s0 .1-.1.1V63.2L18 76.5c0 .1.1.2.1.3l10.9 12h.1s.1 0 .1.1h.5l14.4-11.5c.1-.1.2-.3.2-.4l-.3-7.9 4.8 4s.1 0 .1.1h.5L61 64c.1-.1.2-.2.2-.4l.4-5.8 3.5 2.4h.4s.1 0 .1-.1l9.4-7.5c.1-.1.2-.2.2-.3L76.9 40c0 .1 0 .1 0 0zM66 58.7l1-11.2 8.8-6.3-1.5 10.9-8.3 6.6zm-4.4-2.1l.4-5.8v-.1-.1l-.1-.1-.1-.1-7.1-4.7.1-5.1 11.1 6.9-1 11.5-3.3-2.4zm-1.5 6.7l-10.5 8.4-.1-11.8 11.3-8.1-.7 11.5zM43.3 76.8L29.8 87.5 28 75.3l14.7-10.5.6 12zm-24.6-.4l-2.4-11.5L27 75.3l1.7 11.6-10-10.5zm-.2-58.6l3.3 22.8-12.5-9.9L4.8 9.2l13.7 8.6zm21.9-8l.9 21.6L23 41l-3.4-23.2 20.8-8zm2 46.9L26.8 67.1l-2.4-16.6 17.3-9.9.7 16.1zm-19.1-6.1l2.4 16.1-11.2-10.6-3.2-15.6 12 10.1zm20.5 13.1v-.1-.1l-.1-.1-4.2-3.4 3.8-2.6c.2-.1.2-.3.2-.5V56l5.1 3.9.1 11.8-4.7-3.9-.2-4.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -54,8 +54,8 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
| ---------------- | :-----: | --------------------------------------------------------------------------------------------------------------------------------- |
| WEB_GUNICORN | false | Enables Gunicorn to manage Uvicorn web for multiple works |
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
| MAX_WORKERS | None | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
| WEB_CONCURRENCY | 2 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
### LDAP
@ -95,3 +95,8 @@ Setting the following environmental variables will change the theme of the front
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
[workers_per_core]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#workers_per_core
[max_workers]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#max_workers
[web_concurrency]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#web_concurrency

File diff suppressed because one or more lines are too long

View File

@ -133,7 +133,7 @@ export default defineComponent({
const domMadeThisForm = ref<VForm>();
const newTimelineEvent = ref<RecipeTimelineEventIn>({
// @ts-expect-error - TS doesn't like the $auth global user attribute
subject: i18n.t("recipe.user-made-this", { user: $auth.user.fullName } as string),
subject: i18n.tc("recipe.user-made-this", { user: $auth.user.fullName }),
eventType: "comment",
eventMessage: "",
timestamp: undefined,

View File

@ -19,11 +19,11 @@
</div>
</v-card-text>
<v-list v-if="showViewer" dense class="mt-0 pt-0">
<v-list-item v-for="(item, key, index) in labels" :key="index" style="min-height: 25px" dense>
<v-list-item v-for="(item, key, index) in renderedList" :key="index" style="min-height: 25px" dense>
<v-list-item-content>
<v-list-item-title class="pl-4 caption flex row">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div class="ml-auto mr-1">{{ item.value }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
@ -37,6 +37,14 @@
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { Nutrition } from "~/lib/api/types/recipe";
type NutritionLabelType = {
[key: string]: {
label: string;
suffix: string;
value?: string;
};
};
export default defineComponent({
props: {
value: {
@ -50,34 +58,34 @@ export default defineComponent({
},
setup(props, context) {
const { i18n } = useContext();
const labels = {
const labels = <NutritionLabelType>{
calories: {
label: i18n.t("recipe.calories"),
suffix: i18n.t("recipe.calories-suffix"),
label: i18n.tc("recipe.calories"),
suffix: i18n.tc("recipe.calories-suffix"),
},
fatContent: {
label: i18n.t("recipe.fat-content"),
suffix: i18n.t("recipe.grams"),
label: i18n.tc("recipe.fat-content"),
suffix: i18n.tc("recipe.grams"),
},
fiberContent: {
label: i18n.t("recipe.fiber-content"),
suffix: i18n.t("recipe.grams"),
label: i18n.tc("recipe.fiber-content"),
suffix: i18n.tc("recipe.grams"),
},
proteinContent: {
label: i18n.t("recipe.protein-content"),
suffix: i18n.t("recipe.grams"),
label: i18n.tc("recipe.protein-content"),
suffix: i18n.tc("recipe.grams"),
},
sodiumContent: {
label: i18n.t("recipe.sodium-content"),
suffix: i18n.t("recipe.milligrams"),
label: i18n.tc("recipe.sodium-content"),
suffix: i18n.tc("recipe.milligrams"),
},
sugarContent: {
label: i18n.t("recipe.sugar-content"),
suffix: i18n.t("recipe.grams"),
label: i18n.tc("recipe.sugar-content"),
suffix: i18n.tc("recipe.grams"),
},
carbohydrateContent: {
label: i18n.t("recipe.carbohydrate-content"),
suffix: i18n.t("recipe.grams"),
label: i18n.tc("recipe.carbohydrate-content"),
suffix: i18n.tc("recipe.grams"),
},
};
const valueNotNull = computed(() => {
@ -96,11 +104,25 @@ export default defineComponent({
context.emit("input", { ...props.value, [key]: event });
}
// Build a new list that only contains nutritional information that has a value
const renderedList = computed(() => {
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
if (props.value[key]?.trim()) {
item[key] = {
...label,
value: props.value[key],
};
}
return item;
}, {});
});
return {
labels,
valueNotNull,
showViewer,
updateValue,
renderedList,
};
},
});

View File

@ -25,6 +25,7 @@
:label="$t('shopping-list.note')"
rows="1"
auto-grow
@keypress="handleNoteKeyPress"
></v-textarea>
</div>
<div class="d-flex align-end" style="gap: 20px">
@ -95,7 +96,7 @@
</template>
<script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api";
import { defineComponent, computed, watch } from "@nuxtjs/composition-api";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/group";
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
@ -128,9 +129,28 @@ export default defineComponent({
context.emit("input", val);
},
});
watch(
() => props.value.food,
(newFood) => {
// @ts-ignore our logic already assumes there's a label attribute, even if TS doesn't think there is
listItem.value.label = newFood?.label || null;
listItem.value.labelId = listItem.value.label?.id || null;
}
);
return {
listItem,
};
},
methods: {
handleNoteKeyPress(event) {
// Save on Enter
if (!event.shiftKey && event.key === "Enter") {
event.preventDefault();
this.$emit("save");
}
},
}
});
</script>

View File

@ -200,7 +200,7 @@
"created-on-date": "Създадено на {0}",
"unsaved-changes": "Имате незапазени промени. Желаете ли да ги запазите преди да излезете? Натиснете Ок за запазване и Отказ за отхвърляне на промените.",
"clipboard-copy-failure": "Линкът към рецептата е копиран в клипборда.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
"confirm-delete-generic-items": "Сигурни ли сте, че желаете да изтриете следните елементи?"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Сигурни ли сте, че искате да изтриете <b>{groupName}<b/>?",
@ -259,7 +259,7 @@
},
"meal-plan": {
"create-a-new-meal-plan": "Създаване на нов хранителен план",
"update-this-meal-plan": "Update this Meal Plan",
"update-this-meal-plan": "Обнови този План за хранене",
"dinner-this-week": "Вечеря тази седмица",
"dinner-today": "Вечеря Днес",
"dinner-tonight": "Вечеря ТАЗИ ВЕЧЕР",
@ -474,11 +474,11 @@
"add-to-timeline": "Добави към времевата линия",
"recipe-added-to-list": "Рецептата е добавена към списъка",
"recipes-added-to-list": "Рецептите са добавени към списъка",
"successfully-added-to-list": "Successfully added to list",
"successfully-added-to-list": "Успешно добавено в списъка",
"recipe-added-to-mealplan": "Рецептата е добавена към хранителния план",
"failed-to-add-recipes-to-list": "Неуспешно добавяне на рецепта към списъка",
"failed-to-add-recipe-to-mealplan": "Рецептата не беше добавена към хранителния план",
"failed-to-add-to-list": "Failed to add to list",
"failed-to-add-to-list": "Неуспешно добавяне към списъка",
"yield": "Добив",
"quantity": "Количество",
"choose-unit": "Избери единица",
@ -537,8 +537,8 @@
"new-recipe-names-must-be-unique": "Името на рецептата трябва да бъде уникално",
"scrape-recipe": "Обхождане на рецепта",
"scrape-recipe-description": "Обходи рецепта по линк. Предоставете линк за сайт, който искате да бъде обходен. Mealie ще опита да обходи рецептата от този сайт и да я добави във Вашата колекция.",
"scrape-recipe-have-a-lot-of-recipes": "Have a lot of recipes you want to scrape at once?",
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
"scrape-recipe-have-a-lot-of-recipes": "Имате много рецепти, които искате да обходите наведнъж?",
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
"import-original-keywords-as-tags": "Импортирай оригиналните ключови думи като тагове",
"stay-in-edit-mode": "Остани в режим на редакция",
"import-from-zip": "Импортирай от Zip",
@ -562,7 +562,7 @@
"upload-image": "Качване на изображение",
"screen-awake": "Запази екрана активен",
"remove-image": "Премахване на изображение",
"nextStep": "Next step"
"nextStep": "Следваща стъпка"
},
"search": {
"advanced-search": "Разширено търсене",
@ -1187,7 +1187,7 @@
"require-all-tools": "Изискване на всички инструменти",
"cookbook-name": "Име на книгата с рецепти",
"cookbook-with-name": "Книга с рецепти {0}",
"create-a-cookbook": "Create a Cookbook",
"cookbook": "Cookbook"
"create-a-cookbook": "Създай Готварска книга",
"cookbook": "Готварска книга"
}
}

View File

@ -9,11 +9,11 @@
"database-url": "URL de base de datos",
"default-group": "Grupo Predeterminado",
"demo": "Versión Demo",
"demo-status": "Estado Demo",
"demo-status": "Modo Demo",
"development": "Desarrollo",
"docs": "Documentación",
"download-log": "Descargar Log",
"download-recipe-json": "Último JSON recuperado",
"download-recipe-json": "Último JSON extraído",
"github": "Github",
"log-lines": "Líneas de registro",
"not-demo": "No Demo",
@ -33,7 +33,7 @@
"pdf": "PDF",
"recipe": "Receta",
"show-assets": "Mostrar recursos",
"error-submitting-form": "Se ha producido un error al enviar el formulario"
"error-submitting-form": "Error al enviar el formulario"
},
"category": {
"categories": "Categorías",
@ -200,7 +200,7 @@
"created-on-date": "Creado el {0}",
"unsaved-changes": "Tienes cambios sin guardar. ¿Quieres guardar antes de salir? Aceptar para guardar, Cancelar para descartar cambios.",
"clipboard-copy-failure": "No se pudo copiar al portapapeles.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
"confirm-delete-generic-items": "¿Estás seguro que quieres eliminar los siguientes elementos?"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Por favor, confirma que deseas eliminar <b>{groupName}<b/>",
@ -599,9 +599,9 @@
"import-summary": "Importar resumen",
"partial-backup": "Copia de seguridad parcial",
"unable-to-delete-backup": "No se puede eliminar la copia de seguridad.",
"experimental-description": "Backups a total snapshots of the database and data directory of the site. This includes all data and cannot be set to exclude subsets of data. You can think off this as a snapshot of Mealie at a specific time. Currently, {not-crossed-version} (data migrations are not done automatically). These serve as a database agnostic way to export and import data or backup the site to an external location.",
"experimental-description": "Las copias de seguridad son instantáneas completas de la base de datos y del directorio de datos del sitio. Esto incluye todos los datos y no se pueden configurar para excluir subconjuntos de datos. Puedes pensar en esto como una instantánea de Mealie en un momento específico. Estas sirven como una forma agnóstica de la base de datos para exportar e importar datos, o respaldar el sitio en una ubicación externa.",
"backup-restore": "Restaurar Copia de Seguridad",
"back-restore-description": "Restoring this backup will overwrite all the current data in your database and in the data directory and replace them with the contents of this backup. {cannot-be-undone} If the restoration is successful, you will be logged out.",
"back-restore-description": "Restaurar esta copia de seguridad sobrescribirá todos los datos actuales de su base de datos y del directorio de datos y los sustituirá por el contenido de esta copia. {cannot-be-undone} Si la restauración se realiza correctamente, se cerrará su sesión.",
"cannot-be-undone": "Esta acción no se puede deshacer, use con precaución.",
"postgresql-note": "Si estás usando PostGreSQL, por favor revisa el {backup-restore-process} antes de restaurar.",
"backup-restore-process-in-the-documentation": "copia de seguridad/proceso de restauración en la documentación",
@ -723,7 +723,7 @@
"ldap-ready": "LDAP Listo",
"ldap-ready-error-text": "No todos los valores LDAP están configurados. Esto puede ignorarse si no está usando autenticación LDAP.",
"ldap-ready-success-text": "Las variables LDAP requeridas están todas definidas.",
"build": "Compilar",
"build": "Compilación",
"recipe-scraper-version": "Versión de Analizador de Recetas"
},
"shopping-list": {
@ -962,9 +962,9 @@
"settings-chosen-explanation": "Los ajustes seleccionados aquí, excluyendo la opción bloqueada, se aplicarán a todas las recetas seleccionadas.",
"selected-length-recipe-s-settings-will-be-updated": "Se actualizarán los ajustes de {count} receta(s).",
"recipe-data": "Datos de la receta",
"recipe-data-description": "Use this section to manage the data associated with your recipes. You can perform several bulk actions on your recipes including exporting, deleting, tagging, and assigning categories.",
"recipe-columns": "Recipe Columns",
"data-exports-description": "This section provides links to available exports that are ready to download. These exports do expire, so be sure to grab them while they're still available.",
"recipe-data-description": "Utiliza esta sección para gestionar los datos asociados a tus recetas. Puedes realizar varias acciones de forma masiva en tus recetas, como exportar, eliminar, etiquetar y asignar categorías.",
"recipe-columns": "Columnas de Recetas",
"data-exports-description": "Esta sección proporciona enlaces a las exportaciones disponibles listas para descargar. Estas exportaciones caducan, así que asegúrate de descargarlas mientras estén disponibles.",
"data-exports": "Exportación de datos",
"tag": "Etiqueta",
"categorize": "Clasificar",
@ -973,14 +973,14 @@
"categorize-recipes": "Categorizar recetas",
"export-recipes": "Exportar recetas",
"delete-recipes": "Borrar Recetas",
"source-unit-will-be-deleted": "Source Unit will be deleted"
"source-unit-will-be-deleted": "Se eliminará la unidad de origen"
},
"create-alias": "Crear un Alias",
"manage-aliases": "Administrar Alias",
"seed-data": "Datos de ejemplo",
"seed": "Semilla",
"data-management": "Data Management",
"data-management-description": "Select which data set you want to make changes to.",
"data-management": "Gestión de Datos",
"data-management-description": "Selecciona el conjunto de datos al que deseas hacer cambios.",
"select-data": "Seleccionar datos",
"select-language": "Seleccionar idioma",
"columns": "Columnas",
@ -1003,7 +1003,7 @@
},
"user-registration": {
"user-registration": "Registro de usuario",
"registration-success": "Registration Success",
"registration-success": "Registrado con éxito",
"join-a-group": "Unirse a un grupo",
"create-a-new-group": "Crear un grupo nuevo",
"provide-registration-token-description": "Por favor, proporcione el token de registro asociado con el grupo al que desea unirse. Necesitará obtenerlo de un miembro del grupo.",
@ -1050,57 +1050,57 @@
},
"ocr-editor": {
"ocr-editor": "Editor de OCR",
"toolbar": "Toolbar",
"toolbar": "Barra de Herramientas",
"selection-mode": "Modo de selección",
"pan-and-zoom-picture": "Desplazar y hacer zoom en la imagen",
"split-text": "Split text",
"preserve-line-breaks": "Preserve original line breaks",
"split-by-block": "Split by text block",
"flatten": "Flatten regardless of original formating",
"split-text": "Dividir texto",
"preserve-line-breaks": "Mantener los saltos de línea originales",
"split-by-block": "División por bloques de texto",
"flatten": "Acoplar independientemente del formato original",
"help": {
"help": "Help",
"mouse-modes": "Mouse modes",
"selection-mode": "Selection Mode (default)",
"selection-mode-desc": "The selection mode is the main mode that can be used to enter data:",
"help": "Ayuda",
"mouse-modes": "Modos de ratón",
"selection-mode": "Modo selección (por defecto)",
"selection-mode-desc": "El modo de selección es el modo principal que puede utilizarse para introducir datos:",
"selection-mode-steps": {
"draw": "Draw a rectangle on the text you want to select.",
"draw": "Dibuja un rectángulo sobre el texto que deseas seleccionar.",
"click": "Click on any field on the right and then click back on the rectangle above the image.",
"result": "The selected text will appear inside the previously selected field."
},
"pan-and-zoom-mode": "Pan and Zoom Mode",
"pan-and-zoom-mode": "Modo Panorámico y Zoom",
"pan-and-zoom-desc": "Select pan and zoom by clicking the icon. This mode allows to zoom inside the image and move around to make using big images easier.",
"split-text-mode": "Split Text modes",
"split-text-mode": "Modos de división de texto",
"split-modes": {
"line-mode": "Line mode (default)",
"line-mode": "Modo de línea (por defecto)",
"line-mode-desc": "In line mode, the text will be propagated by keeping the original line breaks. This mode is useful when using bulk add on a list of ingredients where one ingredient is one line.",
"block-mode": "Block mode",
"block-mode-desc": "In block mode, the text will be split in blocks. This mode is useful when bulk adding instructions that are usually written in paragraphs.",
"flat-mode": "Flat mode",
"flat-mode-desc": "In flat mode, the text will be added to the selected recipe field with no line breaks."
"block-mode": "Modo en bloque",
"block-mode-desc": "En el modo de bloque, el texto se dividirá en bloques. Este modo es útil cuando se agregan instrucciones que están escritas en párrafos.",
"flat-mode": "Modo texto sin formato",
"flat-mode-desc": "En modo texto sin formato, el texto se añadirá al campo de la receta seleccionado sin saltos de línea."
}
}
},
"admin": {
"maintenance": {
"storage-details": "Storage Details",
"page-title": "Site Maintenance",
"summary-title": "Summary",
"button-label-get-summary": "Get Summary",
"storage-details": "Detalle del almacenamiento",
"page-title": "Mantenimiento del sitio",
"summary-title": "Índice",
"button-label-get-summary": "Obtener Resumen",
"button-label-open-details": "Detalles",
"info-description-data-dir-size": "Data Directory Size",
"info-description-log-file-size": "Log File Size",
"info-description-cleanable-directories": "Cleanable Directories",
"info-description-cleanable-images": "Cleanable Images",
"info-description-data-dir-size": "Tamaño del directorio de datos",
"info-description-log-file-size": "Tamaño del archivo de registro",
"info-description-cleanable-directories": "Directorios eliminables",
"info-description-cleanable-images": "Imágenes eliminables",
"storage": {
"title-temporary-directory": "Directorio temporal (.temp)",
"title-backups-directory": "Backups Directory (backups)",
"title-groups-directory": "Groups Directory (groups)",
"title-recipes-directory": "Recipes Directory (recipes)",
"title-user-directory": "User Directory (user)"
"title-backups-directory": "Directorio de Copias de Seguridad (respaldos)",
"title-groups-directory": "Directorio de Grupos (grupos)",
"title-recipes-directory": "Directorio de Recetas (recetas)",
"title-user-directory": "Directorio de usuario (usuario)"
},
"action-delete-log-files-name": "Delete Log Files",
"action-delete-log-files-description": "Deletes all the log files",
"action-clean-directories-name": "Clean Directories",
"action-delete-log-files-name": "Borrar archivos de registro",
"action-delete-log-files-description": "Elimina todos los archivos de registro",
"action-clean-directories-name": "Limpiar directorios",
"action-clean-directories-description": "Removes all the recipe folders that are not valid UUIDs",
"action-clean-temporary-files-name": "Eliminar archivos temporales",
"action-clean-temporary-files-description": "Eliminar todos los archivos y carpetas del directorio .temp",
@ -1119,8 +1119,8 @@
"ingredients-natural-language-processor": "Ingredients Natural Language Processor",
"ingredients-natural-language-processor-explanation": "Mealie uses Conditional Random Fields (CRFs) for parsing and processing ingredients. The model used for ingredients is based off a data set of over 100,000 ingredients from a dataset compiled by the New York Times. Note that as the model is trained in English only, you may have varied results when using the model in other languages. This page is a playground for testing the model.",
"ingredients-natural-language-processor-explanation-2": "It's not perfect, but it yields great results in general and is a good starting point for manually parsing ingredients into individual fields. Alternatively, you can also use the \"Brute\" processor that uses a pattern matching technique to identify ingredients.",
"nlp": "NLP",
"brute": "Brute",
"nlp": "PLN",
"brute": "En bruto",
"show-individual-confidence": "Mostrar confianza individual",
"ingredient-text": "Texto del ingrediente",
"average-confident": "{0} Confianza",
@ -1148,31 +1148,31 @@
"user-settings-description": "Administrar preferencias, cambiar contraseña y actualizar correo electrónico",
"api-tokens-description": "Administra tus API Tokens para acceder desde aplicaciones externas",
"group-description": "These items are shared within your group. Editing one of them will change it for the whole group!",
"group-settings": "Group Settings",
"group-settings": "Ajustes de grupo",
"group-settings-description": "Manage your common group settings like mealplan and privacy settings.",
"cookbooks-description": "Manage a collection of recipe categories and generate pages for them.",
"members": "Members",
"members-description": "See who's in your group and manage their permissions.",
"members": "Miembros",
"members-description": "Ver quién está en tu grupo y administrar sus permisos.",
"webhooks-description": "Setup webhooks that trigger on days that you have have mealplan scheduled.",
"notifiers": "Notifiers",
"notifiers": "Notificaciones",
"notifiers-description": "Setup email and push notifications that trigger on specific events.",
"manage-data": "Manage Data",
"manage-data-description": "Manage your Food and Units (more options coming soon)",
"data-migrations": "Data Migrations",
"data-migrations-description": "Migrate your existing data from other applications like Nextcloud Recipes and Chowdown",
"email-sent": "Email Sent",
"error-sending-email": "Error Sending Email",
"personal-information": "Personal Information",
"preferences": "Preferences",
"show-advanced-description": "Show advanced features (API Keys, Webhooks, and Data Management)",
"back-to-profile": "Back to Profile",
"looking-for-privacy-settings": "Looking for Privacy Settings?",
"manage-your-api-tokens": "Manage Your API Tokens",
"manage-user-profile": "Manage User Profile",
"manage-cookbooks": "Manage Cookbooks",
"manage-data": "Administrar datos",
"manage-data-description": "Administra tu Comida y Unidades (próximamente más opciones)",
"data-migrations": "Migración de datos",
"data-migrations-description": "Migrar tus datos existentes de otras aplicaciones como Nextcloud Recipes y Chowdown",
"email-sent": "Email enviado",
"error-sending-email": "Error enviando email",
"personal-information": "Datos Personales",
"preferences": "Preferencias",
"show-advanced-description": "Mostrar características avanzadas (API Keys, Webhooks y Gestión de Datos)",
"back-to-profile": "Volver al perfil",
"looking-for-privacy-settings": "¿Buscas los ajustes de privacidad?",
"manage-your-api-tokens": "Gestiona tus API tokens",
"manage-user-profile": "Administrar Perfil de Usuario",
"manage-cookbooks": "Administrar Recetario",
"manage-members": "Gestionar miembros",
"manage-webhooks": "Manage Webhooks",
"manage-notifiers": "Manage Notifiers",
"manage-webhooks": "Gestionar Webhooks",
"manage-notifiers": "Administrar Notificadores",
"manage-data-migrations": "Administrar Migraciones de Datos"
},
"cookbook": {
@ -1187,7 +1187,7 @@
"require-all-tools": "Requiere todos los utensilios",
"cookbook-name": "Nombre del recetario",
"cookbook-with-name": "Recetario {0}",
"create-a-cookbook": "Create a Cookbook",
"cookbook": "Cookbook"
"create-a-cookbook": "Crear Recetario",
"cookbook": "Recetario"
}
}

View File

@ -200,7 +200,7 @@
"created-on-date": "Gemaakt op {0}",
"unsaved-changes": "Er zijn niet-opgeslagen wijzigingen. Wil je eerst opslaan voordat je vertrekt? Okay om op te slaan, Annuleren om wijzigingen ongedaan te maken.",
"clipboard-copy-failure": "Kopiëren naar klembord mislukt.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?"
"confirm-delete-generic-items": "Weet u zeker dat u de volgende items wilt verwijderen?"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Weet je zeker dat je <b>{groupName}<b/> wil verwijderen?",

View File

@ -707,7 +707,7 @@
"email-configured": "Email настроен",
"email-test-results": "Результаты теста Email",
"ready": "Готово",
"not-ready": "Не готово - Проверьте переменные окружающей среды",
"not-ready": "Не готово - Проверьте переменные окружения",
"succeeded": "Выполнено успешно",
"failed": "Ошибка",
"general-about": "Общая информация",

View File

@ -1,8 +1,8 @@
interface AuthRedirectParams {
interface AdminRedirectParams {
$auth: any
redirect: (path: string) => void
}
export default function ({ $auth, redirect }: AuthRedirectParams) {
export default function ({ $auth, redirect }: AdminRedirectParams) {
// If the user is not an admin redirect to the home page
if (!$auth.user.admin) {
return redirect("/")

View File

@ -0,0 +1,11 @@
interface AdvancedOnlyRedirectParams {
$auth: any
redirect: (path: string) => void
}
export default function ({ $auth, redirect }: AdvancedOnlyRedirectParams) {
// If the user is not allowed to access advanced features redirect to the home page
if (!$auth.user.advanced) {
console.warn("User is not allowed to access advanced features");
return redirect("/")
}
}

View File

@ -0,0 +1,12 @@
interface CanManageRedirectParams {
$auth: any
redirect: (path: string) => void
}
export default function ({ $auth, redirect }: CanManageRedirectParams) {
// If the user is not allowed to manage group settings redirect to the home page
console.log($auth.user)
if (!$auth.user.canManage) {
console.warn("User is not allowed to manage group settings");
return redirect("/")
}
}

View File

@ -0,0 +1,11 @@
interface CanOrganizeRedirectParams {
$auth: any
redirect: (path: string) => void
}
export default function ({ $auth, redirect }: CanOrganizeRedirectParams) {
// If the user is not allowed to organize redirect to the home page
if (!$auth.user.canOrganize) {
console.warn("User is not allowed to organize data");
return redirect("/")
}
}

View File

@ -0,0 +1,12 @@
interface GroupOnlyRedirectParams {
$auth: any
route: any
redirect: (path: string) => void
}
export default function ({ $auth, route, redirect }: GroupOnlyRedirectParams) {
// this can only be used for routes that have a groupSlug parameter (e.g. /g/:groupSlug/...)
if (route.params.groupSlug !== $auth.user.groupSlug) {
redirect("/")
}
}

View File

@ -28,7 +28,7 @@
"date-fns": "^2.29.3",
"fuse.js": "^6.6.2",
"isomorphic-dompurify": "^1.0.0",
"nuxt": "^2.16.0",
"nuxt": "^2.17.3",
"v-jsoneditor": "^1.4.5",
"vue-advanced-cropper": "^1.11.6",
"vuedraggable": "^2.24.3",
@ -56,8 +56,8 @@
},
"resolutions": {
"@nuxtjs/i18n/**/ufo": "0.7.9",
"vue-template-compiler": "2.7.14",
"vue-demi": "^0.13.11",
"vue-template-compiler": "2.7.16",
"postcss-preset-env": "^7.0.0",
"typescript": "^4.9.5"
}

View File

@ -31,7 +31,24 @@
<BaseButton type="button" :loading="generatingToken" create @click.prevent="handlePasswordReset">
{{ $t("user.generate-password-reset-link") }}
</BaseButton>
<AppButtonCopy v-if="resetUrl" :copy-text="resetUrl"></AppButtonCopy>
</div>
<div v-if="resetUrl" class="mb-2">
<v-card-text>
<p class="text-center pb-0">
{{ resetUrl }}
</p>
</v-card-text>
<v-card-actions class="align-center pt-0" style="gap: 4px">
<BaseButton cancel @click="resetUrl = ''"> {{ $t("general.close") }} </BaseButton>
<v-spacer></v-spacer>
<BaseButton v-if="user.email" color="info" class="mr-1" @click="sendResetEmail">
<template #icon>
{{ $globals.icons.email }}
</template>
{{ $t("user.email") }}
</BaseButton>
<AppButtonCopy :icon="false" color="info" :copy-text="resetUrl" />
</v-card-actions>
</div>
<AutoForm v-model="user" :items="userForm" update-mode :disabled-fields="disabledFields" />
@ -46,7 +63,7 @@
<script lang="ts">
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
import { useAdminApi } from "~/composables/api";
import { useAdminApi, useUserApi } from "~/composables/api";
import { useGroups } from "~/composables/use-groups";
import { alert } from "~/composables/use-toast";
import { useUserForm } from "~/composables/use-users";
@ -118,6 +135,17 @@ export default defineComponent({
generatingToken.value = false;
}
const userApi = useUserApi();
async function sendResetEmail() {
if (!user.value?.email) return;
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
if (response && response.status === 200) {
alert.success(i18n.tc("profile.email-sent"));
} else {
alert.error(i18n.tc("profile.error-sending-email"));
}
}
return {
user,
disabledFields,
@ -130,6 +158,7 @@ export default defineComponent({
handlePasswordReset,
resetUrl,
generatingToken,
sendResetEmail,
};
},
});

View File

@ -98,31 +98,23 @@
</template>
<script lang="ts">
import { defineComponent, reactive, ref, useRouter } from "@nuxtjs/composition-api";
import { defineComponent, reactive, ref } from "@nuxtjs/composition-api";
import draggable from "vuedraggable";
import { useCookbooks } from "@/composables/use-group-cookbooks";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
import { ReadCookBook } from "~/lib/api/types/cookbook";
export default defineComponent({
components: { CookbookEditor, draggable },
middleware: ["auth", "group-only"],
setup() {
const { isOwnGroup, loggedIn } = useLoggedInState();
const router = useRouter();
if (!(loggedIn.value && isOwnGroup.value)) {
router.back();
}
const dialogStates = reactive({
create: false,
delete: false,
});
const { cookbooks, actions } = useCookbooks();
// create
const createTarget = ref<ReadCookBook | null>(null);
async function createCookbook() {
@ -146,7 +138,6 @@ export default defineComponent({
dialogStates.delete = false;
deleteTarget.value = null;
}
return {
cookbooks,
actions,

View File

@ -48,46 +48,54 @@
</div>
<v-expansion-panels v-model="panels" multiple>
<v-expansion-panel v-for="(ing, index) in parsedIng" :key="index">
<v-expansion-panel-header class="my-0 py-0" disable-icon-rotate>
<template #default="{ open }">
<v-fade-transition>
<span v-if="!open" key="0"> {{ ing.input }} </span>
</v-fade-transition>
</template>
<template #actions>
<v-icon left :color="isError(ing) ? 'error' : 'success'">
{{ isError(ing) ? $globals.icons.alert : $globals.icons.check }}
</v-icon>
<div class="my-auto" :color="isError(ing) ? 'error-text' : 'success-text'">
{{ ing.confidence ? asPercentage(ing.confidence.average) : "" }}
</div>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content class="pb-0 mb-0">
<RecipeIngredientEditor v-model="parsedIng[index].ingredient" allow-insert-ingredient @insert-ingredient="insertIngredient(index)" @delete="deleteIngredient(index)" />
{{ ing.input }}
<v-card-actions>
<v-spacer />
<BaseButton
v-if="errors[index].unitError && errors[index].unitErrorMessage !== ''"
color="warning"
small
@click="createUnit(ing.ingredient.unit, index)"
>
{{ errors[index].unitErrorMessage }}
</BaseButton>
<BaseButton
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
color="warning"
small
@click="createFood(ing.ingredient.food, index)"
>
{{ errors[index].foodErrorMessage }}
</BaseButton>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>
<draggable
v-if="parsedIng.length > 0"
v-model="parsedIng"
handle=".handle"
:style="{ width: '100%' }"
ghost-class="ghost"
>
<v-expansion-panel v-for="(ing, index) in parsedIng" :key="index">
<v-expansion-panel-header class="my-0 py-0" disable-icon-rotate>
<template #default="{ open }">
<v-fade-transition>
<span v-if="!open" key="0"> {{ ing.input }} </span>
</v-fade-transition>
</template>
<template #actions>
<v-icon left :color="isError(ing) ? 'error' : 'success'">
{{ isError(ing) ? $globals.icons.alert : $globals.icons.check }}
</v-icon>
<div class="my-auto" :color="isError(ing) ? 'error-text' : 'success-text'">
{{ ing.confidence ? asPercentage(ing.confidence.average) : "" }}
</div>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content class="pb-0 mb-0">
<RecipeIngredientEditor v-model="parsedIng[index].ingredient" allow-insert-ingredient @insert-ingredient="insertIngredient(index)" @delete="deleteIngredient(index)" />
{{ ing.input }}
<v-card-actions>
<v-spacer />
<BaseButton
v-if="errors[index].unitError && errors[index].unitErrorMessage !== ''"
color="warning"
small
@click="createUnit(ing.ingredient.unit, index)"
>
{{ errors[index].unitErrorMessage }}
</BaseButton>
<BaseButton
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
color="warning"
small
@click="createFood(ing.ingredient.food, index)"
>
{{ errors[index].foodErrorMessage }}
</BaseButton>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>
</draggable>
</v-expansion-panels>
</v-container>
</v-container>
@ -96,6 +104,7 @@
<script lang="ts">
import { computed, defineComponent, ref, useContext, useRoute, useRouter } from "@nuxtjs/composition-api";
import { invoke, until } from "@vueuse/core";
import draggable from "vuedraggable";
import {
CreateIngredientFood,
CreateIngredientUnit,
@ -122,7 +131,9 @@ interface Error {
export default defineComponent({
components: {
RecipeIngredientEditor,
draggable
},
middleware: ["auth", "group-only"],
setup() {
const { $auth } = useContext();
const panels = ref<number[]>([]);

View File

@ -33,6 +33,7 @@ import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
export default defineComponent({
components: { AdvancedOnly },
middleware: ["auth", "group-only"],
setup() {
const { $auth, $globals, i18n } = useContext();

View File

@ -22,6 +22,7 @@ export default defineComponent({
components: {
RecipeOrganizerPage,
},
middleware: ["auth", "group-only"],
setup() {
const { items, actions } = useCategoryStore();

View File

@ -22,6 +22,7 @@ export default defineComponent({
components: {
RecipeOrganizerPage,
},
middleware: ["auth", "group-only"],
setup() {
const { items, actions } = useTagStore();

View File

@ -17,6 +17,7 @@ import RecipeTimeline from "~/components/Domain/Recipe/RecipeTimeline.vue";
export default defineComponent({
components: { RecipeTimeline },
middleware: ["auth", "group-only"],
setup() {
const api = useUserApi();
const ready = ref<boolean>(false);

View File

@ -22,6 +22,7 @@ export default defineComponent({
components: {
RecipeOrganizerPage,
},
middleware: ["auth", "group-only"],
setup() {
const toolStore = useToolStore();
const dialog = ref(false);

View File

@ -30,6 +30,7 @@
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
export default defineComponent({
middleware: ["auth", "can-organize-only"],
props: {
value: {
type: Boolean,

View File

@ -227,7 +227,6 @@ import { useFoodStore, useLabelStore } from "~/composables/store";
import { VForm } from "~/types/vuetify";
export default defineComponent({
components: { MultiPurposeLabel, RecipeDataAliasManagerDialog },
setup() {
const userApi = useUserApi();

View File

@ -65,6 +65,7 @@ import { useGroupSelf } from "~/composables/use-groups";
import { ReadGroupPreferences } from "~/lib/api/types/group";
export default defineComponent({
middleware: ["auth", "can-manage-only"],
setup() {
const { group, actions: groupActions } = useGroupSelf();

View File

@ -46,6 +46,7 @@ import { isSameDay, addDays, parseISO } from "date-fns";
import { useMealplans } from "~/composables/use-group-mealplan";
export default defineComponent({
middleware: ["auth"],
setup() {
const route = useRoute();
const router = useRouter();

View File

@ -39,6 +39,7 @@
:recipe-id="mealplan.recipe ? mealplan.recipe.id : ''"
class="mb-2"
:route="mealplan.recipe ? true : false"
:rating="mealplan.recipe ? mealplan.recipe.rating : 0"
:slug="mealplan.recipe ? mealplan.recipe.slug : mealplan.title"
:description="mealplan.recipe ? mealplan.recipe.description : mealplan.text"
:name="mealplan.recipe ? mealplan.recipe.name : mealplan.title"

View File

@ -98,6 +98,7 @@ export default defineComponent({
GroupMealPlanRuleForm,
RecipeChips,
},
middleware: ["auth"],
props: {
value: {
type: Boolean,

View File

@ -78,6 +78,7 @@ export default defineComponent({
components: {
UserAvatar,
},
middleware: ["auth"],
setup() {
const api = useUserApi();

View File

@ -85,6 +85,7 @@ const MIGRATIONS = {
};
export default defineComponent({
middleware: ["auth", "advanced-only"],
setup() {
const { $globals, i18n } = useContext();

View File

@ -124,6 +124,7 @@ interface OptionSection {
}
export default defineComponent({
middleware: ["auth", "advanced-only"],
setup() {
const api = useUserApi();

View File

@ -34,6 +34,7 @@ import { useUserApi } from "~/composables/api";
import { ReportOut } from "~/lib/api/types/reports";
export default defineComponent({
middleware: "auth",
setup() {
const route = useRoute();
const id = route.value.params.id;

View File

@ -50,6 +50,7 @@ import GroupWebhookEditor from "~/components/Domain/Group/GroupWebhookEditor.vue
export default defineComponent({
components: { GroupWebhookEditor },
middleware: ["auth", "advanced-only"],
setup() {
const { actions, webhooks } = useGroupWebhooks();

View File

@ -235,6 +235,7 @@ export default defineComponent({
RecipeList,
ShoppingListItemEditor,
},
middleware: "auth",
setup() {
const { $auth, i18n } = useContext();
const preferences = useShoppingListPreferences();
@ -643,15 +644,15 @@ export default defineComponent({
// Create New Item
const createEditorOpen = ref(false);
const createListItemData = ref<ShoppingListItemCreate>(ingredientResetFactory());
const createListItemData = ref<ShoppingListItemCreate>(listItemFactory());
function ingredientResetFactory(): ShoppingListItemCreate {
function listItemFactory(isFood = false): ShoppingListItemCreate {
return {
shoppingListId: id,
checked: false,
position: shoppingList.value?.listItems?.length || 1,
isFood: false,
quantity: 1,
isFood,
quantity: 0,
note: "",
labelId: undefined,
unitId: undefined,
@ -664,6 +665,11 @@ export default defineComponent({
return;
}
if (!createListItemData.value.foodId && !createListItemData.value.note) {
// don't create an empty item
return;
}
loadingCounter.value += 1;
// make sure it's inserted into the end of the list, which may have been updated
@ -674,8 +680,7 @@ export default defineComponent({
loadingCounter.value -= 1;
if (data) {
createListItemData.value = ingredientResetFactory();
createEditorOpen.value = false;
createListItemData.value = listItemFactory(createListItemData.value.isFood || false);
refresh();
}
}

View File

@ -44,6 +44,7 @@ import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
middleware: "auth",
setup() {
const { $auth } = useContext();
const userApi = useUserApi();

View File

@ -14,6 +14,7 @@ import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
components: { RecipeCardSection },
middleware: "auth",
setup() {
const api = useUserApi();
const route = useRoute();

View File

@ -69,6 +69,7 @@ import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
export default defineComponent({
middleware: ["auth", "advanced-only"],
setup() {
const nuxtContext = useContext();

View File

@ -135,6 +135,7 @@ export default defineComponent({
UserAvatar,
UserPasswordStrength,
},
middleware: "auth",
setup() {
const { $auth } = useContext();
const user = computed(() => $auth.user as unknown as UserOut);

View File

@ -113,7 +113,7 @@
<p>{{ $t('profile.group-description') }}</p>
</div>
<v-row tag="section">
<v-col cols="12" sm="12" md="6">
<v-col v-if="$auth.user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.group-settings'), to: `/group` }"
:image="require('~/static/svgs/manage-group-settings.svg')"
@ -162,17 +162,16 @@
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
<AdvancedOnly>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-data'), to: `/group/data/foods` }"
:image="require('~/static/svgs/manage-recipes.svg')"
>
<template #title> {{ $t('profile.manage-data') }} </template>
{{ $t('profile.manage-data-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
<!-- $auth.user.canOrganize should not be null because of the auth middleware -->
<v-col v-if="$auth.user.canOrganize" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: $tc('profile.manage-data'), to: `/group/data/foods` }"
:image="require('~/static/svgs/manage-recipes.svg')"
>
<template #title> {{ $t('profile.manage-data') }} </template>
{{ $t('profile.manage-data-description') }}
</UserProfileLinkCard>
</v-col>
<AdvancedOnly>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
@ -208,6 +207,7 @@ export default defineComponent({
UserAvatar,
StatsCards,
},
middleware: "auth",
scrollToTop: true,
setup() {
const { $auth, i18n } = useContext();

File diff suppressed because it is too large Load Diff

View File

@ -112,7 +112,7 @@ async def system_startup():
logger.info("-----SYSTEM STARTUP----- \n")
logger.info("------APP SETTINGS------")
logger.info(
settings.json(
settings.model_dump_json(
indent=4,
exclude={
"SECRET",

View File

@ -1,7 +1,9 @@
from abc import ABC, abstractmethod
from pathlib import Path
from urllib import parse as urlparse
from pydantic import BaseModel, BaseSettings, PostgresDsn
from pydantic import BaseModel, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class AbstractDBProvider(ABC):
@ -38,15 +40,19 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
POSTGRES_PORT: str = "5432"
POSTGRES_DB: str = "mealie"
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
@property
def db_url(self) -> str:
host = f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}"
return PostgresDsn.build(
scheme="postgresql",
user=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=host,
path=f"/{self.POSTGRES_DB or ''}",
return str(
PostgresDsn.build(
scheme="postgresql",
username=self.POSTGRES_USER,
password=urlparse.quote_plus(self.POSTGRES_PASSWORD),
host=host,
path=f"{self.POSTGRES_DB or ''}",
)
)
@property

View File

@ -1,7 +1,8 @@
import secrets
from pathlib import Path
from pydantic import BaseSettings, NoneStr, validator
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from mealie.core.settings.themes import Theme
@ -55,7 +56,8 @@ class AppSettings(BaseSettings):
SECURITY_USER_LOCKOUT_TIME: int = 24
"time in hours"
@validator("BASE_URL")
@field_validator("BASE_URL")
@classmethod
def remove_trailing_slash(cls, v: str) -> str:
if v and v[-1] == "/":
return v[:-1]
@ -100,12 +102,12 @@ class AppSettings(BaseSettings):
# ===============================================
# Email Configuration
SMTP_HOST: str | None
SMTP_HOST: str | None = None
SMTP_PORT: str | None = "587"
SMTP_FROM_NAME: str | None = "Mealie"
SMTP_FROM_EMAIL: str | None
SMTP_USER: str | None
SMTP_PASSWORD: str | None
SMTP_FROM_EMAIL: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
@property
@ -122,11 +124,11 @@ class AppSettings(BaseSettings):
@staticmethod
def validate_smtp(
host: str | None,
port: str | None,
from_name: str | None,
from_email: str | None,
strategy: str | None,
host: str | None = None,
port: str | None = None,
from_name: str | None = None,
from_email: str | None = None,
strategy: str | None = None,
user: str | None = None,
password: str | None = None,
) -> bool:
@ -143,15 +145,15 @@ class AppSettings(BaseSettings):
# LDAP Configuration
LDAP_AUTH_ENABLED: bool = False
LDAP_SERVER_URL: NoneStr = None
LDAP_SERVER_URL: str | None = None
LDAP_TLS_INSECURE: bool = False
LDAP_TLS_CACERTFILE: NoneStr = None
LDAP_TLS_CACERTFILE: str | None = None
LDAP_ENABLE_STARTTLS: bool = False
LDAP_BASE_DN: NoneStr = None
LDAP_QUERY_BIND: NoneStr = None
LDAP_QUERY_PASSWORD: NoneStr = None
LDAP_USER_FILTER: NoneStr = None
LDAP_ADMIN_FILTER: NoneStr = None
LDAP_BASE_DN: str | None = None
LDAP_QUERY_BIND: str | None = None
LDAP_QUERY_PASSWORD: str | None = None
LDAP_USER_FILTER: str | None = None
LDAP_ADMIN_FILTER: str | None = None
LDAP_ID_ATTRIBUTE: str = "uid"
LDAP_MAIL_ATTRIBUTE: str = "mail"
LDAP_NAME_ATTRIBUTE: str = "name"
@ -173,9 +175,7 @@ class AppSettings(BaseSettings):
# Testing Config
TESTING: bool = False
class Config:
arbitrary_types_allowed = True
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:

View File

@ -1,4 +1,4 @@
from pydantic import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Theme(BaseSettings):
@ -17,6 +17,4 @@ class Theme(BaseSettings):
dark_info: str = "#1976D2"
dark_warning: str = "#FF6D00"
dark_error: str = "#EF5350"
class Config:
env_prefix = "theme_"
model_config = SettingsConfigDict(env_prefix="theme_", extra="allow")

View File

@ -1,3 +1,4 @@
import os
from collections.abc import Callable
from pathlib import Path
from time import sleep
@ -87,7 +88,12 @@ def main():
if max_retry == 0:
raise ConnectionError("Database connection failed - exiting application.")
alembic_cfg = Config(str(PROJECT_DIR / "alembic.ini"))
alembic_cfg_path = os.getenv("ALEMBIC_CONFIG_FILE", default=str(PROJECT_DIR / "alembic.ini"))
if not os.path.isfile(alembic_cfg_path):
raise Exception("Provided alembic config path doesn't exist")
alembic_cfg = Config(alembic_cfg_path)
if db_is_at_head(alembic_cfg):
logger.debug("Migration not needed.")
else:

View File

@ -1,7 +1,7 @@
from functools import wraps
from uuid import UUID
from pydantic import BaseModel, Field, NoneStr
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import select
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
from sqlalchemy.orm.mapper import Mapper
@ -21,7 +21,7 @@ class AutoInitConfig(BaseModel):
Config class for `auto_init` decorator.
"""
get_attr: NoneStr = None
get_attr: str | None = None
exclude: set = Field(default_factory=_default_exclusion)
# auto_create: bool = False
@ -31,16 +31,16 @@ def _get_config(relation_cls: type[SqlAlchemyBase]) -> AutoInitConfig:
Returns the config for the given class.
"""
cfg = AutoInitConfig()
cfgKeys = cfg.dict().keys()
cfgKeys = cfg.model_dump().keys()
# Get the config for the class
try:
class_config: AutoInitConfig = relation_cls.Config
class_config: ConfigDict = relation_cls.model_config
except AttributeError:
return cfg
# Map all matching attributes in Config to all AutoInitConfig attributes
for attr in dir(class_config):
for attr in class_config:
if attr in cfgKeys:
setattr(cfg, attr, getattr(class_config, attr))
setattr(cfg, attr, class_config[attr])
return cfg
@ -97,7 +97,7 @@ def handle_one_to_many_list(
updated_elems.append(existing_elem)
new_elems = [safe_call(relation_cls, elem, session=session) for elem in elems_to_create]
new_elems = [safe_call(relation_cls, elem.copy(), session=session) for elem in elems_to_create]
return new_elems + updated_elems
@ -164,7 +164,7 @@ def auto_init(): # sourcery no-metrics
setattr(self, key, instances)
elif relation_dir == ONETOMANY:
instance = safe_call(relation_cls, val, session=session)
instance = safe_call(relation_cls, val.copy() if val else None, session=session)
setattr(self, key, instance)
elif relation_dir == MANYTOONE and not use_list:

View File

@ -29,12 +29,15 @@ def get_valid_call(func: Callable, args_dict) -> dict:
return {k: v for k, v in args_dict.items() if k in valid_args}
def safe_call(func, dict_args: dict, **kwargs) -> Any:
def safe_call(func, dict_args: dict | None, **kwargs) -> Any:
"""
Safely calls the supplied function with the supplied dictionary of arguments.
by removing any invalid arguments.
"""
if dict_args is None:
dict_args = {}
if kwargs:
dict_args.update(kwargs)

View File

@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Optional
import sqlalchemy as sa
import sqlalchemy.orm as orm
from pydantic import ConfigDict
from sqlalchemy import select
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm.session import Session
@ -79,9 +80,8 @@ class Group(SqlAlchemyBase, BaseMixins):
ingredient_foods: Mapped[list["IngredientFoodModel"]] = orm.relationship("IngredientFoodModel", **common_args)
tools: Mapped[list["Tool"]] = orm.relationship("Tool", **common_args)
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
class Config:
exclude = {
model_config = ConfigDict(
exclude={
"users",
"webhooks",
"shopping_lists",
@ -91,6 +91,7 @@ class Group(SqlAlchemyBase, BaseMixins):
"mealplans",
"data_exports",
}
)
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,6 +1,7 @@
from datetime import datetime
from typing import TYPE_CHECKING
from pydantic import ConfigDict
from sqlalchemy import ForeignKey, orm
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
@ -47,9 +48,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
# Relationships
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="group_reports", single_parent=True)
class Config:
exclude = ["entries"]
model_config = ConfigDict(exclude=["entries"])
@auto_init()
def __init__(self, **_) -> None:

View File

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING, Optional
from pydantic import ConfigDict
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, UniqueConstraint, orm
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import Mapped, mapped_column
@ -69,9 +70,7 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
recipe_references: Mapped[list[ShoppingListItemRecipeReference]] = orm.relationship(
ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan"
)
class Config:
exclude = {"id", "label", "food", "unit"}
model_config = ConfigDict(exclude={"id", "label", "food", "unit"})
@api_extras
@auto_init()
@ -91,9 +90,7 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
)
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
class Config:
exclude = {"id", "recipe"}
model_config = ConfigDict(exclude={"id", "recipe"})
@auto_init()
def __init__(self, **_) -> None:
@ -112,9 +109,7 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
"MultiPurposeLabel", back_populates="shopping_lists_label_settings"
)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class Config:
exclude = {"label"}
model_config = ConfigDict(exclude={"label"})
@auto_init()
def __init__(self, **_) -> None:
@ -146,9 +141,7 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
collection_class=ordering_list("position"),
)
extras: Mapped[list[ShoppingListExtras]] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
class Config:
exclude = {"id", "list_items"}
model_config = ConfigDict(exclude={"id", "list_items"})
@api_extras
@auto_init()

View File

@ -1,3 +1,4 @@
from pydantic import ConfigDict
from sqlalchemy import ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column
@ -28,12 +29,12 @@ class RecipeInstruction(SqlAlchemyBase):
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
RecipeIngredientRefLink, cascade="all, delete-orphan"
)
class Config:
exclude = {
model_config = ConfigDict(
exclude={
"id",
"ingredient_references",
}
)
@auto_init()
def __init__(self, ingredient_references, session, **_) -> None:

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
import sqlalchemy as sa
import sqlalchemy.orm as orm
from pydantic import ConfigDict
from sqlalchemy import event
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import Mapped, mapped_column, validates
@ -134,10 +135,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
# Automatically updated by sqlalchemy event, do not write to this manually
name_normalized: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
description_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
class Config:
get_attr = "slug"
exclude = {
model_config = ConfigDict(
get_attr="slug",
exclude={
"assets",
"notes",
"nutrition",
@ -146,7 +146,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
"settings",
"comments",
"timeline_events",
}
},
)
@validates("name")
def validate_name(self, _, name):

View File

@ -2,6 +2,7 @@ import enum
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from pydantic import ConfigDict
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, mapped_column
@ -84,9 +85,8 @@ class User(SqlAlchemyBase, BaseMixins):
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
)
class Config:
exclude = {
model_config = ConfigDict(
exclude={
"password",
"admin",
"can_manage",
@ -94,6 +94,7 @@ class User(SqlAlchemyBase, BaseMixins):
"can_organize",
"group",
}
)
@hybrid_property
def group_slug(self) -> str:

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} is opgedateer, {url}",
"generic-duplicated": "{name} is gekopieer",
"generic-deleted": "{name} is verwyder"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} تم تحديثه، {url}",
"generic-duplicated": "تم تكرار {name}",
"generic-deleted": "تم حذف {name}"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} беше актуализирано, {url}",
"generic-duplicated": "{name} е дублицирано",
"generic-deleted": "{name} беше изтрито"
},
"datetime": {
"year": "година|години",
"day": "ден|дни",
"hour": "час|часове",
"minute": "минута|минути",
"second": "секунда|секунди",
"millisecond": "милисекунда|милисекунди",
"microsecond": "микросекунда|микросекунди"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} ha estat actualitzat, {url}",
"generic-duplicated": "S'ha duplicat {name}",
"generic-deleted": "{name} ha estat eliminat"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} byl aktualizován, {url}",
"generic-duplicated": "{name} byl duplikován",
"generic-deleted": "{name} byl odstraněn"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} er blevet opdateret, {url}",
"generic-duplicated": "{name} er blevet dublikeret",
"generic-deleted": "{name} er blevet slettet"
},
"datetime": {
"year": "år|år",
"day": "dag|dage",
"hour": "time|timer",
"minute": "minut|minutter",
"second": "sekund|sekunder",
"millisecond": "millisekund|millisekunder",
"microsecond": "mikrosekund|mikrosekunder"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} wurde aktualisiert, {url}",
"generic-duplicated": "{name} wurde dupliziert",
"generic-deleted": "{name} wurde gelöscht"
},
"datetime": {
"year": "jahr|jahre",
"day": "tag|tage",
"hour": "stunde|stunden",
"minute": "minute|minuten",
"second": "sekunde|sekunden",
"millisecond": "millisekunde|millisekunden",
"microsecond": "mikrosekunde|mikrosekunden"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "Se ha actualizado {name}, {url}",
"generic-duplicated": "Se ha duplicado {name}",
"generic-deleted": "Se ha eliminado {name}"
},
"datetime": {
"year": "año|años",
"day": "día|días",
"hour": "hora|horas",
"minute": "minuto|minutos",
"second": "segundo|segundos",
"millisecond": "milisegundo|milisegundos",
"microsecond": "microsegundo|microsegundos"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} on päivitetty, {url}",
"generic-duplicated": "{name} on kahdennettu",
"generic-deleted": "{name} on poistettu"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} a été mis à jour, {url}",
"generic-duplicated": "{name} a été dupliqué",
"generic-deleted": "{name} a été supprimé"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} a été mis à jour, {url}",
"generic-duplicated": "{name} a été dupliqué",
"generic-deleted": "{name} a été supprimé"
},
"datetime": {
"year": "année|années",
"day": "jour|jours",
"hour": "heure|heures",
"minute": "minute|minutes",
"second": "seconde|secondes",
"millisecond": "milliseconde|millisecondes",
"microsecond": "microseconde|microsecondes"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} עודכן, {url}",
"generic-duplicated": "{name} שוכפל",
"generic-deleted": "{name} נמחק"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} je ažuriran, {url}",
"generic-duplicated": "{name} je dupliciran",
"generic-deleted": "{name} je obrisan"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} frissítve, {url}",
"generic-duplicated": "{name} duplikálva",
"generic-deleted": "{name} törölve lett"
},
"datetime": {
"year": "év",
"day": "nap",
"hour": "óra",
"minute": "perc",
"second": "másodperc",
"millisecond": "ezredmásodperc",
"microsecond": "mikroszekundum"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} è stato aggiornato, {url}",
"generic-duplicated": "{name} è stato duplicato",
"generic-deleted": "{name} è stato eliminato"
},
"datetime": {
"year": "anno|anni",
"day": "giorno|giorni",
"hour": "ora|ore",
"minute": "minuto|minuti",
"second": "secondo|secondi",
"millisecond": "millisecondo|millisecondi",
"microsecond": "microsecondo|microsecondi"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} atnaujintas, {url}",
"generic-duplicated": "{name} buvo nukopijuotas",
"generic-deleted": "{name} ištrintas"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} is bijgewerkt, {url}",
"generic-duplicated": "(naam) is gekopieerd",
"generic-deleted": "{name} is verwijderd"
},
"datetime": {
"year": "jaar|jaren",
"day": "dag|dagen",
"hour": "uur|uren",
"minute": "minuut|minuten",
"second": "seconde|seconden",
"millisecond": "milliseconde milliseconden",
"microsecond": "microseconde microseconden"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} har blitt oppdatert, {url}",
"generic-duplicated": "{name} har blitt duplisert",
"generic-deleted": "{name} har blitt slettet"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} został zaktualizowany. {url}",
"generic-duplicated": "{name} został zduplikowany",
"generic-deleted": "{name} został usunięty"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} foi atualizado, {url}",
"generic-duplicated": "{name} foi duplicada",
"generic-deleted": "{name} foi excluído"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} foi atualizado, {url}",
"generic-duplicated": "{name} foi duplicado",
"generic-deleted": "{name} foi removido"
},
"datetime": {
"year": "ano|anos",
"day": "dia|dias",
"hour": "hora|horas",
"minute": "minuto|minutos",
"second": "segundo|segundos",
"millisecond": "milissegundo|milissegundos",
"microsecond": "microssegundo|microssegundos"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} a fost actualizat, {url}",
"generic-duplicated": "{name} a fost duplicat",
"generic-deleted": "{name} a fost șters"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} был обновлен, {url}",
"generic-duplicated": "Копия {name} была создана",
"generic-deleted": "{name} был удален"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} bol aktualizovaný, {url}",
"generic-duplicated": "{name} bol duplikovaný",
"generic-deleted": "{name} bol vymazaný"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} je bil posodobljen, {url}",
"generic-duplicated": "{name} je bilo podvojeno",
"generic-deleted": "{name} je bil izbrisan"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} је ажурирано, {url}",
"generic-duplicated": "{name} је дуплиран",
"generic-deleted": "{name} је обрисан"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} har uppdaterats, {url}",
"generic-duplicated": "{name} har duplicerats",
"generic-deleted": "{name} har tagits bort"
},
"datetime": {
"year": "år|år",
"day": "dag|dagar",
"hour": "timme|timmar",
"minute": "minut|minuter",
"second": "sekund|sekunder",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} güncellendi, {url}",
"generic-duplicated": "{name} yinelendi",
"generic-deleted": "{name} silindi"
},
"datetime": {
"year": "yıl|yıllar",
"day": "gün|günler",
"hour": "saat|saatler",
"minute": "dakika|dakikalar",
"second": "saniye|saniyeler",
"millisecond": "milisaniye|milisaniyeler",
"microsecond": "mikrosaniye|mikrosaniyeler"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} оновлено, {url}",
"generic-duplicated": "{name} дубльовано",
"generic-deleted": "{name} видалено"
},
"datetime": {
"year": "рік|роки",
"day": "день|дні",
"hour": "година|години",
"minute": "хвилина|хвилини",
"second": "секунда|секунди",
"millisecond": "мілісекунда|мілісекунди",
"microsecond": "мікросекунда|мікросекунди"
}
}

View File

@ -31,5 +31,14 @@
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-duplicated": "{name} has been duplicated",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
}
}

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