mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 16:24:31 -04:00
feat: Ingredient Parser Enhancements (#6228)
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
:placeholder="$t('recipe.quantity')"
|
:placeholder="$t('recipe.quantity')"
|
||||||
@keypress="quantityFilter"
|
@keypress="quantityFilter"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template v-if="enableDragHandle" #prepend>
|
||||||
<v-icon
|
<v-icon
|
||||||
class="mr-n1 handle"
|
class="mr-n1 handle"
|
||||||
>
|
>
|
||||||
@@ -178,6 +178,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<slot name="before-divider" />
|
||||||
<v-divider
|
<v-divider
|
||||||
v-if="!mdAndUp"
|
v-if="!mdAndUp"
|
||||||
class="my-4"
|
class="my-4"
|
||||||
@@ -196,7 +197,7 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
|||||||
// defineModel replaces modelValue prop
|
// defineModel replaces modelValue prop
|
||||||
const model = defineModel<RecipeIngredient>({ required: true });
|
const model = defineModel<RecipeIngredient>({ required: true });
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
unitError: {
|
unitError: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -217,6 +218,14 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableDragHandle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
deleteDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits([
|
defineEmits([
|
||||||
@@ -270,8 +279,8 @@ const btns = computed(() => {
|
|||||||
text: i18n.t("general.delete"),
|
text: i18n.t("general.delete"),
|
||||||
event: "delete",
|
event: "delete",
|
||||||
children: undefined,
|
children: undefined,
|
||||||
|
disabled: props.deleteDisabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ import { useUserApi } from "~/composables/api";
|
|||||||
import { uuid4, deepCopy } from "~/composables/use-utils";
|
import { uuid4, deepCopy } from "~/composables/use-utils";
|
||||||
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||||
import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
|
import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
|
||||||
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useNavigationWarning } from "~/composables/use-navigation-warning";
|
import { useNavigationWarning } from "~/composables/use-navigation-warning";
|
||||||
|
|
||||||
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
||||||
@@ -200,6 +201,7 @@ const display = useDisplay();
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
@@ -258,11 +260,11 @@ const paramsEdit = useRouteQuery<BooleanString>("edit", "");
|
|||||||
const paramsParse = useRouteQuery<BooleanString>("parse", "");
|
const paramsParse = useRouteQuery<BooleanString>("parse", "");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (paramsEdit.value === "true") {
|
if (paramsEdit.value === "true" && isOwnGroup.value) {
|
||||||
setMode(PageMode.EDIT);
|
setMode(PageMode.EDIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paramsParse.value === "true") {
|
if (paramsParse.value === "true" && isOwnGroup.value) {
|
||||||
toggleIsParsing(true);
|
toggleIsParsing(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||||
:key="ingredient.referenceId"
|
:key="ingredient.referenceId"
|
||||||
v-model="recipe.recipeIngredient[index]"
|
v-model="recipe.recipeIngredient[index]"
|
||||||
|
enable-drag-handle
|
||||||
enable-context-menu
|
enable-context-menu
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||||
|
|||||||
@@ -6,106 +6,107 @@
|
|||||||
@update:model-value="emit('update:modelValue', $event)"
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
<v-container class="pa-2 ma-0" style="background-color: rgb(var(--v-theme-background));">
|
<v-container class="pa-2 ma-0" style="background-color: rgb(var(--v-theme-background));">
|
||||||
<BaseCardSectionTitle :title="$t('recipe.parser.ingredient-parser')">
|
<div v-if="state.loading.parser" class="my-6">
|
||||||
<div v-if="!state.allReviewed" class="mb-4">
|
<AppLoader waiting-text="" class="my-6" />
|
||||||
<p>{{ $t("recipe.parser.ingredient-parser-description") }}</p>
|
</div>
|
||||||
<p>{{ $t("recipe.parser.ingredient-parser-final-review-description") }}</p>
|
<div v-else>
|
||||||
</div>
|
<BaseCardSectionTitle :title="$t('recipe.parser.ingredient-parser')">
|
||||||
<div class="d-flex flex-wrap align-center">
|
<div v-if="!state.allReviewed" class="mb-4">
|
||||||
<div class="text-body-2 mr-2">
|
<p>{{ $t("recipe.parser.ingredient-parser-description") }}</p>
|
||||||
{{ $t("recipe.parser.select-parser") }}
|
<p>{{ $t("recipe.parser.ingredient-parser-final-review-description") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex flex-wrap align-center">
|
||||||
<BaseOverflowButton
|
<div class="text-body-2 mr-2">
|
||||||
v-model="parser"
|
{{ $t("recipe.parser.select-parser") }}
|
||||||
:disabled="state.loading.parser"
|
</div>
|
||||||
btn-class="mx-2"
|
<div class="d-flex align-center">
|
||||||
:items="availableParsers"
|
<BaseOverflowButton
|
||||||
|
v-model="parser"
|
||||||
|
:disabled="state.loading.parser"
|
||||||
|
btn-class="mx-2"
|
||||||
|
:items="availableParsers"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="40"
|
||||||
|
color="info"
|
||||||
|
:disabled="state.loading.parser"
|
||||||
|
@click="parseIngredients"
|
||||||
|
>
|
||||||
|
<v-icon>{{ $globals.icons.refresh }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseCardSectionTitle>
|
||||||
|
<v-card v-if="!state.allReviewed && currentIng">
|
||||||
|
<v-card-text class="pb-0 mb-0">
|
||||||
|
<div class="text-center px-8 py-4 mb-6">
|
||||||
|
<p class="text-h5 font-italic">
|
||||||
|
{{ currentIng.input }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center pa-0 ma-0">
|
||||||
|
<v-icon
|
||||||
|
:color="(currentIng.confidence?.average || 0) < confidenceThreshold ? 'error' : 'success'"
|
||||||
|
>
|
||||||
|
{{ (currentIng.confidence?.average || 0) < confidenceThreshold ? $globals.icons.alert : $globals.icons.check }}
|
||||||
|
</v-icon>
|
||||||
|
<span
|
||||||
|
class="ml-2"
|
||||||
|
:color="currentIngHasError ? 'error-text' : 'success-text'"
|
||||||
|
>
|
||||||
|
{{ $t("recipe.parser.confidence-score") }}: {{ currentIng.confidence ? asPercentage(currentIng.confidence?.average!) : "" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<RecipeIngredientEditor
|
||||||
|
v-model="currentIng.ingredient"
|
||||||
|
:unit-error="!!currentMissingUnit"
|
||||||
|
:unit-error-tooltip="$t('recipe.parser.this-unit-could-not-be-parsed-automatically')"
|
||||||
|
:food-error="!!currentMissingFood"
|
||||||
|
:food-error-tooltip="$t('recipe.parser.this-food-could-not-be-parsed-automatically')"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
<v-card-actions>
|
||||||
icon
|
<v-spacer />
|
||||||
size="40"
|
<BaseButton
|
||||||
color="info"
|
v-if="currentMissingUnit && !currentIng.ingredient.unit?.id"
|
||||||
:disabled="state.loading.parser"
|
color="warning"
|
||||||
@click="parseIngredients"
|
size="small"
|
||||||
>
|
@click="createMissingUnit"
|
||||||
<v-icon>{{ $globals.icons.refresh }}</v-icon>
|
>
|
||||||
</v-btn>
|
{{ i18n.t("recipe.parser.missing-unit", { unit: currentMissingUnit }) }}
|
||||||
</div>
|
</BaseButton>
|
||||||
</div>
|
<BaseButton
|
||||||
</BaseCardSectionTitle>
|
v-if="currentMissingUnit && currentIng.ingredient.unit?.id"
|
||||||
<AppLoader v-if="state.loading.parser" waiting-text="" class="my-6" />
|
color="warning"
|
||||||
<v-card v-else-if="!state.allReviewed && currentIng">
|
size="small"
|
||||||
<v-card-text class="pb-0 mb-0">
|
@click="addMissingUnitAsAlias"
|
||||||
<div class="text-center px-8 py-4 mb-6">
|
>
|
||||||
<p class="text-h5 font-italic">
|
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingUnit, item: currentIng.ingredient.unit.name }) }}
|
||||||
{{ currentIng.input }}
|
</BaseButton>
|
||||||
</p>
|
<BaseButton
|
||||||
</div>
|
v-if="currentMissingFood && !currentIng.ingredient.food?.id"
|
||||||
<div class="d-flex align-center pa-0 ma-0">
|
color="warning"
|
||||||
<v-icon
|
size="small"
|
||||||
:color="(currentIng.confidence?.average || 0) < confidenceThreshold ? 'error' : 'success'"
|
@click="createMissingFood"
|
||||||
>
|
>
|
||||||
{{ (currentIng.confidence?.average || 0) < confidenceThreshold ? $globals.icons.alert : $globals.icons.check }}
|
{{ i18n.t("recipe.parser.missing-food", { food: currentMissingFood }) }}
|
||||||
</v-icon>
|
</BaseButton>
|
||||||
<span
|
<BaseButton
|
||||||
class="ml-2"
|
v-if="currentMissingFood && currentIng.ingredient.food?.id"
|
||||||
:color="currentIngHasError ? 'error-text' : 'success-text'"
|
color="warning"
|
||||||
>
|
size="small"
|
||||||
{{ $t("recipe.parser.confidence-score") }}: {{ currentIng.confidence ? asPercentage(currentIng.confidence?.average!) : "" }}
|
@click="addMissingFoodAsAlias"
|
||||||
</span>
|
>
|
||||||
</div>
|
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingFood, item: currentIng.ingredient.food.name }) }}
|
||||||
<RecipeIngredientEditor
|
</BaseButton>
|
||||||
v-model="currentIng.ingredient"
|
</v-card-actions>
|
||||||
:unit-error="!!currentMissingUnit"
|
</v-card-text>
|
||||||
:unit-error-tooltip="$t('recipe.parser.this-unit-could-not-be-parsed-automatically')"
|
</v-card>
|
||||||
:food-error="!!currentMissingFood"
|
<div v-else>
|
||||||
:food-error-tooltip="$t('recipe.parser.this-food-could-not-be-parsed-automatically')"
|
<v-card-title class="text-center pt-0 pb-8">
|
||||||
/>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<BaseButton
|
|
||||||
v-if="currentMissingUnit && !currentIng.ingredient.unit?.id"
|
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
@click="createMissingUnit"
|
|
||||||
>
|
|
||||||
{{ i18n.t("recipe.parser.missing-unit", { unit: currentMissingUnit }) }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-if="currentMissingUnit && currentIng.ingredient.unit?.id"
|
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
@click="addMissingUnitAsAlias"
|
|
||||||
>
|
|
||||||
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingUnit, item: currentIng.ingredient.unit.name }) }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-if="currentMissingFood && !currentIng.ingredient.food?.id"
|
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
@click="createMissingFood"
|
|
||||||
>
|
|
||||||
{{ i18n.t("recipe.parser.missing-food", { food: currentMissingFood }) }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-if="currentMissingFood && currentIng.ingredient.food?.id"
|
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
@click="addMissingFoodAsAlias"
|
|
||||||
>
|
|
||||||
{{ i18n.t("recipe.parser.add-text-as-alias-for-item", { text: currentMissingFood, item: currentIng.ingredient.food.name }) }}
|
|
||||||
</BaseButton>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
<v-expansion-panels v-else>
|
|
||||||
<v-card-title>{{ $t("recipe.parser.parsing-completed") }}</v-card-title>
|
|
||||||
<v-expansion-panel>
|
|
||||||
<v-expansion-panel-title>
|
|
||||||
{{ $t("recipe.parser.review-parsed-ingredients") }}
|
{{ $t("recipe.parser.review-parsed-ingredients") }}
|
||||||
</v-expansion-panel-title>
|
</v-card-title>
|
||||||
<v-expansion-panel-text>
|
<v-card-text style="max-height: 60vh; overflow-y: auto;">
|
||||||
<VueDraggable
|
<VueDraggable
|
||||||
v-model="parsedIngs"
|
v-model="parsedIngs"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
@@ -117,48 +118,66 @@
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ghostClass: 'ghost',
|
ghostClass: 'ghost',
|
||||||
}"
|
}"
|
||||||
|
class="px-6"
|
||||||
@start="drag = true"
|
@start="drag = true"
|
||||||
@end="drag = false"
|
@end="drag = false"
|
||||||
>
|
>
|
||||||
<TransitionGroup
|
<TransitionGroup
|
||||||
type="transition"
|
type="transition"
|
||||||
>
|
>
|
||||||
<div v-for="(ingredient, index) in parsedIngs" :key="index">
|
<v-lazy v-for="(ingredient, index) in parsedIngs" :key="index">
|
||||||
<RecipeIngredientEditor
|
<RecipeIngredientEditor
|
||||||
v-model="ingredient.ingredient"
|
v-model="ingredient.ingredient"
|
||||||
|
enable-drag-handle
|
||||||
enable-context-menu
|
enable-context-menu
|
||||||
class="list-group-item"
|
class="list-group-item pb-8"
|
||||||
|
:delete-disabled="parsedIngs.length <= 1"
|
||||||
@delete="parsedIngs.splice(index, 1)"
|
@delete="parsedIngs.splice(index, 1)"
|
||||||
@insert-above="insertNewIngredient(index)"
|
@insert-above="insertNewIngredient(index)"
|
||||||
@insert-below="insertNewIngredient(index + 1)"
|
@insert-below="insertNewIngredient(index + 1)"
|
||||||
/>
|
>
|
||||||
<p class="pt-0 pb-4 my-0 text-caption">
|
<template #before-divider>
|
||||||
{{ $t("recipe.original-text-with-value", { originalText: ingredient.input }) }}
|
<p v-if="ingredient.input" class="py-0 my-0 text-caption">
|
||||||
</p>
|
{{ $t("recipe.original-text-with-value", { originalText: ingredient.input }) }}
|
||||||
</div>
|
</p>
|
||||||
|
</template>
|
||||||
|
</RecipeIngredientEditor>
|
||||||
|
</v-lazy>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</VueDraggable>
|
</VueDraggable>
|
||||||
</v-expansion-panel-text>
|
</v-card-text>
|
||||||
</v-expansion-panel>
|
</div>
|
||||||
</v-expansion-panels>
|
</div>
|
||||||
</v-container>
|
</v-container>
|
||||||
<template v-if="!state.loading.parser" #custom-card-action>
|
<template v-if="!state.loading.parser" #custom-card-action>
|
||||||
<BaseButton
|
<!-- Parse -->
|
||||||
v-if="!state.allReviewed"
|
<div v-if="!state.allReviewed" class="d-flex justify-space-between align-center">
|
||||||
color="info"
|
<v-checkbox
|
||||||
:icon="$globals.icons.arrowRightBold"
|
v-model="currentIngShouldDelete"
|
||||||
icon-right
|
color="error"
|
||||||
:text="$t('general.next')"
|
hide-details
|
||||||
@click="nextIngredient"
|
density="compact"
|
||||||
/>
|
:label="i18n.t('recipe.parser.delete-item')"
|
||||||
<BaseButton
|
class="mr-4"
|
||||||
v-else
|
/>
|
||||||
create
|
<BaseButton
|
||||||
:text="$t('general.save')"
|
:color="currentIngShouldDelete ? 'error' : 'info'"
|
||||||
:icon="$globals.icons.save"
|
:icon="currentIngShouldDelete ? $globals.icons.delete : $globals.icons.arrowRightBold"
|
||||||
:loading="state.loading.save"
|
:icon-right="!currentIngShouldDelete"
|
||||||
@click="saveIngs"
|
:text="$t(currentIngShouldDelete ? 'recipe.parser.delete-item' : 'general.next')"
|
||||||
/>
|
@click="nextIngredient"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Review -->
|
||||||
|
<div v-else>
|
||||||
|
<BaseButton
|
||||||
|
create
|
||||||
|
:text="$t('general.save')"
|
||||||
|
:icon="$globals.icons.save"
|
||||||
|
:loading="state.loading.save"
|
||||||
|
@click="saveIngs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -226,6 +245,7 @@ const currentIng = ref<ParsedIngredient | null>(null);
|
|||||||
const currentMissingUnit = ref("");
|
const currentMissingUnit = ref("");
|
||||||
const currentMissingFood = ref("");
|
const currentMissingFood = ref("");
|
||||||
const currentIngHasError = computed(() => currentMissingUnit.value || currentMissingFood.value);
|
const currentIngHasError = computed(() => currentMissingUnit.value || currentMissingFood.value);
|
||||||
|
const currentIngShouldDelete = ref(false);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
currentParsedIndex: -1,
|
currentParsedIndex: -1,
|
||||||
@@ -297,6 +317,11 @@ function checkFood(ing: ParsedIngredient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextIngredient() {
|
function nextIngredient() {
|
||||||
|
if (currentIngShouldDelete.value) {
|
||||||
|
parsedIngs.value.splice(state.currentParsedIndex, 1);
|
||||||
|
currentIngShouldDelete.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
let nextIndex = state.currentParsedIndex + 1;
|
let nextIndex = state.currentParsedIndex + 1;
|
||||||
|
|
||||||
while (nextIndex < parsedIngs.value.length) {
|
while (nextIndex < parsedIngs.value.length) {
|
||||||
@@ -304,6 +329,7 @@ function nextIngredient() {
|
|||||||
if (shouldReview(current)) {
|
if (shouldReview(current)) {
|
||||||
state.currentParsedIndex = nextIndex;
|
state.currentParsedIndex = nextIndex;
|
||||||
currentIng.value = current;
|
currentIng.value = current;
|
||||||
|
currentIngShouldDelete.value = false;
|
||||||
checkUnit(current);
|
checkUnit(current);
|
||||||
checkFood(current);
|
checkFood(current);
|
||||||
return;
|
return;
|
||||||
@@ -462,6 +488,16 @@ watch(parser, () => {
|
|||||||
parseIngredients();
|
parseIngredients();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch([parsedIngs, () => state.allReviewed], () => {
|
||||||
|
if (!state.allReviewed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedIngs.value.length) {
|
||||||
|
insertNewIngredient(0);
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
function asPercentage(num: number | undefined): string {
|
function asPercentage(num: number | undefined): string {
|
||||||
if (!num) {
|
if (!num) {
|
||||||
return "0%";
|
return "0%";
|
||||||
|
|||||||
@@ -671,12 +671,12 @@
|
|||||||
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "No Food",
|
"no-food": "No Food",
|
||||||
"parsing-completed": "Parsing Completed",
|
|
||||||
"review-parsed-ingredients": "Review parsed ingredients",
|
"review-parsed-ingredients": "Review parsed ingredients",
|
||||||
"confidence-score": "Confidence Score",
|
"confidence-score": "Confidence Score",
|
||||||
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
|
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
|
||||||
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
|
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
|
||||||
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}"
|
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
|
||||||
|
"delete-item": "Delete Item"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
"not-linked-ingredients": "Additional Ingredients",
|
"not-linked-ingredients": "Additional Ingredients",
|
||||||
|
|||||||
Reference in New Issue
Block a user