mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-18 23:15:56 -05:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23aa61f17 | ||
|
|
cd39d0c4cb | ||
|
|
20e2d4e1a1 | ||
|
|
c09cc5a323 | ||
|
|
6d7b6bccab | ||
|
|
91fea086e5 | ||
|
|
e2fbe118a7 | ||
|
|
904e6b7d82 | ||
|
|
5aafb56c4f | ||
|
|
b4740d291d | ||
|
|
fc6dc34ace | ||
|
|
73d86f6f6b | ||
|
|
8e225ee796 | ||
|
|
ced233d361 | ||
|
|
b173172e6c | ||
|
|
a66db96eb5 | ||
|
|
dfd5abfb5d | ||
|
|
e2ae5cb5b6 | ||
|
|
634aa5cd25 | ||
|
|
23c7bd7e3d | ||
|
|
9c1ee972c9 | ||
|
|
1b9023c8c0 | ||
|
|
3a37cd6959 | ||
|
|
8da0d010a5 | ||
|
|
37f7f770a8 | ||
|
|
1cebbefd88 | ||
|
|
d55149b904 | ||
|
|
fad7acadfc | ||
|
|
a539c6cd2e | ||
|
|
7b5502d019 | ||
|
|
26d9d8fe24 | ||
|
|
b64f14aaae | ||
|
|
9b686ecd2b | ||
|
|
a956a638f4 | ||
|
|
c9d9e6822e | ||
|
|
4a563b76ad | ||
|
|
73f97c2cca | ||
|
|
75e3c99d72 | ||
|
|
217ddd8814 | ||
|
|
f2cc8dc922 | ||
|
|
b8329def91 | ||
|
|
2ae7dc3b82 | ||
|
|
510a63a71f | ||
|
|
14433819c3 | ||
|
|
96a9dbccb6 | ||
|
|
cfe20214e5 | ||
|
|
eef54879fe | ||
|
|
c789ecf0ba | ||
|
|
008f55e725 | ||
|
|
bcbe32f503 |
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@@ -37,6 +37,17 @@ jobs:
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hkotel/mealie
|
||||
ghcr.io/${{ github.repository }}
|
||||
# Overwrite the image.version label with our tag
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ inputs.tag }}
|
||||
|
||||
- name: Retrieve Python package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -57,5 +68,6 @@ jobs:
|
||||
hkotel/mealie:${{ inputs.tag }}
|
||||
ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||
${{ inputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
COMMIT=${{ github.sha }}
|
||||
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.0
|
||||
rev: v0.15.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import dotenv
|
||||
@@ -10,6 +11,7 @@ from pydantic import ConfigDict
|
||||
from requests import Response
|
||||
from utils import CodeDest, CodeKeys, inject_inline, log
|
||||
|
||||
from mealie.lang.locale_config import LOCALE_CONFIG, LocalePluralFoodHandling, LocaleTextDirection
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
BASE = pathlib.Path(__file__).parent.parent.parent
|
||||
@@ -17,57 +19,6 @@ BASE = pathlib.Path(__file__).parent.parent.parent
|
||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocaleData:
|
||||
name: str
|
||||
dir: str = "ltr"
|
||||
|
||||
|
||||
LOCALE_DATA: dict[str, LocaleData] = {
|
||||
"af-ZA": LocaleData(name="Afrikaans (Afrikaans)"),
|
||||
"ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"),
|
||||
"bg-BG": LocaleData(name="Български (Bulgarian)"),
|
||||
"ca-ES": LocaleData(name="Català (Catalan)"),
|
||||
"cs-CZ": LocaleData(name="Čeština (Czech)"),
|
||||
"da-DK": LocaleData(name="Dansk (Danish)"),
|
||||
"de-DE": LocaleData(name="Deutsch (German)"),
|
||||
"el-GR": LocaleData(name="Ελληνικά (Greek)"),
|
||||
"en-GB": LocaleData(name="British English"),
|
||||
"en-US": LocaleData(name="American English"),
|
||||
"es-ES": LocaleData(name="Español (Spanish)"),
|
||||
"et-EE": LocaleData(name="Eesti (Estonian)"),
|
||||
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
||||
"fr-BE": LocaleData(name="Belge (Belgian)"),
|
||||
"fr-CA": LocaleData(name="Français canadien (Canadian French)"),
|
||||
"fr-FR": LocaleData(name="Français (French)"),
|
||||
"gl-ES": LocaleData(name="Galego (Galician)"),
|
||||
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
||||
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
|
||||
"hu-HU": LocaleData(name="Magyar (Hungarian)"),
|
||||
"is-IS": LocaleData(name="Íslenska (Icelandic)"),
|
||||
"it-IT": LocaleData(name="Italiano (Italian)"),
|
||||
"ja-JP": LocaleData(name="日本語 (Japanese)"),
|
||||
"ko-KR": LocaleData(name="한국어 (Korean)"),
|
||||
"lt-LT": LocaleData(name="Lietuvių (Lithuanian)"),
|
||||
"lv-LV": LocaleData(name="Latviešu (Latvian)"),
|
||||
"nl-NL": LocaleData(name="Nederlands (Dutch)"),
|
||||
"no-NO": LocaleData(name="Norsk (Norwegian)"),
|
||||
"pl-PL": LocaleData(name="Polski (Polish)"),
|
||||
"pt-BR": LocaleData(name="Português do Brasil (Brazilian Portuguese)"),
|
||||
"pt-PT": LocaleData(name="Português (Portuguese)"),
|
||||
"ro-RO": LocaleData(name="Română (Romanian)"),
|
||||
"ru-RU": LocaleData(name="Pусский (Russian)"),
|
||||
"sk-SK": LocaleData(name="Slovenčina (Slovak)"),
|
||||
"sl-SI": LocaleData(name="Slovenščina (Slovenian)"),
|
||||
"sr-SP": LocaleData(name="српски (Serbian)"),
|
||||
"sv-SE": LocaleData(name="Svenska (Swedish)"),
|
||||
"tr-TR": LocaleData(name="Türkçe (Turkish)"),
|
||||
"uk-UA": LocaleData(name="Українська (Ukrainian)"),
|
||||
"vi-VN": LocaleData(name="Tiếng Việt (Vietnamese)"),
|
||||
"zh-CN": LocaleData(name="简体中文 (Chinese simplified)"),
|
||||
"zh-TW": LocaleData(name="繁體中文 (Chinese traditional)"),
|
||||
}
|
||||
|
||||
LOCALE_TEMPLATE = """// This Code is auto generated by gen_ts_locales.py
|
||||
export const LOCALES = [{% for locale in locales %}
|
||||
{
|
||||
@@ -75,6 +26,7 @@ export const LOCALES = [{% for locale in locales %}
|
||||
value: "{{ locale.locale }}",
|
||||
progress: {{ locale.progress }},
|
||||
dir: "{{ locale.dir }}",
|
||||
pluralFoodHandling: "{{ locale.plural_food_handling }}",
|
||||
},{% endfor %}
|
||||
];
|
||||
|
||||
@@ -87,10 +39,11 @@ class TargetLanguage(MealieModel):
|
||||
id: str
|
||||
name: str
|
||||
locale: str
|
||||
dir: str = "ltr"
|
||||
dir: LocaleTextDirection = LocaleTextDirection.LTR
|
||||
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
|
||||
threeLettersCode: str
|
||||
twoLettersCode: str
|
||||
progress: float = 0.0
|
||||
progress: int = 0
|
||||
|
||||
|
||||
class CrowdinApi:
|
||||
@@ -117,43 +70,15 @@ class CrowdinApi:
|
||||
def get_languages(self) -> list[TargetLanguage]:
|
||||
response = self.get_project()
|
||||
tls = response.json()["data"]["targetLanguages"]
|
||||
return [TargetLanguage(**t) for t in tls]
|
||||
|
||||
models = [TargetLanguage(**t) for t in tls]
|
||||
|
||||
models.insert(
|
||||
0,
|
||||
TargetLanguage(
|
||||
id="en-US",
|
||||
name="English",
|
||||
locale="en-US",
|
||||
dir="ltr",
|
||||
threeLettersCode="en",
|
||||
twoLettersCode="en",
|
||||
progress=100,
|
||||
),
|
||||
)
|
||||
|
||||
progress: list[dict] = self.get_progress()["data"]
|
||||
|
||||
for model in models:
|
||||
if model.locale in LOCALE_DATA:
|
||||
locale_data = LOCALE_DATA[model.locale]
|
||||
model.name = locale_data.name
|
||||
model.dir = locale_data.dir
|
||||
|
||||
for p in progress:
|
||||
if p["data"]["languageId"] == model.id:
|
||||
model.progress = p["data"]["translationProgress"]
|
||||
|
||||
models.sort(key=lambda x: x.locale, reverse=True)
|
||||
return models
|
||||
|
||||
def get_progress(self) -> dict:
|
||||
def get_progress(self) -> dict[str, int]:
|
||||
response = requests.get(
|
||||
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
|
||||
headers=self.headers,
|
||||
)
|
||||
return response.json()
|
||||
data = response.json()["data"]
|
||||
return {p["data"]["languageId"]: p["translationProgress"] for p in data}
|
||||
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
@@ -195,8 +120,8 @@ def inject_nuxt_values():
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
match_data = LOCALE_DATA.get(match.stem)
|
||||
match_dir = match_data.dir if match_data else "ltr"
|
||||
match_data = LOCALE_CONFIG.get(match.stem)
|
||||
match_dir = match_data.dir if match_data else LocaleTextDirection.LTR
|
||||
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
|
||||
all_langs.append(lang_string)
|
||||
@@ -221,9 +146,82 @@ def inject_registration_validation_values():
|
||||
inject_inline(reg_valid, CodeKeys.nuxt_local_messages, all_langs)
|
||||
|
||||
|
||||
def _get_local_models() -> list[TargetLanguage]:
|
||||
return [
|
||||
TargetLanguage(
|
||||
id=locale,
|
||||
name=data.name,
|
||||
locale=locale,
|
||||
threeLettersCode=locale.split("-")[-1],
|
||||
twoLettersCode=locale.split("-")[-1],
|
||||
)
|
||||
for locale, data in LOCALE_CONFIG.items()
|
||||
if locale != "en-US" # Crowdin doesn't include this, so we manually inject it later
|
||||
]
|
||||
|
||||
|
||||
def _get_local_progress() -> dict[str, int]:
|
||||
with open(CodeDest.use_locales) as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract the array content between [ and ]
|
||||
match = re.search(r"export const LOCALES = (\[.*?\]);", content, re.DOTALL)
|
||||
if not match:
|
||||
raise ValueError("Could not find LOCALES array in file")
|
||||
|
||||
# Convert JS to JSON
|
||||
array_content = match.group(1)
|
||||
|
||||
# Replace unquoted keys with quoted keys for valid JSON
|
||||
# This converts: { name: "value" } to { "name": "value" }
|
||||
json_str = re.sub(r"([,\{\s])([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'\1"\2":', array_content)
|
||||
|
||||
# Remove trailing commas before } and ]
|
||||
json_str = re.sub(r",(\s*[}\]])", r"\1", json_str)
|
||||
|
||||
locales = json.loads(json_str)
|
||||
return {locale["value"]: locale["progress"] for locale in locales}
|
||||
|
||||
|
||||
def get_languages() -> list[TargetLanguage]:
|
||||
if API_KEY:
|
||||
api = CrowdinApi(None)
|
||||
models = api.get_languages()
|
||||
progress = api.get_progress()
|
||||
else:
|
||||
log.warning("CROWDIN_API_KEY is not set, using local lanugages instead")
|
||||
log.warning("DOUBLE CHECK the output!!! Do not overwrite with bad local locale data!")
|
||||
models = _get_local_models()
|
||||
progress = _get_local_progress()
|
||||
|
||||
models.insert(
|
||||
0,
|
||||
TargetLanguage(
|
||||
id="en-US",
|
||||
name="English",
|
||||
locale="en-US",
|
||||
dir=LocaleTextDirection.LTR,
|
||||
plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT,
|
||||
threeLettersCode="en",
|
||||
twoLettersCode="en",
|
||||
progress=100,
|
||||
),
|
||||
)
|
||||
|
||||
for model in models:
|
||||
if model.locale in LOCALE_CONFIG:
|
||||
locale_data = LOCALE_CONFIG[model.locale]
|
||||
model.name = locale_data.name
|
||||
model.dir = locale_data.dir
|
||||
model.plural_food_handling = locale_data.plural_food_handling
|
||||
model.progress = progress.get(model.id, model.progress)
|
||||
|
||||
models.sort(key=lambda x: x.locale, reverse=True)
|
||||
return models
|
||||
|
||||
|
||||
def generate_locales_ts_file():
|
||||
api = CrowdinApi(None)
|
||||
models = api.get_languages()
|
||||
models = get_languages()
|
||||
tmpl = Template(LOCALE_TEMPLATE)
|
||||
rendered = tmpl.render(locales=models)
|
||||
|
||||
@@ -233,10 +231,6 @@ def generate_locales_ts_file():
|
||||
|
||||
|
||||
def main():
|
||||
if API_KEY is None or API_KEY == "":
|
||||
log.error("CROWDIN_API_KEY is not set")
|
||||
return
|
||||
|
||||
generate_locales_ts_file()
|
||||
inject_nuxt_values()
|
||||
inject_registration_validation_values()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:24@sha256:1de022d8459f896fff2e7b865823699dc7a8d5567507e8b87b14a7442e07f206 \
|
||||
FROM node:24@sha256:00e9195ebd49985a6da8921f419978d85dfe354589755192dc090425ce4da2f7 \
|
||||
AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
@@ -111,7 +111,6 @@ RUN . $VENV_PATH/bin/activate \
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base AS production
|
||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
|
||||
|
||||
@@ -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.10.2`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.11.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
|
||||
|
||||
|
||||
@@ -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.10.2 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.11.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.10.2 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.11.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
max-width: 950px !important;
|
||||
}
|
||||
|
||||
.lg-container {
|
||||
max-width: 1100px !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-application {
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = computed(() => auth.user.value?.groupSlug);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const sections = ref([
|
||||
|
||||
@@ -73,11 +73,11 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
@@ -88,11 +88,11 @@ const router = useRouter();
|
||||
const book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user.value && book.value?.householdId)) {
|
||||
if (!(auth.user.value && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth.user.value.householdId === book.value.householdId;
|
||||
return auth.user.value.householdId === book.value.householdId;
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ const MEAL_DAY_OPTIONS = [
|
||||
];
|
||||
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
console.warn("handleQueryFilterInput called with value:", value);
|
||||
queryFilterString.value = value || "";
|
||||
}
|
||||
|
||||
@@ -114,7 +113,7 @@ const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "last_made",
|
||||
label: i18n.t("general.last-made"),
|
||||
type: "date",
|
||||
type: "relativeDate",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<v-select
|
||||
v-if="field.type !== 'boolean'"
|
||||
:model-value="field.relationalOperatorValue"
|
||||
:items="field.relationalOperatorOptions"
|
||||
:items="field.relationalOperatorChoices"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="underlined"
|
||||
@@ -129,9 +129,9 @@
|
||||
:class="config.col.class"
|
||||
>
|
||||
<v-select
|
||||
v-if="field.fieldOptions"
|
||||
v-if="field.fieldChoices"
|
||||
:model-value="field.values"
|
||||
:items="field.fieldOptions"
|
||||
:items="field.fieldChoices"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
multiple
|
||||
@@ -169,23 +169,39 @@
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
:model-value="$d(safeNewDate(field.value + 'T00:00:00'))"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
class="date-input"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
|
||||
:model-value="safeNewDate(field.value + 'T00:00:00')"
|
||||
hide-header
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:local="$i18n.locale"
|
||||
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
|
||||
/>
|
||||
</v-menu>
|
||||
<!--
|
||||
Relative dates are assumed to be negative intervals with a unit of days.
|
||||
The input is a *positive*, interpreted internally as a *negative* offset.
|
||||
-->
|
||||
<v-number-input
|
||||
v-else-if="field.type === 'relativeDate'"
|
||||
:model-value="parseRelativeDateOffset(field.value)"
|
||||
:suffix="$t('query-filter.dates.days-ago', parseRelativeDateOffset(field.value))"
|
||||
variant="underlined"
|
||||
control-variant="stacked"
|
||||
density="compact"
|
||||
inset
|
||||
:min="0"
|
||||
:precision="0"
|
||||
class="date-input"
|
||||
@update:model-value="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
v-model="field.organizers"
|
||||
@@ -319,7 +335,13 @@ import { useDebounceFn } from "@vueuse/core";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
|
||||
import type {
|
||||
LogicalOperator,
|
||||
QueryFilterJSON,
|
||||
QueryFilterJSONPart,
|
||||
RelationalKeyword,
|
||||
RelationalOperator,
|
||||
} from "~/lib/api/types/non-generated";
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { useUserStore } from "~/composables/store/use-user-store";
|
||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||
@@ -341,7 +363,14 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
const {
|
||||
logOps,
|
||||
placeholderKeywords,
|
||||
getRelOps,
|
||||
buildQueryFilterString,
|
||||
getFieldFromFieldDef,
|
||||
isOrganizerType,
|
||||
} = useQueryFilterBuilder();
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
@@ -396,16 +425,29 @@ function setField(index: number, fieldLabel: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
|
||||
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldChoices !== fields.value[index].fieldChoices);
|
||||
const updatedField = { ...fields.value[index], ...fieldDef };
|
||||
|
||||
// we have to set this explicitly since it might be undefined
|
||||
updatedField.fieldOptions = fieldDef.fieldOptions;
|
||||
updatedField.fieldChoices = fieldDef.fieldChoices;
|
||||
|
||||
fields.value[index] = {
|
||||
...getFieldFromFieldDef(updatedField, resetValue),
|
||||
id: fields.value[index].id, // keep the id
|
||||
};
|
||||
|
||||
// Defaults
|
||||
switch (fields.value[index].type) {
|
||||
case "date":
|
||||
fields.value[index].value = safeNewDate("");
|
||||
break;
|
||||
case "relativeDate":
|
||||
fields.value[index].value = "$NOW-30d";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
@@ -425,12 +467,21 @@ function setLogicalOperatorValue(field: FieldWithId, index: number, value: Logic
|
||||
}
|
||||
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
const relOps = getRelOps(field.type);
|
||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||
}
|
||||
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
state.datePickers[index] = false;
|
||||
fields.value[index].value = value;
|
||||
|
||||
if (field.type === "relativeDate") {
|
||||
// Value is set to an int representing the offset from $NOW
|
||||
// Values are assumed to be negative offsets ('-') with a unit of days ('d')
|
||||
fields.value[index].value = `$NOW-${Math.abs(value)}d`;
|
||||
}
|
||||
else {
|
||||
fields.value[index].value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
@@ -448,12 +499,7 @@ function removeField(index: number) {
|
||||
state.datePickers.splice(index, 1);
|
||||
}
|
||||
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
/* newFields.forEach((field, index) => {
|
||||
const updatedField = getFieldFromFieldDef(field);
|
||||
fields.value[index] = updatedField; // recursive!!!
|
||||
}); */
|
||||
|
||||
const fieldsUpdater = useDebounceFn(() => {
|
||||
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
||||
if (qf) {
|
||||
console.debug(`Set query filter: ${qf}`);
|
||||
@@ -519,6 +565,9 @@ async function initializeFields() {
|
||||
...getFieldFromFieldDef(fieldDef),
|
||||
id: useUid(),
|
||||
};
|
||||
|
||||
const relOps = getRelOps(field.type);
|
||||
|
||||
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
|
||||
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
|
||||
field.logicalOperator = part.logicalOperator
|
||||
@@ -527,12 +576,15 @@ async function initializeFields() {
|
||||
field.relationalOperatorValue = part.relationalOperator
|
||||
? relOps.value[part.relationalOperator]
|
||||
: field.relationalOperatorValue;
|
||||
field.relationalOperatorValue = part.relationalOperator
|
||||
? relOps.value[part.relationalOperator]
|
||||
: field.relationalOperatorValue;
|
||||
|
||||
if (field.leftParenthesis || field.rightParenthesis) {
|
||||
state.showAdvanced = true;
|
||||
}
|
||||
|
||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||
if (typeof part.value === "string") {
|
||||
field.values = part.value ? [part.value] : [];
|
||||
}
|
||||
@@ -601,7 +653,7 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
relationalOperator: field.relationalOperatorValue?.value,
|
||||
};
|
||||
|
||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||
part.value = field.values.map(value => value.toString());
|
||||
}
|
||||
else if (field.type === "boolean") {
|
||||
@@ -619,6 +671,50 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
return qfJSON;
|
||||
}
|
||||
|
||||
function safeNewDate(input: string): Date {
|
||||
const date = new Date(input);
|
||||
if (isNaN(date.getTime())) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a relative date string offset (e.g. $NOW-30d --> 30)
|
||||
*
|
||||
* Currently only values with a negative offset ('-') and a unit of days ('d') are supported
|
||||
*/
|
||||
function parseRelativeDateOffset(value: string): number {
|
||||
const defaultVal = 30;
|
||||
if (!value) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!value.startsWith(placeholderKeywords.value["$NOW"].value)) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
const remainder = value.slice(placeholderKeywords.value["$NOW"].value.length);
|
||||
if (!remainder.startsWith("-")) {
|
||||
throw new Error("Invalid operator (not '-')");
|
||||
}
|
||||
|
||||
if (remainder.slice(-1) !== "d") {
|
||||
throw new Error("Invalid unit (not 'd')");
|
||||
}
|
||||
|
||||
// Slice off sign and unit
|
||||
return parseInt(remainder.slice(1, -1));
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Unable to parse relative date offset from '${value}': ${error}`);
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
const multiple = fields.value.length > 1;
|
||||
const adv = state.showAdvanced;
|
||||
@@ -689,4 +785,13 @@ const config = computed(() => {
|
||||
.bg-light {
|
||||
background-color: rgba(255, 255, 255, var(--bg-opactity));
|
||||
}
|
||||
|
||||
:deep(.date-input input) {
|
||||
text-align: end;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
:deep(.date-input .v-field__field) {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -130,11 +130,11 @@ defineEmits<{
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
|
||||
@@ -160,11 +160,11 @@ defineEmits<{
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
|
||||
@@ -219,7 +219,7 @@ const EVENTS = {
|
||||
shuffle: "shuffle",
|
||||
};
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const useMobileCards = computed(() => {
|
||||
@@ -234,7 +234,7 @@ const sortLoading = ref(false);
|
||||
const randomSeed = ref(Date.now().toString());
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const page = ref(1);
|
||||
const perPage = 32;
|
||||
|
||||
@@ -202,13 +202,13 @@ const newMealdateString = computed(() => {
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { household } = useHouseholdSelf();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
@@ -296,12 +296,12 @@ const recipeRefWithScale = computed(() =>
|
||||
);
|
||||
const isAdminAndNotOwner = computed(() => {
|
||||
return (
|
||||
$auth.user.value?.admin
|
||||
&& $auth.user.value?.id !== recipeRef.value?.userId
|
||||
auth.user.value?.admin
|
||||
&& auth.user.value?.id !== recipeRef.value?.userId
|
||||
);
|
||||
});
|
||||
const canDelete = computed(() => {
|
||||
const user = $auth.user.value;
|
||||
const user = auth.user.value;
|
||||
const recipe = recipeRef.value;
|
||||
return user && recipe && (user.admin || user.id === recipe.userId);
|
||||
});
|
||||
|
||||
@@ -110,8 +110,8 @@ defineEmits<{
|
||||
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = $auth.user.value?.groupSlug;
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = auth.user.value?.groupSlug;
|
||||
const router = useRouter();
|
||||
|
||||
// Initialize sort state with default sorting by dateAdded descending
|
||||
|
||||
@@ -217,7 +217,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const preferences = useShoppingListPreferences();
|
||||
const ready = ref(false);
|
||||
@@ -239,9 +239,9 @@ const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||
|
||||
watch([dialog, () => preferences.value.viewAllLists], () => {
|
||||
if (dialog.value) {
|
||||
currentHouseholdSlug.value = $auth.user.value?.householdSlug || "";
|
||||
currentHouseholdSlug.value = auth.user.value?.householdSlug || "";
|
||||
filteredShoppingLists.value = props.shoppingLists.filter(
|
||||
list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id,
|
||||
list => preferences.value.viewAllLists || list.userId === auth.user.value?.id,
|
||||
);
|
||||
|
||||
if (filteredShoppingLists.value.length === 1 && !state.shoppingListShowAllToggled) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
dark
|
||||
color="primary-lighten-1 top-0 position-relative left-0"
|
||||
:rounded="!$vuetify.display.xs"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
@@ -32,9 +33,8 @@
|
||||
|
||||
<v-btn
|
||||
v-if="$vuetify.display.xs"
|
||||
icon
|
||||
size="x-small"
|
||||
class="rounded-circle"
|
||||
light
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>
|
||||
@@ -87,7 +87,7 @@ const emit = defineEmits<{
|
||||
selected: [recipe: RecipeSummary];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const loading = ref(false);
|
||||
const selectedIndex = ref(-1);
|
||||
|
||||
@@ -153,7 +153,7 @@ watch(dialog, (val) => {
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
watch(route, close);
|
||||
|
||||
function open() {
|
||||
|
||||
@@ -119,10 +119,10 @@ whenever(
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { household } = useHouseholdSelf();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
|
||||
@@ -34,11 +34,11 @@ import { useLazyRecipes } from "~/composables/recipes";
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, RecipeExplorerPageSearch },
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
|
||||
@@ -141,13 +141,13 @@ const emit = defineEmits<{
|
||||
ready: [];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
const showRandomLoading = ref(false);
|
||||
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const {
|
||||
state,
|
||||
|
||||
@@ -81,11 +81,11 @@ import {
|
||||
usePublicToolStore,
|
||||
} from "~/composables/store";
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const {
|
||||
state,
|
||||
|
||||
@@ -52,14 +52,14 @@ const isFavorite = computed(() => {
|
||||
|
||||
async function toggleFavorite() {
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
if (!$auth.user.value) return;
|
||||
if (!auth.user.value) return;
|
||||
if (!isFavorite.value) {
|
||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
||||
await api.users.addFavorite(auth.user.value?.id, props.recipeId);
|
||||
}
|
||||
else {
|
||||
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
|
||||
await api.users.removeFavorite(auth.user.value?.id, props.recipeId);
|
||||
}
|
||||
await refreshUserRatings();
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
density="compact"
|
||||
variant="solo"
|
||||
return-object
|
||||
:items="units || []"
|
||||
:custom-filter="normalizeFilter"
|
||||
:items="filteredUnits"
|
||||
:custom-filter="() => true"
|
||||
item-title="name"
|
||||
class="mx-1"
|
||||
:placeholder="$t('recipe.choose-unit')"
|
||||
@@ -117,8 +117,8 @@
|
||||
density="compact"
|
||||
variant="solo"
|
||||
return-object
|
||||
:items="foods || []"
|
||||
:custom-filter="normalizeFilter"
|
||||
:items="filteredFoods"
|
||||
:custom-filter="() => true"
|
||||
item-title="name"
|
||||
class="mx-1 py-0"
|
||||
:placeholder="$t('recipe.choose-food')"
|
||||
@@ -176,7 +176,6 @@
|
||||
variant="solo"
|
||||
return-object
|
||||
:items="search.data.value || []"
|
||||
:custom-filter="normalizeFilter"
|
||||
item-title="name"
|
||||
class="mx-1 py-0"
|
||||
:placeholder="$t('search.type-to-search')"
|
||||
@@ -227,11 +226,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive, toRefs } from "vue";
|
||||
import { ref, computed, reactive, toRefs, watch } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||
import { normalizeFilter } from "~/composables/use-utils";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
import { useNuxtApp } from "#app";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
@@ -343,8 +342,8 @@ const btns = computed(() => {
|
||||
// Foods
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
const foodSearch = ref("");
|
||||
const foodAutocomplete = ref<HTMLInputElement>();
|
||||
const { search: foodSearch, filtered: filteredFoods } = useSearch(foodStore.store);
|
||||
|
||||
async function createAssignFood() {
|
||||
foodData.data.name = foodSearch.value;
|
||||
@@ -355,8 +354,8 @@ async function createAssignFood() {
|
||||
|
||||
// Recipes
|
||||
const route = useRoute();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||
@@ -375,8 +374,8 @@ watch(loading, (val) => {
|
||||
// Units
|
||||
const unitStore = useUnitStore();
|
||||
const unitsData = useUnitData();
|
||||
const unitSearch = ref("");
|
||||
const unitAutocomplete = ref<HTMLInputElement>();
|
||||
const { search: unitSearch, filtered: filteredUnits } = useSearch(unitStore.store);
|
||||
|
||||
async function createAssignUnit() {
|
||||
unitsData.data.name = unitSearch.value;
|
||||
@@ -430,9 +429,6 @@ function quantityFilter(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
const { showTitle } = toRefs(state);
|
||||
|
||||
const foods = foodStore.store;
|
||||
const units = unitStore.store;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { useParsedIngredientText } from "~/composables/recipes";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
|
||||
interface Props {
|
||||
ingredient?: RecipeIngredient;
|
||||
@@ -20,6 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const { ingredient, scale = 1 } = defineProps<Props>();
|
||||
const { useParsedIngredientText } = useIngredientTextParser();
|
||||
|
||||
const baseText = computed(() => {
|
||||
if (!ingredient) return "";
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RecipeIngredient } from "~/lib/api/types/household";
|
||||
import { useParsedIngredientText } from "~/composables/recipes";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
|
||||
interface Props {
|
||||
ingredient: RecipeIngredient;
|
||||
@@ -44,8 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
});
|
||||
const route = useRoute();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
|
||||
const { useParsedIngredientText } = useIngredientTextParser();
|
||||
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.scale, true, groupSlug.value.toString());
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
|
||||
interface Props {
|
||||
@@ -66,6 +66,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isCookMode: false,
|
||||
});
|
||||
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
function validateTitle(title?: string | null) {
|
||||
return !(title === undefined || title === "" || title === null);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ const madeThisDialog = ref(false);
|
||||
const userApi = useUserApi();
|
||||
const { household } = useHouseholdSelf();
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const domMadeThisForm = ref<VForm>();
|
||||
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||
subject: "",
|
||||
@@ -179,7 +179,7 @@ const newTimelineEventTimestampString = computed(() => {
|
||||
const lastMade = ref(props.recipe.lastMade);
|
||||
const lastMadeReady = ref(false);
|
||||
onMounted(async () => {
|
||||
if (!$auth.user?.value?.householdSlug) {
|
||||
if (!auth.user?.value?.householdSlug) {
|
||||
lastMade.value = props.recipe.lastMade;
|
||||
}
|
||||
else {
|
||||
@@ -255,8 +255,8 @@ async function createTimelineEvent() {
|
||||
madeThisFormLoading.value = true;
|
||||
|
||||
newTimelineEvent.value.recipeId = props.recipe.id;
|
||||
// Note: $auth.user is now a ref
|
||||
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
|
||||
// Note: auth.user is now a ref
|
||||
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: auth.user.value?.fullName });
|
||||
|
||||
// the user only selects the date, so we set the time to end of day local time
|
||||
// we choose the end of day so it always comes after "new recipe" events
|
||||
|
||||
@@ -73,10 +73,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
|
||||
|
||||
const attrs = computed(() => {
|
||||
return props.small
|
||||
|
||||
@@ -162,9 +162,9 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user?.value?.groupSlug || "");
|
||||
|
||||
// =================================================================
|
||||
// Context Menu
|
||||
|
||||
@@ -220,11 +220,11 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
|
||||
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
||||
|
||||
const display = useDisplay();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => (route.params.groupSlug as string) || auth.user?.value?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -431,6 +431,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||
|
||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
|
||||
const dialog = ref(false);
|
||||
const disabledSteps = ref<number[]>([]);
|
||||
@@ -581,7 +582,7 @@ function setUsedIngredients() {
|
||||
watch(activeRefs, () => setUsedIngredients());
|
||||
|
||||
function autoSetReferences() {
|
||||
useExtractIngredientReferences(
|
||||
extractIngredientReferences(
|
||||
props.recipe.recipeIngredient,
|
||||
activeRefs.value,
|
||||
activeText.value,
|
||||
|
||||
@@ -197,7 +197,7 @@ import type { IngredientFood, IngredientUnit, ParsedIngredient, RecipeIngredient
|
||||
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
@@ -208,6 +208,8 @@ const props = defineProps<{
|
||||
ingredients: NoUndefinedField<RecipeIngredient[]>;
|
||||
}>();
|
||||
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
||||
|
||||
@@ -192,7 +192,7 @@ import { useStaticRoutes } from "~/composables/api";
|
||||
import type { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||
import { parseIngredientText, useNutritionLabels } from "~/composables/recipes";
|
||||
import { useIngredientTextParser, useNutritionLabels } from "~/composables/recipes";
|
||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||
|
||||
@@ -362,6 +362,8 @@ const hasNotes = computed(() => {
|
||||
return props.recipe.notes && props.recipe.notes.length > 0;
|
||||
});
|
||||
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
function parseText(ingredient: RecipeIngredient) {
|
||||
return parseIngredientText(ingredient, props.scale);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
<v-card width="400">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="state.search"
|
||||
v-memo="[state.search]"
|
||||
v-model="searchInput"
|
||||
v-memo="[searchInput]"
|
||||
class="mb-2"
|
||||
hide-details
|
||||
density="comfortable"
|
||||
@@ -146,17 +146,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { watchDebounced } from "@vueuse/core";
|
||||
|
||||
export interface SelectableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
import type { ISearchableItem } from "~/composables/use-search";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => SelectableItem[],
|
||||
type: Array as () => ISearchableItem[],
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
@@ -175,12 +171,11 @@ export default defineNuxtComponent({
|
||||
emits: ["update:requireAll", "update:modelValue"],
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
search: "",
|
||||
menu: false,
|
||||
});
|
||||
|
||||
// Use shallowRef for better performance with arrays
|
||||
const debouncedSearch = shallowRef("");
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
|
||||
|
||||
const combinator = computed({
|
||||
get: () => (props.requireAll ? "hasAll" : "hasAny"),
|
||||
@@ -191,7 +186,7 @@ export default defineNuxtComponent({
|
||||
|
||||
// Use shallowRef to prevent deep reactivity on large arrays
|
||||
const selected = computed({
|
||||
get: () => props.modelValue as SelectableItem[],
|
||||
get: () => props.modelValue as ISearchableItem[],
|
||||
set: (value) => {
|
||||
context.emit("update:modelValue", value);
|
||||
},
|
||||
@@ -204,44 +199,12 @@ export default defineNuxtComponent({
|
||||
},
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
() => state.search,
|
||||
(newSearch) => {
|
||||
debouncedSearch.value = newSearch;
|
||||
},
|
||||
{ debounce: 500, maxWait: 1500, immediate: false }, // Increased debounce time
|
||||
);
|
||||
|
||||
const filtered = computed(() => {
|
||||
const items = props.items;
|
||||
const search = debouncedSearch.value;
|
||||
|
||||
if (!search || search.length < 2) { // Only filter after 2 characters
|
||||
return items;
|
||||
}
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
return items.filter(item => item.name.toLowerCase().includes(searchLower));
|
||||
});
|
||||
|
||||
const selectedCount = computed(() => selected.value.length);
|
||||
const selectedIds = computed(() => {
|
||||
return new Set(selected.value.map(item => item.id));
|
||||
});
|
||||
|
||||
const handleCheckboxClick = (item: SelectableItem) => {
|
||||
const currentSelection = selected.value;
|
||||
const isSelected = selectedIds.value.has(item.id);
|
||||
|
||||
if (isSelected) {
|
||||
selected.value = currentSelection.filter(i => i.id !== item.id);
|
||||
}
|
||||
else {
|
||||
selected.value = [...currentSelection, item];
|
||||
}
|
||||
};
|
||||
|
||||
const handleRadioClick = (item: SelectableItem) => {
|
||||
const handleRadioClick = (item: ISearchableItem) => {
|
||||
if (selectedRadio.value === item) {
|
||||
selectedRadio.value = null;
|
||||
}
|
||||
@@ -250,18 +213,18 @@ export default defineNuxtComponent({
|
||||
function clearSelection() {
|
||||
selected.value = [];
|
||||
selectedRadio.value = null;
|
||||
state.search = "";
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
return {
|
||||
combinator,
|
||||
state,
|
||||
searchInput,
|
||||
selected,
|
||||
selectedRadio,
|
||||
selectedCount,
|
||||
selectedIds,
|
||||
filtered,
|
||||
handleCheckboxClick,
|
||||
handleRadioClick,
|
||||
clearSelection,
|
||||
};
|
||||
|
||||
@@ -62,15 +62,15 @@ export default defineNuxtComponent({
|
||||
error: false,
|
||||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { store: users } = useUserStore();
|
||||
const user = computed(() => {
|
||||
return users.value.find(user => user.id === props.userId);
|
||||
});
|
||||
|
||||
const imageURL = computed(() => {
|
||||
// Note: $auth.user is a ref now
|
||||
const authUser = $auth.user.value;
|
||||
// Note: auth.user is a ref now
|
||||
const authUser = auth.user.value;
|
||||
const key = authUser?.cacheKey ?? "";
|
||||
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
||||
});
|
||||
|
||||
@@ -102,9 +102,9 @@ export default defineNuxtComponent({
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const isAdmin = computed(() => $auth.user.value?.admin);
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const token = ref("");
|
||||
const selectedGroup = ref<string | null>(null);
|
||||
const selectedHousehold = ref<string | null>(null);
|
||||
|
||||
@@ -106,11 +106,11 @@ export default defineNuxtComponent({
|
||||
const i18n = useI18n();
|
||||
const { $appInfo, $globals } = useNuxtApp();
|
||||
const display = useDisplay();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const cookbookPreferences = useCookbookPreferences();
|
||||
const ownCookbookStore = useCookbookStore(i18n);
|
||||
@@ -152,7 +152,7 @@ export default defineNuxtComponent({
|
||||
};
|
||||
}
|
||||
|
||||
const currentUserHouseholdId = computed(() => $auth.user.value?.householdId);
|
||||
const currentUserHouseholdId = computed(() => auth.user.value?.householdId);
|
||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||
if (!cookbooks.value?.length) {
|
||||
return [];
|
||||
@@ -187,7 +187,7 @@ export default defineNuxtComponent({
|
||||
});
|
||||
|
||||
links.sort((a, b) => a.title.localeCompare(b.title));
|
||||
if ($auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
||||
if (auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
||||
return ownLinks;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -97,10 +97,10 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { loggedIn } = useLoggedInState();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const { xs, smAndUp } = useDisplay();
|
||||
|
||||
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
||||
@@ -128,7 +128,7 @@ export default defineNuxtComponent({
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await $auth.signOut("/login?direct=1");
|
||||
await auth.signOut("/login?direct=1");
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -168,13 +168,13 @@ export default defineNuxtComponent({
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { loggedIn, isOwnGroup } = useLoggedInState();
|
||||
const isAdmin = computed(() => $auth.user.value?.admin);
|
||||
const canManage = computed(() => $auth.user.value?.canManage);
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const canManage = computed(() => auth.user.value?.canManage);
|
||||
|
||||
const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined);
|
||||
const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined);
|
||||
const userFavoritesLink = computed(() => auth.user.value ? `/user/${auth.user.value.id}/favorites` : undefined);
|
||||
const userProfileLink = computed(() => auth.user.value ? "/user/profile" : undefined);
|
||||
|
||||
const toggleDark = useToggleDarkMode();
|
||||
|
||||
@@ -217,7 +217,7 @@ export default defineNuxtComponent({
|
||||
isAdmin,
|
||||
canManage,
|
||||
isOwnGroup,
|
||||
sessionUser: $auth.user,
|
||||
sessionUser: auth.user,
|
||||
toggleDark,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
*/
|
||||
export default defineNuxtComponent({
|
||||
setup(_, ctx) {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const r = $auth.user.value?.advanced || false;
|
||||
const r = auth.user.value?.advanced || false;
|
||||
|
||||
return () => {
|
||||
return r ? ctx.slots.default?.() : null;
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
v-model:search="searchInput"
|
||||
item-title="name"
|
||||
return-object
|
||||
:items="items"
|
||||
:custom-filter="normalizeFilter"
|
||||
:items="filteredItems"
|
||||
:prepend-icon="icon || $globals.icons.tags"
|
||||
auto-select-first
|
||||
clearable
|
||||
color="primary"
|
||||
hide-details
|
||||
:custom-filter="() => true"
|
||||
@keyup.enter="emitCreate"
|
||||
>
|
||||
<template
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { normalizeFilter } from "~/composables/use-utils";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
@@ -85,7 +85,10 @@ export default defineNuxtComponent({
|
||||
emits: ["update:modelValue", "update:item-id", "create"],
|
||||
setup(props, context) {
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
const searchInput = ref("");
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||
|
||||
const itemIdVal = computed({
|
||||
get: () => {
|
||||
return props.itemId || undefined;
|
||||
@@ -123,8 +126,8 @@ export default defineNuxtComponent({
|
||||
itemVal,
|
||||
itemIdVal,
|
||||
searchInput,
|
||||
filteredItems,
|
||||
emitCreate,
|
||||
normalizeFilter,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -165,14 +165,14 @@ export function clearPageState(slug: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* usePageUser provides a wrapper around $auth that provides a type-safe way to
|
||||
* usePageUser provides a wrapper around auth that provides a type-safe way to
|
||||
* access the UserOut type from the context. If no user is logged in then an empty
|
||||
* object with all properties set to their zero value is returned.
|
||||
*/
|
||||
export function usePageUser(): { user: UserOut } {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
if (!$auth.user.value) {
|
||||
if (!auth.user.value) {
|
||||
return {
|
||||
user: {
|
||||
id: "",
|
||||
@@ -188,5 +188,5 @@ export function usePageUser(): { user: UserOut } {
|
||||
};
|
||||
}
|
||||
|
||||
return { user: $auth.user.value };
|
||||
return { user: auth.user.value };
|
||||
}
|
||||
|
||||
@@ -1,60 +1,82 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi, beforeEach } from "vitest";
|
||||
import { useExtractIngredientReferences } from "./use-extract-ingredient-references";
|
||||
import { useLocales } from "../use-locales";
|
||||
|
||||
vi.mock("../use-locales");
|
||||
|
||||
const punctuationMarks = ["*", "?", "/", "!", "**", "&", "."];
|
||||
|
||||
describe("test use extract ingredient references", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||
} as any);
|
||||
});
|
||||
|
||||
test("when text empty return empty", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "");
|
||||
expect(result).toStrictEqual(new Set());
|
||||
});
|
||||
|
||||
test("when and ingredient matches exactly and has a reference id, return the referenceId", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion");
|
||||
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test.each(punctuationMarks)("when ingredient is suffixed by punctuation, return the referenceId", (suffix) => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix, true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix);
|
||||
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test.each(punctuationMarks)("when ingredient is prefixed by punctuation, return the referenceId", (prefix) => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion");
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test("when ingredient is first on a multiline, return the referenceId", () => {
|
||||
const multilineSting = "lksjdlk\nOnion";
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting, true);
|
||||
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting);
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test("when the ingredient matches partially exactly and has a reference id, return the referenceId", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions");
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test("when the ingredient matches with different casing and has a reference id, return the referenceId", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions");
|
||||
expect(result).toEqual(new Set(["123"]));
|
||||
});
|
||||
|
||||
test("when no ingredients, return empty", () => {
|
||||
const result = useExtractIngredientReferences([], [], "A sentence containing oNions", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([], [], "A sentence containing oNions");
|
||||
expect(result).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("when and ingredient matches but in the existing referenceIds, do not return the referenceId", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion");
|
||||
|
||||
expect(result).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("when an word is 2 letter of shorter, it is ignored", () => {
|
||||
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On", true);
|
||||
const { extractIngredientReferences } = useExtractIngredientReferences();
|
||||
const result = extractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On");
|
||||
|
||||
expect(result).toEqual(new Set());
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import { useIngredientTextParser } from "~/composables/recipes";
|
||||
|
||||
function normalize(word: string): string {
|
||||
let normalizing = word;
|
||||
@@ -18,11 +18,6 @@ function removeStartingPunctuation(word: string): string {
|
||||
return word.replace(punctuationAtBeginning, "");
|
||||
}
|
||||
|
||||
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
|
||||
const searchText = parseIngredientText(ingredient);
|
||||
return searchText.toLowerCase().includes(word.toLowerCase());
|
||||
}
|
||||
|
||||
function isBlackListedWord(word: string) {
|
||||
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
|
||||
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
|
||||
@@ -39,20 +34,33 @@ function isBlackListedWord(word: string) {
|
||||
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
|
||||
}
|
||||
|
||||
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
|
||||
const availableIngredients = recipeIngredients
|
||||
.filter(ingredient => ingredient.referenceId !== undefined)
|
||||
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
||||
export function useExtractIngredientReferences() {
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const allMatchedIngredientIds: string[] = text
|
||||
.toLowerCase()
|
||||
.split(/\s/)
|
||||
.map(normalize)
|
||||
.filter(word => word.length > 2)
|
||||
.filter(word => !isBlackListedWord(word))
|
||||
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
|
||||
.map(ingredient => ingredient.referenceId as string);
|
||||
// deduplicate
|
||||
function extractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
|
||||
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
|
||||
const searchText = parseIngredientText(ingredient);
|
||||
return searchText.toLowerCase().includes(word.toLowerCase());
|
||||
}
|
||||
|
||||
return new Set<string>(allMatchedIngredientIds);
|
||||
const availableIngredients = recipeIngredients
|
||||
.filter(ingredient => ingredient.referenceId !== undefined)
|
||||
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
||||
|
||||
const allMatchedIngredientIds: string[] = text
|
||||
.toLowerCase()
|
||||
.split(/\s/)
|
||||
.map(normalize)
|
||||
.filter(word => word.length > 2)
|
||||
.filter(word => !isBlackListedWord(word))
|
||||
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
|
||||
.map(ingredient => ingredient.referenceId as string);
|
||||
// deduplicate
|
||||
|
||||
return new Set<string>(allMatchedIngredientIds);
|
||||
}
|
||||
|
||||
return {
|
||||
extractIngredientReferences,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { useFraction } from "./use-fraction";
|
||||
export { useRecipe } from "./use-recipe";
|
||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
|
||||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
||||
export { useIngredientTextParser } from "./use-recipe-ingredients";
|
||||
export { useNutritionLabels } from "./use-recipe-nutrition";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
export { useRecipePermissions } from "./use-recipe-permissions";
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { parseIngredientText } from "./use-recipe-ingredients";
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||
import { useIngredientTextParser } from "./use-recipe-ingredients";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { useLocales } from "../use-locales";
|
||||
|
||||
vi.mock("../use-locales");
|
||||
|
||||
let parseIngredientText: (ingredient: RecipeIngredient, scale?: number, includeFormating?: boolean) => string;
|
||||
|
||||
describe("parseIngredientText", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||
} as any);
|
||||
({ parseIngredientText } = useIngredientTextParser());
|
||||
});
|
||||
|
||||
describe(parseIngredientText.name, () => {
|
||||
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
|
||||
quantity: 1,
|
||||
food: {
|
||||
@@ -128,4 +141,98 @@ describe(parseIngredientText.name, () => {
|
||||
|
||||
expect(parseIngredientText(ingredient, 2)).toEqual("2 tablespoons diced onions");
|
||||
});
|
||||
|
||||
test("plural handling: 'always' strategy uses plural food with unit", () => {
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "always" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "always" },
|
||||
} as any);
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 2,
|
||||
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onions");
|
||||
});
|
||||
|
||||
test("plural handling: 'never' strategy never uses plural food", () => {
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "never" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "never" },
|
||||
} as any);
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 2,
|
||||
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onion");
|
||||
});
|
||||
|
||||
test("plural handling: 'without-unit' strategy uses plural food without unit", () => {
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||
} as any);
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 2,
|
||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||
unit: undefined,
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("2 diced onions");
|
||||
});
|
||||
|
||||
test("plural handling: 'without-unit' strategy uses singular food with unit", () => {
|
||||
vi.mocked(useLocales).mockReturnValue({
|
||||
locales: [{ value: "en-US", pluralFoodHandling: "without-unit" }],
|
||||
locale: { value: "en-US", pluralFoodHandling: "without-unit" },
|
||||
} as any);
|
||||
const { parseIngredientText } = useIngredientTextParser();
|
||||
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 2,
|
||||
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", useAbbreviation: false },
|
||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onion");
|
||||
});
|
||||
|
||||
test("decimal below minimum precision shows < 0.001", () => {
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 0.0001,
|
||||
unit: { id: "1", name: "cup", useAbbreviation: false },
|
||||
food: { id: "1", name: "salt" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("< 0.001 cup salt");
|
||||
});
|
||||
|
||||
test("fraction below minimum denominator shows < 1/10", () => {
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 0.05,
|
||||
unit: { id: "1", name: "cup", fraction: true, useAbbreviation: false },
|
||||
food: { id: "1", name: "salt" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient)).toEqual("< <sup>1</sup><span>⁄</span><sub>10</sub> cup salt");
|
||||
});
|
||||
|
||||
test("fraction below minimum denominator without formatting shows < 1/10", () => {
|
||||
const ingredient = createRecipeIngredient({
|
||||
quantity: 0.05,
|
||||
unit: { id: "1", name: "cup", fraction: true, useAbbreviation: false },
|
||||
food: { id: "1", name: "salt" },
|
||||
});
|
||||
|
||||
expect(parseIngredientText(ingredient, 1, false)).toEqual("< 1/10 cup salt");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useFraction } from "./use-fraction";
|
||||
import { useLocales } from "../use-locales";
|
||||
import type { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
|
||||
const { frac } = useFraction();
|
||||
|
||||
const FRAC_MIN_DENOM = 10;
|
||||
const DECIMAL_PRECISION = 3;
|
||||
|
||||
export function sanitizeIngredientHTML(rawHtml: string) {
|
||||
return DOMPurify.sanitize(rawHtml, {
|
||||
USE_PROFILES: { html: true },
|
||||
@@ -56,47 +60,90 @@ type ParsedIngredientText = {
|
||||
recipeLink?: string;
|
||||
};
|
||||
|
||||
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
|
||||
const { quantity, food, unit, note, referencedRecipe } = ingredient;
|
||||
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
||||
const usePluralFood = (!quantity) || quantity * scale > 1;
|
||||
|
||||
let returnQty = "";
|
||||
|
||||
// casting to number is required as sometimes quantity is a string
|
||||
if (quantity && Number(quantity) !== 0) {
|
||||
if (unit && !unit.fraction) {
|
||||
returnQty = Number((quantity * scale).toPrecision(3)).toString();
|
||||
}
|
||||
else {
|
||||
const fraction = frac(quantity * scale, 10, true);
|
||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||
returnQty += fraction[0];
|
||||
}
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
returnQty += includeFormating
|
||||
? `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`
|
||||
: ` ${fraction[1]}/${fraction[2]}`;
|
||||
}
|
||||
}
|
||||
function shouldUsePluralFood(quantity: number, hasUnit: boolean, pluralFoodHandling: string): boolean {
|
||||
if (quantity && quantity <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unitName = useUnitName(unit || undefined, usePluralUnit);
|
||||
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
|
||||
switch (pluralFoodHandling) {
|
||||
case "always":
|
||||
return true;
|
||||
case "without-unit":
|
||||
return !(quantity && hasUnit);
|
||||
case "never":
|
||||
return false;
|
||||
|
||||
default:
|
||||
// same as without-unit
|
||||
return !(quantity && hasUnit);
|
||||
}
|
||||
}
|
||||
|
||||
export function useIngredientTextParser() {
|
||||
const { locales, locale } = useLocales();
|
||||
|
||||
function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true, groupSlug?: string): ParsedIngredientText {
|
||||
const filteredLocales = locales.filter(lc => lc.value === locale.value);
|
||||
const pluralFoodHandling = filteredLocales.length ? filteredLocales[0].pluralFoodHandling : "without-unit";
|
||||
|
||||
const { quantity, food, unit, note, referencedRecipe } = ingredient;
|
||||
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
||||
const usePluralFood = shouldUsePluralFood((quantity || 0) * scale, !!unit, pluralFoodHandling);
|
||||
|
||||
let returnQty = "";
|
||||
|
||||
// casting to number is required as sometimes quantity is a string
|
||||
if (quantity && Number(quantity) !== 0) {
|
||||
const scaledQuantity = Number((quantity * scale));
|
||||
|
||||
if (unit && !unit.fraction) {
|
||||
const minVal = 10 ** -DECIMAL_PRECISION;
|
||||
returnQty = scaledQuantity >= minVal
|
||||
? Number(scaledQuantity.toPrecision(DECIMAL_PRECISION)).toString()
|
||||
: `< ${minVal}`;
|
||||
}
|
||||
else {
|
||||
const minVal = 1 / FRAC_MIN_DENOM;
|
||||
const isUnderMinVal = !(scaledQuantity >= minVal);
|
||||
|
||||
const fraction = !isUnderMinVal ? frac(scaledQuantity, FRAC_MIN_DENOM, true) : [0, 1, FRAC_MIN_DENOM];
|
||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||
returnQty += fraction[0];
|
||||
}
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
returnQty += includeFormating
|
||||
? `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`
|
||||
: ` ${fraction[1]}/${fraction[2]}`;
|
||||
}
|
||||
|
||||
if (isUnderMinVal) {
|
||||
returnQty = `< ${returnQty}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unitName = useUnitName(unit || undefined, usePluralUnit);
|
||||
const ingName = referencedRecipe ? referencedRecipe.name || "" : useFoodName(food || undefined, usePluralFood);
|
||||
|
||||
return {
|
||||
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
||||
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
||||
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
|
||||
note: note ? sanitizeIngredientHTML(note) : undefined,
|
||||
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
|
||||
};
|
||||
};
|
||||
|
||||
function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
|
||||
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
|
||||
|
||||
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
||||
return sanitizeIngredientHTML(text);
|
||||
};
|
||||
|
||||
return {
|
||||
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
||||
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
||||
name: ingName ? sanitizeIngredientHTML(ingName) : undefined,
|
||||
note: note ? sanitizeIngredientHTML(note) : undefined,
|
||||
recipeLink: useRecipeLink(referencedRecipe || undefined, groupSlug),
|
||||
useParsedIngredientText,
|
||||
parseIngredientText,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
|
||||
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
|
||||
|
||||
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
||||
return sanitizeIngredientHTML(text);
|
||||
}
|
||||
|
||||
@@ -5,251 +5,293 @@ export const LOCALES = [
|
||||
value: "zh-TW",
|
||||
progress: 9,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "简体中文 (Chinese simplified)",
|
||||
value: "zh-CN",
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Tiếng Việt (Vietnamese)",
|
||||
value: "vi-VN",
|
||||
progress: 2,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Українська (Ukrainian)",
|
||||
value: "uk-UA",
|
||||
progress: 83,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Türkçe (Turkish)",
|
||||
value: "tr-TR",
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Svenska (Swedish)",
|
||||
value: "sv-SE",
|
||||
progress: 61,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "српски (Serbian)",
|
||||
value: "sr-SP",
|
||||
progress: 9,
|
||||
progress: 16,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Slovenščina (Slovenian)",
|
||||
value: "sl-SI",
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Slovenčina (Slovak)",
|
||||
value: "sk-SK",
|
||||
progress: 47,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Pусский (Russian)",
|
||||
value: "ru-RU",
|
||||
progress: 44,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Română (Romanian)",
|
||||
value: "ro-RO",
|
||||
progress: 44,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Português (Portuguese)",
|
||||
value: "pt-PT",
|
||||
progress: 39,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Português do Brasil (Brazilian Portuguese)",
|
||||
value: "pt-BR",
|
||||
progress: 46,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Polski (Polish)",
|
||||
value: "pl-PL",
|
||||
progress: 49,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Norsk (Norwegian)",
|
||||
value: "no-NO",
|
||||
progress: 42,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Nederlands (Dutch)",
|
||||
value: "nl-NL",
|
||||
progress: 54,
|
||||
progress: 60,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Latviešu (Latvian)",
|
||||
value: "lv-LV",
|
||||
progress: 35,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Lietuvių (Lithuanian)",
|
||||
value: "lt-LT",
|
||||
progress: 30,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "한국어 (Korean)",
|
||||
value: "ko-KR",
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "日本語 (Japanese)",
|
||||
value: "ja-JP",
|
||||
progress: 36,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "never",
|
||||
},
|
||||
{
|
||||
name: "Italiano (Italian)",
|
||||
value: "it-IT",
|
||||
progress: 49,
|
||||
progress: 52,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Íslenska (Icelandic)",
|
||||
value: "is-IS",
|
||||
progress: 43,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Magyar (Hungarian)",
|
||||
value: "hu-HU",
|
||||
progress: 46,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Hrvatski (Croatian)",
|
||||
value: "hr-HR",
|
||||
progress: 30,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "עברית (Hebrew)",
|
||||
value: "he-IL",
|
||||
progress: 64,
|
||||
dir: "rtl",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Galego (Galician)",
|
||||
value: "gl-ES",
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Français (French)",
|
||||
value: "fr-FR",
|
||||
progress: 67,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Français canadien (Canadian French)",
|
||||
value: "fr-CA",
|
||||
progress: 83,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Belge (Belgian)",
|
||||
value: "fr-BE",
|
||||
progress: 39,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Suomi (Finnish)",
|
||||
value: "fi-FI",
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Eesti (Estonian)",
|
||||
value: "et-EE",
|
||||
progress: 44,
|
||||
progress: 45,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Español (Spanish)",
|
||||
value: "es-ES",
|
||||
progress: 45,
|
||||
progress: 46,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "American English",
|
||||
value: "en-US",
|
||||
progress: 100.0,
|
||||
progress: 100,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "without-unit",
|
||||
},
|
||||
{
|
||||
name: "British English",
|
||||
value: "en-GB",
|
||||
progress: 42,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "without-unit",
|
||||
},
|
||||
{
|
||||
name: "Ελληνικά (Greek)",
|
||||
value: "el-GR",
|
||||
progress: 41,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Deutsch (German)",
|
||||
value: "de-DE",
|
||||
progress: 83,
|
||||
progress: 85,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Dansk (Danish)",
|
||||
value: "da-DK",
|
||||
progress: 63,
|
||||
progress: 65,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Čeština (Czech)",
|
||||
value: "cs-CZ",
|
||||
progress: 43,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Català (Catalan)",
|
||||
value: "ca-ES",
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Български (Bulgarian)",
|
||||
value: "bg-BG",
|
||||
progress: 49,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "العربية (Arabic)",
|
||||
value: "ar-SA",
|
||||
progress: 25,
|
||||
dir: "rtl",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
{
|
||||
name: "Afrikaans (Afrikaans)",
|
||||
value: "af-ZA",
|
||||
progress: 26,
|
||||
dir: "ltr",
|
||||
pluralFoodHandling: "always",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||
import { LOCALES } from "./available-locales";
|
||||
import { useGlobalI18n } from "../use-global-i18n";
|
||||
|
||||
export const useLocales = () => {
|
||||
const i18n = useI18n();
|
||||
const i18n = useGlobalI18n();
|
||||
const { current: vuetifyLocale } = useLocale();
|
||||
|
||||
const locale = computed<LocaleObject["code"]>({
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export const useLoggedInState = function () {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
|
||||
const loggedIn = computed(() => $auth.loggedIn.value);
|
||||
const loggedIn = computed(() => auth.loggedIn.value);
|
||||
const isOwnGroup = computed(() => {
|
||||
if (!route.params.groupSlug) {
|
||||
return loggedIn.value;
|
||||
}
|
||||
else {
|
||||
return loggedIn.value && $auth.user.value?.groupSlug === route.params.groupSlug;
|
||||
return loggedIn.value && auth.user.value?.groupSlug === route.params.groupSlug;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import type { LogicalOperator, RecipeOrganizer, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
|
||||
import type { LogicalOperator, PlaceholderKeyword, RecipeOrganizer, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
|
||||
|
||||
export interface FieldLogicalOperator {
|
||||
label: string;
|
||||
@@ -11,6 +11,11 @@ export interface FieldRelationalOperator {
|
||||
value: RelationalKeyword | RelationalOperator;
|
||||
}
|
||||
|
||||
export interface FieldPlaceholderKeyword {
|
||||
label: string;
|
||||
value: PlaceholderKeyword;
|
||||
}
|
||||
|
||||
export interface OrganizerBase {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -22,6 +27,7 @@ export type FieldType
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "date"
|
||||
| "relativeDate"
|
||||
| RecipeOrganizer;
|
||||
|
||||
export type FieldValue
|
||||
@@ -41,8 +47,8 @@ export interface FieldDefinition {
|
||||
label: string;
|
||||
type: FieldType;
|
||||
|
||||
// only for select/organizer fields
|
||||
fieldOptions?: SelectableItem[];
|
||||
// Select/Organizer
|
||||
fieldChoices?: SelectableItem[];
|
||||
}
|
||||
|
||||
export interface Field extends FieldDefinition {
|
||||
@@ -50,10 +56,10 @@ export interface Field extends FieldDefinition {
|
||||
logicalOperator?: FieldLogicalOperator;
|
||||
value: FieldValue;
|
||||
relationalOperatorValue: FieldRelationalOperator;
|
||||
relationalOperatorOptions: FieldRelationalOperator[];
|
||||
relationalOperatorChoices: FieldRelationalOperator[];
|
||||
rightParenthesis?: string;
|
||||
|
||||
// only for select/organizer fields
|
||||
// Select/Organizer
|
||||
values: FieldValue[];
|
||||
organizers: OrganizerBase[];
|
||||
}
|
||||
@@ -161,6 +167,36 @@ export function useQueryFilterBuilder() {
|
||||
};
|
||||
});
|
||||
|
||||
const placeholderKeywords = computed<Record<PlaceholderKeyword, FieldPlaceholderKeyword>>(() => {
|
||||
const NOW = {
|
||||
label: "Now",
|
||||
value: "$NOW",
|
||||
} as FieldPlaceholderKeyword;
|
||||
|
||||
return {
|
||||
$NOW: NOW,
|
||||
};
|
||||
});
|
||||
|
||||
const relativeDateRelOps = computed<Record<RelationalKeyword | RelationalOperator, FieldRelationalOperator>>(() => {
|
||||
const ops = { ...relOps.value };
|
||||
|
||||
ops[">="] = { ...relOps.value[">="], label: i18n.t("query-filter.relational-operators.is-newer-than") };
|
||||
ops["<="] = { ...relOps.value["<="], label: i18n.t("query-filter.relational-operators.is-older-than") };
|
||||
|
||||
return ops;
|
||||
});
|
||||
|
||||
function getRelOps(fieldType: FieldType): typeof relOps | typeof relativeDateRelOps {
|
||||
switch (fieldType) {
|
||||
case "relativeDate":
|
||||
return relativeDateRelOps;
|
||||
|
||||
default:
|
||||
return relOps;
|
||||
}
|
||||
}
|
||||
|
||||
function isOrganizerType(type: FieldType): type is Organizer {
|
||||
return (
|
||||
type === Organizer.Category
|
||||
@@ -173,10 +209,14 @@ export function useQueryFilterBuilder() {
|
||||
};
|
||||
|
||||
function getFieldFromFieldDef(field: Field | FieldDefinition, resetValue = false): Field {
|
||||
const updatedField = { logicalOperator: logOps.value.AND, ...field } as Field;
|
||||
let operatorOptions: FieldRelationalOperator[];
|
||||
if (updatedField.fieldOptions?.length || isOrganizerType(updatedField.type)) {
|
||||
operatorOptions = [
|
||||
const updatedField = {
|
||||
logicalOperator: logOps.value.AND,
|
||||
...field,
|
||||
} as Field;
|
||||
|
||||
let operatorChoices: FieldRelationalOperator[];
|
||||
if (updatedField.fieldChoices?.length || isOrganizerType(updatedField.type)) {
|
||||
operatorChoices = [
|
||||
relOps.value["IN"],
|
||||
relOps.value["NOT IN"],
|
||||
relOps.value["CONTAINS ALL"],
|
||||
@@ -185,7 +225,7 @@ export function useQueryFilterBuilder() {
|
||||
else {
|
||||
switch (updatedField.type) {
|
||||
case "string":
|
||||
operatorOptions = [
|
||||
operatorChoices = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value["LIKE"],
|
||||
@@ -193,7 +233,7 @@ export function useQueryFilterBuilder() {
|
||||
];
|
||||
break;
|
||||
case "number":
|
||||
operatorOptions = [
|
||||
operatorChoices = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value[">"],
|
||||
@@ -203,10 +243,10 @@ export function useQueryFilterBuilder() {
|
||||
];
|
||||
break;
|
||||
case "boolean":
|
||||
operatorOptions = [relOps.value["="]];
|
||||
operatorChoices = [relOps.value["="]];
|
||||
break;
|
||||
case "date":
|
||||
operatorOptions = [
|
||||
operatorChoices = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value[">"],
|
||||
@@ -215,13 +255,20 @@ export function useQueryFilterBuilder() {
|
||||
relOps.value["<="],
|
||||
];
|
||||
break;
|
||||
case "relativeDate":
|
||||
operatorChoices = [
|
||||
// "<=" is first since "older than" is the most common operator
|
||||
relativeDateRelOps.value["<="],
|
||||
relativeDateRelOps.value[">="],
|
||||
];
|
||||
break;
|
||||
default:
|
||||
operatorOptions = [relOps.value["="], relOps.value["<>"]];
|
||||
operatorChoices = [relOps.value["="], relOps.value["<>"]];
|
||||
}
|
||||
}
|
||||
updatedField.relationalOperatorOptions = operatorOptions;
|
||||
if (!operatorOptions.includes(updatedField.relationalOperatorValue)) {
|
||||
updatedField.relationalOperatorValue = operatorOptions[0];
|
||||
updatedField.relationalOperatorChoices = operatorChoices;
|
||||
if (!operatorChoices.includes(updatedField.relationalOperatorValue)) {
|
||||
updatedField.relationalOperatorValue = operatorChoices[0];
|
||||
}
|
||||
|
||||
if (resetValue) {
|
||||
@@ -271,7 +318,7 @@ export function useQueryFilterBuilder() {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
|
||||
if (field.values?.length) {
|
||||
let val: string;
|
||||
if (field.type === "string" || field.type === "date" || isOrganizerType(field.type)) {
|
||||
@@ -316,7 +363,8 @@ export function useQueryFilterBuilder() {
|
||||
|
||||
return {
|
||||
logOps,
|
||||
relOps,
|
||||
placeholderKeywords,
|
||||
getRelOps,
|
||||
buildQueryFilterString,
|
||||
getFieldFromFieldDef,
|
||||
isOrganizerType,
|
||||
|
||||
117
frontend/composables/use-search.ts
Normal file
117
frontend/composables/use-search.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { watchDebounced } from "@vueuse/core";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
export interface IAlias {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ISearchableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
aliases?: IAlias[] | undefined;
|
||||
}
|
||||
|
||||
interface ISearchItemInternal extends ISearchableItem {
|
||||
aliasesText?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ISearchOptions {
|
||||
debounceMs?: number;
|
||||
maxWaitMs?: number;
|
||||
minSearchLength?: number;
|
||||
fuseOptions?: Partial<IFuseOptions<ISearchItemInternal>>;
|
||||
}
|
||||
|
||||
export function useSearch<T extends ISearchableItem>(
|
||||
items: ComputedRef<T[]> | Ref<T[]> | T[],
|
||||
options: ISearchOptions = {},
|
||||
) {
|
||||
const {
|
||||
debounceMs = 0,
|
||||
maxWaitMs = 1500,
|
||||
minSearchLength = 1,
|
||||
fuseOptions: customFuseOptions = {},
|
||||
} = options;
|
||||
|
||||
// State
|
||||
const search = ref("");
|
||||
const debouncedSearch = shallowRef("");
|
||||
|
||||
// Flatten item aliases to include as searchable text
|
||||
const searchItems = computed(() => {
|
||||
const itemsArray = Array.isArray(items) ? items : items.value;
|
||||
return itemsArray.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
aliasesText: item.aliases ? item.aliases.map(a => a.name).join(" ") : "",
|
||||
} as ISearchItemInternal;
|
||||
});
|
||||
});
|
||||
|
||||
// Default Fuse options
|
||||
const defaultFuseOptions: IFuseOptions<ISearchItemInternal> = {
|
||||
keys: [
|
||||
{ name: "name", weight: 3 },
|
||||
{ name: "pluralName", weight: 3 },
|
||||
{ name: "abbreviation", weight: 2 },
|
||||
{ name: "pluralAbbreviation", weight: 2 },
|
||||
{ name: "aliasesText", weight: 1 },
|
||||
],
|
||||
ignoreLocation: true,
|
||||
shouldSort: true,
|
||||
threshold: 0.3,
|
||||
minMatchCharLength: 1,
|
||||
findAllMatches: false,
|
||||
};
|
||||
|
||||
// Merge custom options with defaults
|
||||
const fuseOptions = computed(() => ({
|
||||
...defaultFuseOptions,
|
||||
...customFuseOptions,
|
||||
}));
|
||||
|
||||
// Debounce search input
|
||||
watchDebounced(
|
||||
() => search.value,
|
||||
(newSearch) => {
|
||||
debouncedSearch.value = newSearch;
|
||||
},
|
||||
{ debounce: debounceMs, maxWait: maxWaitMs, immediate: false },
|
||||
);
|
||||
|
||||
// Initialize Fuse instance
|
||||
const fuse = computed(() => {
|
||||
return new Fuse(searchItems.value || [], fuseOptions.value);
|
||||
});
|
||||
|
||||
// Compute filtered results
|
||||
const filtered = computed(() => {
|
||||
const itemsArray = Array.isArray(items) ? items : items.value;
|
||||
const searchTerm = debouncedSearch.value.trim();
|
||||
|
||||
// If no search query or less than minSearchLength characters, return all items
|
||||
if (!searchTerm || searchTerm.length < minSearchLength) {
|
||||
return itemsArray;
|
||||
}
|
||||
|
||||
if (!itemsArray || itemsArray.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = fuse.value.search(searchTerm);
|
||||
return results.map(result => result.item as T);
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
search.value = "";
|
||||
debouncedSearch.value = "";
|
||||
};
|
||||
|
||||
return {
|
||||
search,
|
||||
debouncedSearch,
|
||||
filtered,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -6,10 +6,10 @@ const loading = ref(false);
|
||||
const ready = ref(false);
|
||||
|
||||
export const useUserSelfRatings = function () {
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
async function refreshUserRatings() {
|
||||
if (!$auth.user.value || loading.value) {
|
||||
if (!auth.user.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useUserSelfRatings = function () {
|
||||
loading.value = true;
|
||||
const api = useUserApi();
|
||||
|
||||
const userId = $auth.user.value?.id || "";
|
||||
const userId = auth.user.value?.id || "";
|
||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||
|
||||
loading.value = false;
|
||||
|
||||
@@ -34,6 +34,9 @@ const normalizeLigatures = replaceAllBuilder(new Map([
|
||||
["st", "st"],
|
||||
]));
|
||||
|
||||
/**
|
||||
* @deprecated prefer fuse.js/use-search.ts
|
||||
*/
|
||||
export const normalize = (str: string) => {
|
||||
if (!str) {
|
||||
return "";
|
||||
@@ -45,6 +48,9 @@ export const normalize = (str: string) => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated prefer fuse.js/use-search.ts
|
||||
*/
|
||||
export const normalizeFilter: FilterFunction = (value: string, query: string) => {
|
||||
const normalizedValue = normalize(value);
|
||||
const normalizeQuery = normalize(query);
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "أكبر من",
|
||||
"is-greater-than-or-equal-to": "أكبر من أو يساوي",
|
||||
"is-less-than": "أقل من",
|
||||
"is-less-than-or-equal-to": "أقل من أو يساوي"
|
||||
"is-less-than-or-equal-to": "أقل من أو يساوي",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "هو",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "يحتوي على كل من",
|
||||
"is-like": "هو مثل",
|
||||
"is-not-like": "ليس مثل"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Прилага се за всички дни",
|
||||
"applies-on-days": "Всеки/всяка {0}",
|
||||
"meal-plan-settings": "Настройки на плана за хранене",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Добавяне на всички към списъка за пазаруване",
|
||||
"add-day-to-list": "Добавяне на ден към списъка за пазаруване"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Данните за мигриране са премахнати",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Блокиран ли е уебсайтът?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Опитайте вместо това да импортирате суровия HTML код.",
|
||||
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Импортиране на оригиналните категории",
|
||||
"stay-in-edit-mode": "Остани в режим на редакция",
|
||||
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
|
||||
"import-from-zip": "Импортирай от Zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "е по-голямо от",
|
||||
"is-greater-than-or-equal-to": "е по-голямо от или равно на",
|
||||
"is-less-than": "е по-малко от",
|
||||
"is-less-than-or-equal-to": "e по-малко или равно на"
|
||||
"is-less-than-or-equal-to": "e по-малко или равно на",
|
||||
"is-older-than": "е по-стар от",
|
||||
"is-newer-than": "е по-нов от"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "е",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "съдържа всички от",
|
||||
"is-like": "е като",
|
||||
"is-not-like": "не е като"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "преди дни|преди ден|преди дни"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "és més gran que",
|
||||
"is-greater-than-or-equal-to": "és més gran o igual a",
|
||||
"is-less-than": "és menys que",
|
||||
"is-less-than-or-equal-to": "és menor o igual a"
|
||||
"is-less-than-or-equal-to": "és menor o igual a",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "és",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "conté tots de",
|
||||
"is-like": "és com",
|
||||
"is-not-like": "no és com"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Nahrát soubor",
|
||||
"created-on-date": "Vytvořeno dne: {0}",
|
||||
"unsaved-changes": "Máte neuložené změny. Chcete je uložit před odchodem? Klikněte Okay pro uložení, Cancel pro smazání změn.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Zahodit změny",
|
||||
"discard-changes-description": "Máte neuložené změny. Určitě je chcete zahodit?",
|
||||
"clipboard-copy-failure": "Zkopírování do schránky se nezdařilo.",
|
||||
"confirm-delete-generic-items": "Opravdu chcete smazat následující položky?",
|
||||
"organizers": "Organizace",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Použije se na všechny dny",
|
||||
"applies-on-days": "Platí pro {0}",
|
||||
"meal-plan-settings": "Nastavení jídelníčku",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Přidat vše do seznamu",
|
||||
"add-day-to-list": "Přidat den do seznamu"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Data z migrace byla smazána",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Webové stránky jsou blokovány?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Zkuste namísto toho importovat raw HTML.",
|
||||
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importovat původní kategorie",
|
||||
"stay-in-edit-mode": "Zůstat v režimu úprav",
|
||||
"parse-recipe-ingredients-after-import": "Po importu analyzovat ingredience receptu",
|
||||
"import-from-zip": "Importovat ze zipu",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "je větší než",
|
||||
"is-greater-than-or-equal-to": "je větší než nebo rovno",
|
||||
"is-less-than": "je menší než",
|
||||
"is-less-than-or-equal-to": "je menší než nebo rovno"
|
||||
"is-less-than-or-equal-to": "je menší než nebo rovno",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "je",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "obsahuje všechny z",
|
||||
"is-like": "je jako",
|
||||
"is-not-like": "není jako"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "er større end",
|
||||
"is-greater-than-or-equal-to": "er større end eller lig med (Automatic Translation)",
|
||||
"is-less-than": "er mindre end (Automatic Translation)",
|
||||
"is-less-than-or-equal-to": "er mindre end eller lig med (Automatic Translation)"
|
||||
"is-less-than-or-equal-to": "er mindre end eller lig med (Automatic Translation)",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "er",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "indeholder alle af",
|
||||
"is-like": "er ligesom",
|
||||
"is-not-like": "er ikke som"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "ist größer als",
|
||||
"is-greater-than-or-equal-to": "ist größer gleich",
|
||||
"is-less-than": "ist weniger als",
|
||||
"is-less-than-or-equal-to": "ist kleiner gleich"
|
||||
"is-less-than-or-equal-to": "ist kleiner gleich",
|
||||
"is-older-than": "Ist älter als",
|
||||
"is-newer-than": "Ist neuer als"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "ist",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "enthält alle",
|
||||
"is-like": "ist wie",
|
||||
"is-not-like": "ist nicht wie"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "είναι μεγαλύτερο από",
|
||||
"is-greater-than-or-equal-to": "είναι μεγαλύτερο από ή ίσο με",
|
||||
"is-less-than": "είναι μικρότερο από",
|
||||
"is-less-than-or-equal-to": "είναι μικρότερο από ή ίσο με"
|
||||
"is-less-than-or-equal-to": "είναι μικρότερο από ή ίσο με",
|
||||
"is-older-than": "είναι παλαιότερο από",
|
||||
"is-newer-than": "είναι νεότερο από"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "είναι",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "περιέχει όλα τα",
|
||||
"is-like": "είναι όμοιο με",
|
||||
"is-not-like": "δεν είναι όμοιο με"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "ημέρες πριν|ημέρα πριν|ημέρες πριν"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Subir Archivo",
|
||||
"created-on-date": "Creado el {0}",
|
||||
"unsaved-changes": "Tienes cambios sin guardar. ¿Quieres guardar antes de salir? Aceptar para guardar, Cancelar para descartar cambios.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Descartar Cambios",
|
||||
"discard-changes-description": "Tiene cambios sin guardar. ¿Está seguro que desea descartarlos?",
|
||||
"clipboard-copy-failure": "No se pudo copiar al portapapeles.",
|
||||
"confirm-delete-generic-items": "¿Estás seguro que quieres eliminar los siguientes elementos?",
|
||||
"organizers": "Organizadores",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Aplica para todos los días",
|
||||
"applies-on-days": "Se aplica en {0}s",
|
||||
"meal-plan-settings": "Configuración del Plan de Comidas",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Añadir todos a la lista",
|
||||
"add-day-to-list": "Añadir Día a la Lista"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Datos de migración eliminados",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "¿Sitio web bloqueado?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Intenta importar el HTML en bruto.",
|
||||
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importar categorías originales",
|
||||
"stay-in-edit-mode": "Permanecer en modo edición",
|
||||
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
|
||||
"import-from-zip": "Importar desde zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "es mayor que",
|
||||
"is-greater-than-or-equal-to": "es mayor que o igual a",
|
||||
"is-less-than": "es menor que",
|
||||
"is-less-than-or-equal-to": "es menor que o igual a"
|
||||
"is-less-than-or-equal-to": "es menor que o igual a",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "es",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contiene todo de",
|
||||
"is-like": "es como",
|
||||
"is-not-like": "no es como"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "on suurem kui",
|
||||
"is-greater-than-or-equal-to": "on suurem või võrdne kui",
|
||||
"is-less-than": "on vähem kui",
|
||||
"is-less-than-or-equal-to": "on väiksem või võrdne kui"
|
||||
"is-less-than-or-equal-to": "on väiksem või võrdne kui",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "on",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "sisaldab kõiki",
|
||||
"is-like": "on nagu",
|
||||
"is-not-like": "ei ole nagu"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
"api-port": "API-portti",
|
||||
"application-mode": "Sovellustila",
|
||||
"database-type": "Tietokannan tyyppi",
|
||||
"database-url": "Tietokannan URL",
|
||||
"database-url": "Tietokannan URL-osoite",
|
||||
"default-group": "Oletusryhmä",
|
||||
"default-household": "Oletuskotitalous",
|
||||
"demo": "Demo",
|
||||
"demo-status": "Demon tila",
|
||||
"demo": "Esittelytila",
|
||||
"demo-status": "Esittelytila",
|
||||
"development": "Kehitys",
|
||||
"docs": "Dokumentit",
|
||||
"download-log": "Latausloki",
|
||||
"download-recipe-json": "Viimeisin haettu JSON",
|
||||
"github": "GitHub",
|
||||
"log-lines": "Lokirivit",
|
||||
"not-demo": "Ei demotilassa",
|
||||
"not-demo": "Ei käytössä",
|
||||
"portfolio": "Portfolio",
|
||||
"production": "Tuotanto",
|
||||
"support": "Tuki",
|
||||
@@ -138,7 +138,7 @@
|
||||
"print": "Tulosta",
|
||||
"print-preferences": "Tulostusasetukset",
|
||||
"random": "Satunnainen",
|
||||
"rating": "Arvio",
|
||||
"rating": "Arvosana",
|
||||
"recent": "Viimeisimmät",
|
||||
"recipe": "Resepti",
|
||||
"recipes": "Reseptit",
|
||||
@@ -153,7 +153,7 @@
|
||||
"sort": "Järjestä",
|
||||
"sort-ascending": "Järjestä nousevasti",
|
||||
"sort-descending": "Järjestä laskevasti",
|
||||
"sort-alphabetically": "Aakkosjärjestyksessä",
|
||||
"sort-alphabetically": "Aakkosjärjestys",
|
||||
"status": "Tila",
|
||||
"subject": "Aihe",
|
||||
"submit": "Lähetä",
|
||||
@@ -205,15 +205,15 @@
|
||||
"copied-to-clipboard": "Kopioitu leikepöydälle",
|
||||
"your-browser-does-not-support-clipboard": "Selaimesi ei tue leikepöytää",
|
||||
"copied-items-to-clipboard": "Mitään ei kopioitu leikepöydälle|Kohde kopioitu leikepöydälle|{count} kohdetta kopioitu leikepöydälle",
|
||||
"actions": "Toimet",
|
||||
"actions": "Toiminnot",
|
||||
"selected-count": "Valittu {count}",
|
||||
"export-all": "Vie kaikki",
|
||||
"refresh": "Päivitä",
|
||||
"upload-file": "Tuo tiedosto",
|
||||
"created-on-date": "Luotu {0}",
|
||||
"unsaved-changes": "Et ole tallentanut tekemiäsi muutoksia. Tallennetaanko ne? Paina \"ok\" tallentaaksesi ja \"peruuta\", jos et halua tallentaa.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"unsaved-changes": "Tallenna muutokset? ”Ok” tallentaa, ”Peruuta” hylkää muutokset.",
|
||||
"discard-changes": "Hylkää muutokset",
|
||||
"discard-changes-description": "Muutoksia ei ole tallennettu. Hylätäänkö muutokset?",
|
||||
"clipboard-copy-failure": "Kopioiminen leikepöydälle epäonnistui.",
|
||||
"confirm-delete-generic-items": "Haluatko varmasti poistaa seuraavat kohteet?",
|
||||
"organizers": "Järjestäjät",
|
||||
@@ -712,8 +712,8 @@
|
||||
"toggle-recipe": "Vaihda osio"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Reseptin etsijä",
|
||||
"recipe-finder-description": "Etsi sopivia reseptejä saatavilla olevien ainesosien perusteella. Voit myös suodattaa tulokset saatavilla olevien ruoanvalmistusvälineiden perusteella, ja asettaa enimmäismäärän puuttuvia ainesosia tai välineitä.",
|
||||
"recipe-finder": "Reseptihaku",
|
||||
"recipe-finder-description": "Etsi sopivia reseptejä saatavilla olevien ainesosien perusteella. Voit myös suodattaa tulokset saatavilla olevien keittiövälineiden perusteella, ja asettaa enimmäismäärän puuttuvia ainesosia tai välineitä.",
|
||||
"selected-ingredients": "Valitut ainesosat",
|
||||
"no-ingredients-selected": "Ei valittuja ainesosia",
|
||||
"missing": "Puuttuu",
|
||||
@@ -723,7 +723,7 @@
|
||||
"include-tools-on-hand": "Sisällytä saatavilla olevat välineet",
|
||||
"max-missing-ingredients": "Puuttuvien ainesten enimmäismäärä",
|
||||
"max-missing-tools": "Puuttuvien välineiden enimmäismäärä",
|
||||
"selected-tools": "Valitut välineet",
|
||||
"selected-tools": "Valitut keittiövälineet",
|
||||
"other-filters": "Muut suodattimet",
|
||||
"ready-to-make": "Valmis tekemään",
|
||||
"almost-ready-to-make": "Melkein valmis tekemään"
|
||||
@@ -980,14 +980,14 @@
|
||||
"tag": "Tunniste"
|
||||
},
|
||||
"tool": {
|
||||
"tools": "Työkalut",
|
||||
"on-hand": "Minulla on tämä työkalu",
|
||||
"create-a-tool": "Luo työkalu",
|
||||
"tool-name": "Työkalun Nimi",
|
||||
"create-new-tool": "Luo Uusi Työkalu",
|
||||
"on-hand-checkbox-label": "Näytä työkalut, jotka omistan jo (valittu)",
|
||||
"required-tools": "Tarvittavat Työkalut",
|
||||
"tool": "Työkalu"
|
||||
"tools": "Keittiövälineet",
|
||||
"on-hand": "Omistan välineen",
|
||||
"create-a-tool": "Lisää keittiöväline",
|
||||
"tool-name": "Keittiöväline",
|
||||
"create-new-tool": "Lisää keittiöväline",
|
||||
"on-hand-checkbox-label": "Näytä keittiövälineeni (valittu)",
|
||||
"required-tools": "Tarvittavat keittiövälineet",
|
||||
"tool": "Keittiöväline"
|
||||
},
|
||||
"user": {
|
||||
"admin": "Ylläpitäjä",
|
||||
@@ -1191,9 +1191,9 @@
|
||||
"tag-data": "Tunnisteen tiedot"
|
||||
},
|
||||
"tools": {
|
||||
"new-tool": "Uusi työkalu",
|
||||
"edit-tool": "Muokkaa työkalua",
|
||||
"tool-data": "Työkalun tiedot"
|
||||
"new-tool": "Lisää keittiöväline",
|
||||
"edit-tool": "Muokkaa keittiövälinettä",
|
||||
"tool-data": "Keittiövälineen tiedot"
|
||||
}
|
||||
},
|
||||
"user-registration": {
|
||||
@@ -1239,7 +1239,7 @@
|
||||
"preview-markdown-button-label": "Esikatsele Markdownia"
|
||||
},
|
||||
"demo": {
|
||||
"info_message_with_version": "Tämä on demo: {version}",
|
||||
"info_message_with_version": "Mealie on esittelytilassa. Mealien versio: {version}",
|
||||
"demo_username": "Käyttäjätunnus: {username}",
|
||||
"demo_password": "Salasana: {password}"
|
||||
},
|
||||
@@ -1404,7 +1404,7 @@
|
||||
"filter-options-description": "Kun vaaditaan kaikki on valittu, keittokirja sisältää vain reseptejä, joissa on kaikki valitut tuotteet. Tämä koskee jokaista valitsimien osajoukkoa, ei valittujen kohteiden poikkileikkausta.",
|
||||
"require-all-categories": "Vaadi Kaikki Kategoriat",
|
||||
"require-all-tags": "Vaadi Kaikki Tunnisteet",
|
||||
"require-all-tools": "Vaadi Kaikki Työkalut",
|
||||
"require-all-tools": "Kaikki keittiövälineet tulee löytyä",
|
||||
"cookbook-name": "Keittokirjan Nimi",
|
||||
"cookbook-with-name": "Keittokirja {0}",
|
||||
"household-cookbook-name": "{0} Keittokirja {1}",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "on suurempi kuin",
|
||||
"is-greater-than-or-equal-to": "on suurempi tai yhtäsuuri kuin",
|
||||
"is-less-than": "on vähemmän kuin",
|
||||
"is-less-than-or-equal-to": "on vähemmän tai yhtäsuuri kuin"
|
||||
"is-less-than-or-equal-to": "on vähemmän tai yhtäsuuri kuin",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "on",
|
||||
@@ -1432,13 +1434,16 @@
|
||||
"contains-all-of": "sisältää kaikki nämä",
|
||||
"is-like": "on kuin",
|
||||
"is-not-like": "ei ole kuin"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
"required": "Tämä kenttä on pakollinen",
|
||||
"invalid-email": "Sähköpostiosoite ei ole kelvollinen",
|
||||
"invalid-url": "URL ei ole kelvollinen",
|
||||
"no-whitespace": "No Whitespace Allowed",
|
||||
"no-whitespace": "Tekstissä ei saa olla välilyöntejä",
|
||||
"min-length": "Vähimmäispituus on {min} merkkiä",
|
||||
"max-length": "Enimmäispituus on {max} merkkiä"
|
||||
}
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Transférer un fichier",
|
||||
"created-on-date": "Créé le {0}",
|
||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Annuler les modifications",
|
||||
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||
"organizers": "Classification",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "S'applique à tous les jours",
|
||||
"applies-on-days": "S'applique les {0}s",
|
||||
"meal-plan-settings": "Paramètres des menus",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Ajouter tout à la liste",
|
||||
"add-day-to-list": "Ajouter un jour à la liste"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Données de migration supprimées",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importer les catégories originales",
|
||||
"stay-in-edit-mode": "Rester en mode édition",
|
||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||
"import-from-zip": "Importer depuis un zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "est supérieur à",
|
||||
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
||||
"is-less-than": "est inférieur à",
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "est",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contient tout",
|
||||
"is-like": "est comme",
|
||||
"is-not-like": "n'est pas similaire à"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Téléverser un fichier",
|
||||
"created-on-date": "Créé le {0}",
|
||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous les enregistrer ? Ok pour enregistrer, annuler pour ignorer les modifications.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Annuler les modifications",
|
||||
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||
"clipboard-copy-failure": "Échec de la copie vers le presse-papiers.",
|
||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||
"organizers": "Classification",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "S'applique à tous les jours",
|
||||
"applies-on-days": "S'applique les {0}s",
|
||||
"meal-plan-settings": "Paramètres des menus",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Ajouter tout à la liste",
|
||||
"add-day-to-list": "Ajouter un jour à la liste"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Données de migration supprimées",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importer les catégories originales",
|
||||
"stay-in-edit-mode": "Rester en mode édition",
|
||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||
"import-from-zip": "Importer depuis un zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "est supérieur à",
|
||||
"is-greater-than-or-equal-to": "est supérieur ou égal à",
|
||||
"is-less-than": "est inférieure à",
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "est",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contient tous les",
|
||||
"is-like": "est similaire à",
|
||||
"is-not-like": "n'est pas similaire à"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Téléverser un fichier",
|
||||
"created-on-date": "Créé le {0}",
|
||||
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir ? OK pour enregistrer, Annuler pour ignorer les modifications.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Annuler les modifications",
|
||||
"discard-changes-description": "Vous avez des changements non sauvegardés. Êtes-vous sûr de vouloir les annuler ?",
|
||||
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
|
||||
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
|
||||
"organizers": "Classification",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "S'applique à tous les jours",
|
||||
"applies-on-days": "S'applique les {0}s",
|
||||
"meal-plan-settings": "Paramètres des menus",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Ajouter tout à la liste",
|
||||
"add-day-to-list": "Ajouter un jour à la liste"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Données de migration supprimées",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importer les catégories originales",
|
||||
"stay-in-edit-mode": "Rester en mode édition",
|
||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||
"import-from-zip": "Importer depuis un zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "est supérieur à",
|
||||
"is-greater-than-or-equal-to": "est plus grand que ou égal à",
|
||||
"is-less-than": "est inférieur à",
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à"
|
||||
"is-less-than-or-equal-to": "est inférieur ou égal à",
|
||||
"is-older-than": "est plus ancien que",
|
||||
"is-newer-than": "est plus récent que"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "est",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contient tout",
|
||||
"is-like": "est comme",
|
||||
"is-not-like": "n'est pas similaire à"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "jours|jour|jours"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "é maior que",
|
||||
"is-greater-than-or-equal-to": "é maior ou igual a",
|
||||
"is-less-than": "é menor que",
|
||||
"is-less-than-or-equal-to": "é menor ou igual a"
|
||||
"is-less-than-or-equal-to": "é menor ou igual a",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "é",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contén todos os",
|
||||
"is-like": "é como",
|
||||
"is-not-like": "non é como"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "גדול מ-",
|
||||
"is-greater-than-or-equal-to": "גדול או שווה ל-",
|
||||
"is-less-than": "קטן מ-",
|
||||
"is-less-than-or-equal-to": "קטן או שווה ל-"
|
||||
"is-less-than-or-equal-to": "קטן או שווה ל-",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "זהה ל-",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "מכיל הכל מתוך",
|
||||
"is-like": "דומה ל-",
|
||||
"is-not-like": "לא דומה לא-"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "nagyobb, mint",
|
||||
"is-greater-than-or-equal-to": "nagyobb vagy egyenlő",
|
||||
"is-less-than": "kevesebb, mint",
|
||||
"is-less-than-or-equal-to": "kevesebb vagy egyenlő"
|
||||
"is-less-than-or-equal-to": "kevesebb vagy egyenlő",
|
||||
"is-older-than": "régebbi, mint",
|
||||
"is-newer-than": "újabb, mint"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "van",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "tartalmazza az összes",
|
||||
"is-like": "hasonló",
|
||||
"is-not-like": "nem hasonló"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "napja|napja|napja"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Hlaða upp skrá",
|
||||
"created-on-date": "Búið til: {0}",
|
||||
"unsaved-changes": "Þú hefur ekki vistað breytingar. Viltu vista áður en þú ferð? Ýttu á \"Í lagi\" til að vista, \"Hætta við\" til að henda breytingum.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Henda breytingum",
|
||||
"discard-changes-description": "Þú ert með óvistaðar breytingar, ertu viss um að þú viljir henda þeim?",
|
||||
"clipboard-copy-failure": "Mistókst að afrita klippispjaldið.",
|
||||
"confirm-delete-generic-items": "Ertu viss um að þú viljir eyða eftirfylgjandi atriðum?",
|
||||
"organizers": "Skipuleggjarar",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Á við alla daga",
|
||||
"applies-on-days": "Gildir þegar er {0},",
|
||||
"meal-plan-settings": "Stillingar matarplans",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Bæta öllum á innkaupalista",
|
||||
"add-day-to-list": "Bæta deginum á innkaupalista"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Gagnaflutningur fjarlægður",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "Er vefsíðan lokuð?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Reyndu að flytja inn HTML kóðann í staðinn.",
|
||||
"import-original-keywords-as-tags": "Nota upprunanleg merki",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Flytja inn upprunalega flokka",
|
||||
"stay-in-edit-mode": "Vera í breytingarham",
|
||||
"parse-recipe-ingredients-after-import": "Greina innhald uppskriftar eftir að búið er að hlaða inn uppskrift",
|
||||
"import-from-zip": "Hlaða inn frá .zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "eftir þann",
|
||||
"is-greater-than-or-equal-to": "þann eða eftir þann",
|
||||
"is-less-than": "fyrir þann",
|
||||
"is-less-than-or-equal-to": "fyrir þann eða þann"
|
||||
"is-less-than-or-equal-to": "fyrir þann eða þann",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "er",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "inniheldur alla af",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"about": "Dettagli",
|
||||
"about-mealie": "Info su Mealie",
|
||||
"api-docs": "Documentazione API",
|
||||
"api-port": "Porta API",
|
||||
"api-port": "Importazione API",
|
||||
"application-mode": "Modalità",
|
||||
"database-type": "Tipo Database",
|
||||
"database-url": "URL Database",
|
||||
@@ -97,7 +97,7 @@
|
||||
"custom": "Personalizzato",
|
||||
"dashboard": "Pannello di controllo",
|
||||
"delete": "Elimina",
|
||||
"disabled": "Disabilitato",
|
||||
"disabled": "Disabilita",
|
||||
"download": "Download",
|
||||
"duplicate": "Duplicato",
|
||||
"edit": "Modifica",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Si applica a ogni giorno",
|
||||
"applies-on-days": "Si applica ai {0}",
|
||||
"meal-plan-settings": "Impostazioni del piano alimentare",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Aggiungi tutto alla lista",
|
||||
"add-day-to-list": "Aggiungi Giorno"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Dati di migrazione rimossi",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "è maggiore di",
|
||||
"is-greater-than-or-equal-to": "è maggiore o uguale di",
|
||||
"is-less-than": "è minore di",
|
||||
"is-less-than-or-equal-to": "è minore o uguale di"
|
||||
"is-less-than-or-equal-to": "è minore o uguale di",
|
||||
"is-older-than": "è più vecchio di",
|
||||
"is-newer-than": "è più recente di"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "è",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contiene tutti i",
|
||||
"is-like": "è simile",
|
||||
"is-not-like": "non è come"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "giorni fa|giorno fa|giorni fa"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "より大きい",
|
||||
"is-greater-than-or-equal-to": "以上",
|
||||
"is-less-than": "より小さい",
|
||||
"is-less-than-or-equal-to": "以下"
|
||||
"is-less-than-or-equal-to": "以下",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "は",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "すべてを含む",
|
||||
"is-like": "次のようなものです",
|
||||
"is-not-like": "というわけではありません"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"about": "정보",
|
||||
"about": "약/대략",
|
||||
"about-mealie": "Mealie에 대해",
|
||||
"api-docs": "API 문서",
|
||||
"api-port": "API 포트",
|
||||
@@ -14,7 +14,7 @@
|
||||
"development": "개발",
|
||||
"docs": "문서",
|
||||
"download-log": "다운로드 기록",
|
||||
"download-recipe-json": "마지막으로 불러온 JSON",
|
||||
"download-recipe-json": "마지막으로 스크랩한 JSON",
|
||||
"github": "GitHub",
|
||||
"log-lines": "로그 줄",
|
||||
"not-demo": "데모 아님",
|
||||
@@ -67,10 +67,10 @@
|
||||
"test-message-sent": "테스트 메시지가 전송됐습니다.",
|
||||
"message-sent": "메세지가 전송됨",
|
||||
"new-notification": "새 알림",
|
||||
"event-notifiers": "이벤트 알림이",
|
||||
"event-notifiers": "이벤트 알리미",
|
||||
"apprise-url-skipped-if-blank": "Apprise URL (비워두면 생략합니다)",
|
||||
"apprise-url-is-left-intentionally-blank": "Apprise URL에는 일반적으로 민감한 정보가 포함되므로, 편집 시 이 필드는 의도적으로 비워둡니다. URL을 수정하려면 여기에 새 주소를 입력하시고, 현재 URL을 유지하려면 비워두십시오.",
|
||||
"enable-notifier": "알림 활성화",
|
||||
"enable-notifier": "알리미 활성화",
|
||||
"what-events": "이 알리미는 어떤 이벤트를 구독해야 합니까?",
|
||||
"user-events": "사용자 이벤트",
|
||||
"mealplan-events": "Mealplan 이벤트",
|
||||
@@ -193,7 +193,7 @@
|
||||
"delete-with-name": "{name} 삭제",
|
||||
"confirm-delete-generic-with-name": "{name}을(를) 정말 삭제하시겠습니까?",
|
||||
"confirm-delete-own-admin-account": "본인의 관리자 계정을 삭제하려고 한다는 점에 유의하세요! 이 작업은 취소할 수 없으며 계정이 영구적으로 삭제됩니다.",
|
||||
"organizer": "정리 도구",
|
||||
"organizer": "정리함",
|
||||
"transfer": "전송",
|
||||
"copy": "복사",
|
||||
"color": "색상",
|
||||
@@ -216,7 +216,7 @@
|
||||
"discard-changes-description": "저장되지 않은 변경사항이 있습니다. 삭제하시겠습니까?",
|
||||
"clipboard-copy-failure": "클립보드에 복사하는 데 실패했습니다.",
|
||||
"confirm-delete-generic-items": "이 항목을 삭제하시겠습니까?",
|
||||
"organizers": "정리 도구",
|
||||
"organizers": "정리함",
|
||||
"caution": "주의",
|
||||
"show-advanced": "고급 표시",
|
||||
"add-field": "필드 추가",
|
||||
@@ -339,7 +339,7 @@
|
||||
"side": "사이드",
|
||||
"sides": "사이드",
|
||||
"start-date": "시작 일자",
|
||||
"rule-day": "규칙의 날",
|
||||
"rule-day": "규칙을 적용할 요일",
|
||||
"meal-type": "식사 유형",
|
||||
"breakfast": "조식",
|
||||
"lunch": "점심",
|
||||
@@ -357,14 +357,14 @@
|
||||
"random-meal": "랜덤 식사",
|
||||
"random-dinner": "랜덤 저녁식사",
|
||||
"random-side": "랜덤 사이드 메뉴",
|
||||
"this-rule-will-apply": "이 규칙은 {dayCriteria} {mealTypeCriteria}에 적용됩니다.",
|
||||
"this-rule-will-apply": "이 규칙은 {dayCriteria} {mealTypeCriteria} 적용됩니다.",
|
||||
"to-all-days": "모든 날에",
|
||||
"on-days": "{0}에",
|
||||
"for-all-meal-types": "모든 식사 종류에",
|
||||
"for-type-meal-types": "{0} 식사 종류에",
|
||||
"meal-plan-rules": "식단 계획 규칙",
|
||||
"new-rule": "새 규칙",
|
||||
"meal-plan-rules-description": "식단 계획에 사용할 레시피를 자동 선택하는 규칙을 생성할 수 있습니다. 이 규칙들은 서버가 식단 계획을 생성할 때 선택할 무작위 레시피 풀을 결정하는 데 사용됩니다. 동일한 요일/유형 제약 조건을 가진 규칙들은 필터가 병합된다는 점에 유의하세요. 실제로 중복 규칙을 생성할 필요는 없지만, 생성하는 것은 가능합니다.",
|
||||
"meal-plan-rules-description": "식단 계획에 사용할 레시피를 자동으로 선택하기 위한 규칙을 만들 수 있습니다. 이러한 규칙은 서버에서 식단 계획을 생성할 때 무작위로 선택할 레시피 목록을 결정하는 데 사용됩니다. 규칙에 동일한 요일/식사 유형 조건이 있는 경우 규칙 필터가 병합된다는 점에 유의하십시오. 실제로 중복 규칙을 만들 필요는 없지만, 만드는 것도 가능합니다.",
|
||||
"new-rule-description": "식단 계획에 새 규칙을 생성할 때, 특정 요일 및/또는 특정 식사 유형에만 적용되도록 규칙을 제한할 수 있습니다. 모든 요일 또는 모든 식사 유형에 규칙을 적용하려면 규칙을 \"모든\"으로 설정하면 됩니다. 이렇게 하면 해당 요일 및/또는 식사 유형의 모든 가능한 값에 규칙이 적용됩니다.",
|
||||
"recipe-rules": "레시피 규칙",
|
||||
"applies-to-all-days": "모든 날짜에 적용됨",
|
||||
@@ -375,7 +375,7 @@
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "이전된 데이터 제거됨",
|
||||
"new-migration": "새 마이그레이션",
|
||||
"new-migration": "새 데이터 이전",
|
||||
"no-file-selected": "선택된 파일이 없습니다",
|
||||
"no-migration-data-available": "이전 데이터가 없습니다.",
|
||||
"previous-migrations": "이전 데이터 이전",
|
||||
@@ -392,19 +392,19 @@
|
||||
},
|
||||
"copymethat": {
|
||||
"description-long": "Mealie는 Copy Me That에서 레시피를 가져올 수 있습니다. 레시피를 HTML 형식으로 내보낸 후, 아래의 .zip 파일을 업로드하세요.",
|
||||
"title": "Copy Me That 레시피 매니저"
|
||||
"title": "Copy Me That Recipe Manager"
|
||||
},
|
||||
"paprika": {
|
||||
"description-long": "Mealie는 Paprika 애플리케이션에서 레시피를 가져올 수 있습니다. Paprika에서 레시피를 내보낸 후, 내보낸 파일의 확장자를 .zip으로 변경하여 아래에 업로드하세요.",
|
||||
"title": "Paprika 레시피 매니저"
|
||||
"title": "Paprika Recipe Manager"
|
||||
},
|
||||
"mealie-pre-v1": {
|
||||
"description-long": "Mealie는 v1.0 이전 버전의 Mealie 애플리케이션에서 레시피를 가져올 수 있습니다. 기존 인스턴스에서 레시피를 내보낸 후 아래의 zip 파일을 업로드하세요. 내보낸 파일에서 레시피만 가져올 수 있다는 점에 유의하십시오.",
|
||||
"description-long": "Mealie는 v1.0 이전 버전의 Mealie 애플리케이션에서 레시피를 가져올 수 있습니다. 기존 인스턴스에서 레시피를 내보낸 후 아래에 있는 압축 파일을 업로드하세요. 내보내기 파일에서는 레시피만 가져올 수 있다는 점에 유의하세요.",
|
||||
"title": "Mealie Pre v1.0"
|
||||
},
|
||||
"tandoor": {
|
||||
"description-long": "Mealie는 Tandoor에서 레시피를 가져올 수 있습니다. 데이터를 \"기본\" 형식으로 내보낸 후 아래의 .zip 파일을 업로드하세요.",
|
||||
"title": "Tandoor 레시피"
|
||||
"title": "Tandoor Recipes"
|
||||
},
|
||||
"cookn": {
|
||||
"description-long": "Mealie는 DVO Cook'n X3의 레시피를 가져올 수 있습니다. \"Cook'n\" 형식으로 요리책이나 메뉴를 내보낸 후, 내보낸 파일의 확장자를 .zip으로 변경하고 아래에 .zip 파일을 업로드하세요.",
|
||||
@@ -420,17 +420,17 @@
|
||||
"recipe-1": "레시피 1",
|
||||
"recipe-2": "레시피 2",
|
||||
"paprika-text": "Mealie는 Paprika 애플리케이션에서 레시피를 가져올 수 있습니다. Paprika에서 레시피를 내보낸 후, 내보낸 파일의 확장자를 .zip으로 변경하여 아래에 업로드하세요.",
|
||||
"mealie-text": "Mealie는 v1.0 이전 버전의 Mealie 애플리케이션에서 레시피를 가져올 수 있습니다. 기존 인스턴스에서 레시피를 내보낸 후 아래의 zip 파일을 업로드하세요. 내보낸 파일에서 레시피만 가져올 수 있다는 점에 유의하십시오.",
|
||||
"mealie-text": "Mealie는 v1.0 이전 버전의 Mealie 애플리케이션에서 레시피를 가져올 수 있습니다. 기존 인스턴스에서 레시피를 내보낸 후 아래에 있는 압축 파일을 업로드하세요. 내보내기 파일에서는 레시피만 가져올 수 있다는 점에 유의하세요.",
|
||||
"plantoeat": {
|
||||
"title": "Plan to Eat",
|
||||
"description-long": "Mealie는 Plan to Eat에서 레시피를 가져올 수 있습니다."
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "내 레시피 박스",
|
||||
"title": "My Recipe Box",
|
||||
"description-long": "Mealie는 My Recipe Box에서 레시피를 가져올 수 있습니다. CSV 형식으로 레시피를 내보낸 다음 아래의 .csv 파일을 업로드하세요."
|
||||
},
|
||||
"recipekeeper": {
|
||||
"title": "레시피 보관함",
|
||||
"title": "Recipe Keeper",
|
||||
"description-long": "Mealie는 Recipe Keeper에서 레시피를 가져올 수 있습니다. 레시피를 zip 형식으로 내보낸 다음 아래의 .zip 파일을 업로드하세요."
|
||||
}
|
||||
},
|
||||
@@ -712,7 +712,7 @@
|
||||
"toggle-recipe": "레시피로 전환"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "레시피 찾기",
|
||||
"recipe-finder": "레시피 검색",
|
||||
"recipe-finder-description": "가지고 있는 재료로 레시피를 검색하세요. 사용 가능한 도구로도 필터링할 수 있으며, 부족한 재료나 도구의 최대 개수를 설정할 수 있습니다.",
|
||||
"selected-ingredients": "선택된 재료",
|
||||
"no-ingredients-selected": "선택된 재료 없음",
|
||||
@@ -794,7 +794,7 @@
|
||||
"latest": "가장 최근",
|
||||
"local-api": "로컬 API",
|
||||
"locale-settings": "국가별 설정",
|
||||
"migrations": "마이그레이션",
|
||||
"migrations": "데이터 이전",
|
||||
"new-page": "새 페이지",
|
||||
"notify": "알림",
|
||||
"organize": "정리",
|
||||
@@ -1395,7 +1395,7 @@
|
||||
},
|
||||
"cookbook": {
|
||||
"cookbooks": "요리책",
|
||||
"description": "요리책은 레시피, 정리 도구 및 기타 필터를 교차 조합하여 레시피를 체계화하는 또 다른 방법입니다. 요리책을 생성하면 사이드바에 항목이 추가되며, 선택한 필터가 적용된 모든 레시피가 해당 요리책에 표시됩니다.",
|
||||
"description": "요리책은 레시피, 정리함 및 기타 필터를 교차 조합하여 레시피를 체계화하는 또 다른 방법입니다. 요리책을 생성하면 사이드바에 항목이 추가되며, 선택한 필터가 적용된 모든 레시피가 해당 요리책에 표시됩니다.",
|
||||
"hide-cookbooks-from-other-households": "다른 가구의 요리책 숨기기",
|
||||
"hide-cookbooks-from-other-households-description": "이 기능을 활성화하면 사이드바에는 귀하의 가구에 속한 요리책만 표시됩니다.",
|
||||
"public-cookbook": "공개 요리책",
|
||||
@@ -1419,19 +1419,24 @@
|
||||
"relational-operators": {
|
||||
"equals": "정확히 일치",
|
||||
"does-not-equal": "일치하지 않음",
|
||||
"is-greater-than": "은(는) 다음보다 크다:",
|
||||
"is-greater-than-or-equal-to": "은(는) 다음보다 크거나 같다:",
|
||||
"is-less-than": "은(는) 다음보다 작다:",
|
||||
"is-less-than-or-equal-to": "은(는) 다음보다 작거나 같다:"
|
||||
"is-greater-than": "다음보다 큼",
|
||||
"is-greater-than-or-equal-to": "다음보다 크거나 같음",
|
||||
"is-less-than": "다음보다 작음",
|
||||
"is-less-than-or-equal-to": "다음보다 작거나 같음",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "은(는)",
|
||||
"is-not": "은(는) 아니다",
|
||||
"is-one-of": "은(는) 다음에 포함:",
|
||||
"is-not-one-of": "은(는) 다음에 포함되지 않음:",
|
||||
"contains-all-of": "은(는) 다음 모두를 포함:",
|
||||
"is-like": "은(는) 다음과 같음:",
|
||||
"is-not-like": "은(는) 다음과 같지 않음:"
|
||||
"is": "같음",
|
||||
"is-not": "아님",
|
||||
"is-one-of": "다음에 포함",
|
||||
"is-not-one-of": "다음에 포함되지 않음",
|
||||
"contains-all-of": "다음 모두를 포함",
|
||||
"is-like": "다음과 같음",
|
||||
"is-not-like": "다음과 같지 않음"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "ir lielāks par",
|
||||
"is-greater-than-or-equal-to": "ir lielāks vai vienāds ar",
|
||||
"is-less-than": "ir mazāks par",
|
||||
"is-less-than-or-equal-to": "ir mazāks vai vienāds ar"
|
||||
"is-less-than-or-equal-to": "ir mazāks vai vienāds ar",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "IR",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "satur visus",
|
||||
"is-like": "ir kā",
|
||||
"is-not-like": "nav tāds, kā"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is groter dan",
|
||||
"is-greater-than-or-equal-to": "is groter dan of gelijk aan",
|
||||
"is-less-than": "is kleiner dan",
|
||||
"is-less-than-or-equal-to": "is kleiner dan of gelijk aan"
|
||||
"is-less-than-or-equal-to": "is kleiner dan of gelijk aan",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "bevat alles van",
|
||||
"is-like": "is zoals",
|
||||
"is-not-like": "is niet zoals"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "er større enn",
|
||||
"is-greater-than-or-equal-to": "er større enn eller lik",
|
||||
"is-less-than": "er mindre enn",
|
||||
"is-less-than-or-equal-to": "er mindre enn eller lik"
|
||||
"is-less-than-or-equal-to": "er mindre enn eller lik",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "er",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "inneholder alle",
|
||||
"is-like": "er som",
|
||||
"is-not-like": "er ikke som"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Dotyczy wszystkich dni",
|
||||
"applies-on-days": "Dotyczy {0}",
|
||||
"meal-plan-settings": "Ustawienia planera posiłków",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Dodaj wszystko do listy",
|
||||
"add-day-to-list": "Dodaj Dzień do Listy"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Dane migracji usunięte",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "jest większe niż",
|
||||
"is-greater-than-or-equal-to": "jest większe lub równe",
|
||||
"is-less-than": "jest mniejsze niż",
|
||||
"is-less-than-or-equal-to": "jest mniejsze lub równe"
|
||||
"is-less-than-or-equal-to": "jest mniejsze lub równe",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "jest",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "zawiera wszystkie z",
|
||||
"is-like": "jest jak",
|
||||
"is-not-like": "nie jest jak"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "é maior que",
|
||||
"is-greater-than-or-equal-to": "é maior ou igual a",
|
||||
"is-less-than": "é menor que",
|
||||
"is-less-than-or-equal-to": "é menor ou igual a"
|
||||
"is-less-than-or-equal-to": "é menor ou igual a",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "é",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contém todos de",
|
||||
"is-like": "é como",
|
||||
"is-not-like": "não é como"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"about": {
|
||||
"about": "Sobre",
|
||||
"about-mealie": "Sobre o Mealie",
|
||||
"about-mealie": "Sobre Mealie",
|
||||
"api-docs": "Documentação de API",
|
||||
"api-port": "Porta da API",
|
||||
"application-mode": "Modo de aplicação",
|
||||
@@ -16,7 +16,7 @@
|
||||
"download-log": "Transferir Log",
|
||||
"download-recipe-json": "Último JSON recuperado",
|
||||
"github": "GitHub",
|
||||
"log-lines": "Linhas de registo",
|
||||
"log-lines": "Linhas de Logs",
|
||||
"not-demo": "Não Demonstração",
|
||||
"portfolio": "Portefólio",
|
||||
"production": "Produção",
|
||||
@@ -212,8 +212,8 @@
|
||||
"upload-file": "Carregar ficheiro",
|
||||
"created-on-date": "Criado em: {0}",
|
||||
"unsaved-changes": "Tem alterações por gravar. Quer gravar antes de sair? OK para gravar, Cancelar para descartar alterações.",
|
||||
"discard-changes": "Discard Changes",
|
||||
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"discard-changes": "Descartar alterações",
|
||||
"discard-changes-description": "Tem alterações por gravar. De certeza que as quer descartar?",
|
||||
"clipboard-copy-failure": "Erro ao copiar para a área de transferência.",
|
||||
"confirm-delete-generic-items": "Tem a certeza de que deseja eliminar os seguintes itens?",
|
||||
"organizers": "Organizadores",
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Aplica-se a todos os dias",
|
||||
"applies-on-days": "Aplica-se em {0}s",
|
||||
"meal-plan-settings": "Definições do Plano de Refeições",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Adicionar todos",
|
||||
"add-day-to-list": "Adicionar Dia"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Dados de migração removidos",
|
||||
@@ -644,7 +644,7 @@
|
||||
"scrape-recipe-website-being-blocked": "O site está bloqueado?",
|
||||
"scrape-recipe-try-importing-raw-html-instead": "Tente importar o HTML bruto em vez disso.",
|
||||
"import-original-keywords-as-tags": "Importar palavras-chave originais como etiquetas",
|
||||
"import-original-categories": "Import original categories",
|
||||
"import-original-categories": "Importar categorias originais",
|
||||
"stay-in-edit-mode": "Permanecer no modo de edição",
|
||||
"parse-recipe-ingredients-after-import": "Analisar ingredientes da receita após a importação",
|
||||
"import-from-zip": "Importar de Zip",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "é maior que",
|
||||
"is-greater-than-or-equal-to": "é maior ou igual a",
|
||||
"is-less-than": "é menor que",
|
||||
"is-less-than-or-equal-to": "é menor ou igual a"
|
||||
"is-less-than-or-equal-to": "é menor ou igual a",
|
||||
"is-older-than": "é mais antigo que",
|
||||
"is-newer-than": "é mais recente que"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "é",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contém todos os",
|
||||
"is-like": "é como",
|
||||
"is-not-like": "não é como"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "dias atrás|dia atrás|dias atrás"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "este mai mare ca",
|
||||
"is-greater-than-or-equal-to": "este mai mare sau egală cu",
|
||||
"is-less-than": "este mai mic decât",
|
||||
"is-less-than-or-equal-to": "este mai mic sau egal cu"
|
||||
"is-less-than-or-equal-to": "este mai mic sau egal cu",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "este",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "conţine toate din",
|
||||
"is-like": "este similar",
|
||||
"is-not-like": "nu este similar"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "больше чем",
|
||||
"is-greater-than-or-equal-to": "больше или равно",
|
||||
"is-less-than": "меньше чем",
|
||||
"is-less-than-or-equal-to": "меньше или равно"
|
||||
"is-less-than-or-equal-to": "меньше или равно",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "соответствует",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "содержит все",
|
||||
"is-like": "содержит",
|
||||
"is-not-like": "не содержит"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "je väčšie ako",
|
||||
"is-greater-than-or-equal-to": "je väčšie alebo rovné",
|
||||
"is-less-than": "je menšie ako",
|
||||
"is-less-than-or-equal-to": "je menšie alebo rovné"
|
||||
"is-less-than-or-equal-to": "je menšie alebo rovné",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "je",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "obsahuje všetky",
|
||||
"is-like": "je ako",
|
||||
"is-not-like": "nie je ako"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "je več kot",
|
||||
"is-greater-than-or-equal-to": "je več kot ali enako kot",
|
||||
"is-less-than": "je manj kot",
|
||||
"is-less-than-or-equal-to": "je manj kot ali enako kot"
|
||||
"is-less-than-or-equal-to": "je manj kot ali enako kot",
|
||||
"is-older-than": "je starejše od",
|
||||
"is-newer-than": "je novejše od"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "je",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "vsebuje vse",
|
||||
"is-like": "je kot",
|
||||
"is-not-like": "ni kot"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "pred dnevi|dan nazaj|pred dnevi"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "је веће од",
|
||||
"is-greater-than-or-equal-to": "је веће или једнако са",
|
||||
"is-less-than": "је мање од",
|
||||
"is-less-than-or-equal-to": "је мање или једнако са"
|
||||
"is-less-than-or-equal-to": "је мање или једнако са",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "је",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "садржи све из",
|
||||
"is-like": "је као",
|
||||
"is-not-like": "није као"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -370,8 +370,8 @@
|
||||
"applies-to-all-days": "Gäller för alla dagar",
|
||||
"applies-on-days": "Gäller på {0}s",
|
||||
"meal-plan-settings": "Inställningar för måltidsplanering",
|
||||
"add-all-to-list": "Add All to List",
|
||||
"add-day-to-list": "Add Day to List"
|
||||
"add-all-to-list": "Lägg till alla i inköpslista",
|
||||
"add-day-to-list": "Lägg till dag i inköpslista"
|
||||
},
|
||||
"migration": {
|
||||
"migration-data-removed": "Importerad data borttagen",
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "är större än",
|
||||
"is-greater-than-or-equal-to": "är större än eller lika med",
|
||||
"is-less-than": "är mindre än",
|
||||
"is-less-than-or-equal-to": "är mindre eller lika med"
|
||||
"is-less-than-or-equal-to": "är mindre eller lika med",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "är",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "innehåller alla",
|
||||
"is-like": "är som",
|
||||
"is-not-like": "är inte som"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "daha büyük",
|
||||
"is-greater-than-or-equal-to": "daha büyük veya eşit",
|
||||
"is-less-than": "daha mı az",
|
||||
"is-less-than-or-equal-to": "daha az veya eşit mi"
|
||||
"is-less-than-or-equal-to": "daha az veya eşit mi",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "hepsini içeriyor mu",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "більше ніж",
|
||||
"is-greater-than-or-equal-to": "більше або дорівнює",
|
||||
"is-less-than": "менше ніж",
|
||||
"is-less-than-or-equal-to": "менше або дорівнює"
|
||||
"is-less-than-or-equal-to": "менше або дорівнює",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "є",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "містить усі",
|
||||
"is-like": "схожий",
|
||||
"is-not-like": "не схожий"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "大于",
|
||||
"is-greater-than-or-equal-to": "大于等于",
|
||||
"is-less-than": "小于",
|
||||
"is-less-than-or-equal-to": "小于等于"
|
||||
"is-less-than-or-equal-to": "小于等于",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "是",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "包含所有",
|
||||
"is-like": "匹配",
|
||||
"is-not-like": "不匹配"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -1422,7 +1422,9 @@
|
||||
"is-greater-than": "is greater than",
|
||||
"is-greater-than-or-equal-to": "is greater than or equal to",
|
||||
"is-less-than": "is less than",
|
||||
"is-less-than-or-equal-to": "is less than or equal to"
|
||||
"is-less-than-or-equal-to": "is less than or equal to",
|
||||
"is-older-than": "is older than",
|
||||
"is-newer-than": "is newer than"
|
||||
},
|
||||
"relational-keywords": {
|
||||
"is": "is",
|
||||
@@ -1432,6 +1434,9 @@
|
||||
"contains-all-of": "contains all of",
|
||||
"is-like": "is like",
|
||||
"is-not-like": "is not like"
|
||||
},
|
||||
"dates": {
|
||||
"days-ago": "days ago|day ago|days ago"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
|
||||
@@ -64,7 +64,7 @@ export default defineNuxtComponent({
|
||||
});
|
||||
|
||||
const i18n = useGlobalI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const ready = ref(false);
|
||||
|
||||
@@ -72,7 +72,7 @@ export default defineNuxtComponent({
|
||||
const router = useRouter();
|
||||
|
||||
async function insertGroupSlugIntoRoute() {
|
||||
const groupSlug = ref($auth.user.value?.groupSlug);
|
||||
const groupSlug = ref(auth.user.value?.groupSlug);
|
||||
if (!groupSlug.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ export enum Organizer {
|
||||
User = "users",
|
||||
}
|
||||
|
||||
export type LogicalOperator = "AND" | "OR";
|
||||
export type PlaceholderKeyword = "$NOW";
|
||||
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
|
||||
export type LogicalOperator = "AND" | "OR";
|
||||
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
|
||||
|
||||
export interface QueryFilterJSON {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user