Localize hard-coded texts (#2044)

* feat(lang): localize some views

* feat(lang): an attempt at localizing vuetify (WIP)

* feat(lang): localized some more screens

* feat(lang): localized some more screens again

* feat(lang): hack to localize vuetify

* feat(lang): localize data management pages

* fix linting errors

---------

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
sephrat 2023-01-29 02:39:51 +01:00 committed by GitHub
parent 754d4c3937
commit f8b8680b45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 695 additions and 393 deletions

View File

@ -8,9 +8,11 @@
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
<!-- TODO Make this localizable -->
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
<!-- TODO: proper pluralization of inputDay -->
{{ $t('meal-plan.this-rule-will-apply', {
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType])
}) }}
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div v-if="preferences">
<BaseCardSectionTitle title="General Preferences"></BaseCardSectionTitle>
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" label="Private Group"></v-checkbox>
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" :label="$t('group.private-group')"></v-checkbox>
<v-select
v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
@ -11,7 +11,7 @@
:label="$t('settings.first-day-of-week')"
/>
<BaseCardSectionTitle class="mt-5" title="Group Recipe Preferences"></BaseCardSectionTitle>
<BaseCardSectionTitle class="mt-5" :title="$tc('group.group-recipe-preferences')"></BaseCardSectionTitle>
<template v-for="(_, key) in preferences">
<v-checkbox
v-if="labels[key]"
@ -38,12 +38,12 @@ export default defineComponent({
const { i18n } = useContext();
const labels = {
recipePublic: "Allow users outside of your group to see your recipes",
recipeShowNutrition: "Show nutrition information",
recipeShowAssets: "Show recipe assets",
recipeLandscapeView: "Default to landscape view",
recipeDisableComments: "Disable recipe comments from users in your group",
recipeDisableAmount: "Disable organizing recipe ingredients by units and food",
recipePublic: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
};
const allDays = [
@ -96,4 +96,4 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
</style>
</style>

View File

@ -53,7 +53,7 @@
<v-icon left>
{{ $globals.icons.chefHat }}
</v-icon>
<v-list-item-title>{{ "Last Made" }}</v-list-item-title>
<v-list-item-title>{{ $t('general.last-made') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>

View File

@ -50,7 +50,7 @@
<div class="d-flex justify-center flex-wrap">
<BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true">
<template #icon> {{ $globals.icons.chefHat }} </template>
I Made This
{{ $t('recipe.made-this') }}
</BaseButton>
</div>
<div class="d-flex justify-center flex-wrap">
@ -63,7 +63,7 @@
<v-icon left>
{{ $globals.icons.calendar }}
</v-icon>
Last Made {{ value ? new Date(value+"Z").toLocaleDateString($i18n.locale) : $t("general.never") }}
{{ $t('recipe.last-made-date', { date: value ? new Date(value+"Z").toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
</v-chip>
</div>
</div>

View File

@ -25,12 +25,10 @@
</v-card-actions>
<AdvancedOnly>
<v-card v-if="isEditForm" flat class="ma-2 mb-2">
<v-card-title> API Extras </v-card-title>
<v-card-title> {{ $t('recipe.api-extras') }} </v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs
within a recipe to reference from 3rd part applications. You can use these keys to contain information to
trigger automation or custom messages to relay to your desired device.
{{ $t('recipe.api-extras-description') }}
<v-row v-for="(_, key) in recipe.extras" :key="key" class="mt-1">
<v-col cols="8">
<v-text-field v-model="recipe.extras[key]" dense :label="key">
@ -45,7 +43,7 @@
</v-card-text>
<v-card-actions class="d-flex">
<div style="max-width: 200px">
<v-text-field v-model="apiNewKey" label="Message Key"></v-text-field>
<v-text-field v-model="apiNewKey" :label="$t('recipe.message-key')"></v-text-field>
</div>
<BaseButton create small class="ml-5" @click="createApiExtra" />
</v-card-actions>

View File

@ -39,7 +39,7 @@
<template #icon>
{{ $globals.icons.foods }}
</template>
Parse
{{ $t('recipe.parse') }}
</BaseButton>
</span>
</template>
@ -53,7 +53,7 @@
<script lang="ts">
import draggable from "vuedraggable";
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe";
@ -75,6 +75,7 @@ export default defineComponent({
setup(props) {
const { user } = usePageUser();
const { imageKey } = usePageState(props.recipe.slug);
const { i18n } = useContext();
const drag = ref(false);
@ -95,11 +96,11 @@ export default defineComponent({
const parserToolTip = computed(() => {
if (props.recipe.settings.disableAmount) {
return "Enable ingredient amounts to use this feature";
return i18n.t("recipe.enable-ingredient-amounts-to-use-this-feature");
} else if (hasFoodOrUnit.value) {
return "Recipes with units or foods defined cannot be parsed.";
return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed");
}
return "Parse ingredients";
return i18n.t("recipe.parse-ingredients");
});
function addIngredient(ingredients: Array<string> | null = null) {

View File

@ -6,7 +6,7 @@
:disable-amount="recipe.settings.disableAmount"
/>
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0">
<h2 class="mb-2 mt-4">Required Tools</h2>
<h2 class="mb-2 mt-4">{{ $t('tool.required-tools') }}</h2>
<v-list-item v-for="(tool, index) in recipe.tools" :key="index" dense>
<v-checkbox
v-model="recipe.tools[index].onHand"

View File

@ -135,15 +135,15 @@
event: 'open',
children: [
{
text: 'Toggle Section',
text: $tc('recipe.toggle-section'),
event: 'toggle-section',
},
{
text: 'Link Ingredients',
text: $tc('recipe.link-ingredients'),
event: 'link-ingredients',
},
{
text: 'Merge Above',
text: $tc('recipe.merge-above'),
event: 'merge-above',
},
{
@ -152,7 +152,7 @@
},
{
icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye,
text: previewStates[index] ? 'Edit Markdown' : 'Preview Markdown',
text: previewStates[index] ? $tc('recipe.edit-markdown') : $tc('markdown-editor.preview-markdown-button-label'),
event: 'preview-step',
},
],
@ -188,7 +188,7 @@
:preview.sync="previewStates[index]"
:display-preview="false"
:textarea="{
hint: 'Attach images by dragging & dropping them into the editor',
hint: $t('recipe.attach-images-hint'),
persistentHint: true,
}"
/>

View File

@ -38,7 +38,7 @@
<!-- Recipe Tools Edit -->
<v-card v-if="isEditForm" class="mt-2">
<v-card-title class="py-2"> Required Tools </v-card-title>
<v-card-title class="py-2"> {{ $t('tool.required-tools') }} </v-card-title>
<v-divider class="mx-2" />
<v-card-text class="pt-0">
<RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" />

View File

@ -9,7 +9,7 @@
{{ toastAlert.text }}
<template #action="{ attrs }">
<v-btn text v-bind="attrs" @click="toastAlert.open = false"> Close </v-btn>
<v-btn text v-bind="attrs" @click="toastAlert.open = false"> {{ $t('general.close') }} </v-btn>
</template>
</v-snackbar>
<v-snackbar

View File

@ -106,8 +106,9 @@ export default defineComponent({
},
submitText: {
type: String,
// TODO Figure out how to localize this default value
default: () => "Create",
default: function () {
return this.$t("general.create");
}
},
keepOpen: {
default: false,
@ -117,6 +118,8 @@ export default defineComponent({
setup(props, context) {
const dialog = computed<boolean>({
get() {
// @ts-expect-error - props inference doesn't work here for some reason
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return props.value;
},
set(val) {

View File

@ -94,8 +94,9 @@ export default defineComponent({
btnText: {
type: String,
required: false,
// TODO Figure out how to localize this default value
default: "Actions",
default: function () {
return this.$t("general.actions");
}
},
},
setup(props, context) {

View File

@ -1,16 +1,18 @@
import { useContext } from "@nuxtjs/composition-api";
import { useClipboard } from "@vueuse/core";
import { alert } from "./use-toast";
export function useCopy() {
const { copy, copied, isSupported } = useClipboard();
const { i18n } = useContext();
function copyText(text: string) {
if (!isSupported) {
alert.error("Clipboard not supported");
alert.error(i18n.tc("general.clipboard-not-supported"));
return;
}
copy(text);
alert.success("Copied to clipboard");
alert.success(i18n.tc("general.copied-to-clipboard"));
}
return { copyText, copied };
@ -18,10 +20,11 @@ export function useCopy() {
export function useCopyList() {
const { copy, isSupported } = useClipboard();
const { i18n } = useContext();
function checkClipboard() {
if (!isSupported) {
alert.error("Your browser does not support clipboard");
alert.error(i18n.tc("general.your-browser-does-not-support-clipboard"));
return false;
}
@ -51,7 +54,7 @@ export function useCopyList() {
function copyText(text: string, len: number) {
copy(text).then(() => {
alert.success(`Copied ${len} items to clipboard`);
alert.success(i18n.tc("general.copied-items-to-clipboard", len));
});
}

View File

@ -1,4 +1,4 @@
import { useAsync, ref, Ref } from "@nuxtjs/composition-api";
import { useAsync, ref, Ref, useContext } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api";
import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
@ -25,6 +25,8 @@ export const useCookbooks = function () {
const api = useUserApi();
const loading = ref(false);
const { i18n } = useContext();
const actions = {
getAll() {
loading.value = true;
@ -54,7 +56,7 @@ export const useCookbooks = function () {
async createOne() {
loading.value = true;
const { data } = await api.cookbooks.createOne({
name: "Cookbook " + String((cookbookStore?.value?.length ?? 0) + 1),
name: i18n.t("cookbook.cookbook-with-name", [String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
});
if (data && cookbookStore?.value) {
cookbookStore.value.push(data);

View File

@ -2,14 +2,17 @@ import { computed, useContext } from "@nuxtjs/composition-api";
import { LOCALES } from "./available-locales";
export const useLocales = () => {
const { i18n } = useContext();
const { i18n, $vuetify } = useContext();
const locale = computed<string>({
get() {
$vuetify.lang.current = i18n.locale; // dirty hack
return i18n.locale;
},
set(value) {
i18n.setLocale(value);
$vuetify.lang.current = value; // this does not persist after window reload :-(
// Reload the page to update the language - not all strings are reactive
window.location.reload();
},

View File

@ -23,19 +23,22 @@ export function usePasswordField() {
}
export const usePasswordStrength = (password: Ref<string>) => {
const { i18n } = useContext();
const score = computed(() => {
return scorePassword(password.value);
});
const strength = computed(() => {
if (score.value < 50) {
return "Weak";
return i18n.tc("user.password-strength-values.weak");
} else if (score.value < 80) {
return "Good";
return i18n.tc("user.password-strength-values.good");
} else if (score.value < 100) {
return "Strong";
return i18n.tc("user.password-strength-values.strong");
} else {
return "Very Strong";
return i18n.tc("user.password-strength-values.very-strong");
}
});

View File

@ -21,7 +21,8 @@
"production": "Production",
"support": "Support",
"version": "Version",
"unknown-version": "unknown"
"unknown-version": "unknown",
"sponsor": "Sponsor"
},
"asset": {
"assets": "Assets",
@ -163,7 +164,17 @@
"transfer": "Transfer",
"copy": "Copy",
"color": "Color",
"timestamp": "Timestamp"
"timestamp": "Timestamp",
"last-made": "Last Made",
"learn-more": "Learn More",
"this-feature-is-currently-inactive": "This feature is currently inactive",
"clipboard-not-supported": "Clipboard not supported",
"copied-to-clipboard": "Copied to clipboard",
"your-browser-does-not-support-clipboard": "Your browser does not support clipboard\")",
"copied-items-to-clipboard": "No item copied to clipboard|One item copied to clipboard|Copied {count} items to clipboard",
"actions": "Actions",
"selected-count": "Selected: {count}",
"export-all": "Export All"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
@ -187,7 +198,25 @@
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
}
},
"manage-members": "Manage Members",
"manage-members-description": "Manage the permissions of the members in your groups. {manage} allows the user to access the data-management page {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.",
"manage": "Manage",
"invite": "Invite",
"looking-to-update-your-profile": "Looking to Update Your Profile?",
"default-recipe-preferences-description": "These are the default settings when a new recipe is created in your group. These can be changed for individual recipes in the recipe settings menu.",
"default-recipe-preferences": "Default Recipe Preferences",
"group-preferences": "Group Preferences",
"private-group": "Private Group",
"allow-users-outside-of-your-group-to-see-your-recipes": "Allow users outside of your group to see your recipes",
"show-nutrition-information": "Show nutrition information",
"show-recipe-assets": "Show recipe assets",
"default-to-landscape-view": "Default to landscape view",
"disable-users-from-commenting-on-recipes": "Disable users from commenting on recipes",
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organizing recipe ingredients by units and food",
"general-preferences": "General Preferences",
"group-recipe-preferences": "Group Recipe Preferences",
"report": "Report"
},
"meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan",
@ -222,7 +251,28 @@
"lunch": "Lunch",
"dinner": "Dinner",
"type-any": "Any",
"day-any": "Any"
"day-any": "Any",
"editor": "Editor",
"meal-recipe": "Meal Recipe",
"meal-title": "Meal Title",
"meal-note": "Meal Note",
"note-only": "Note Only",
"random-meal": "Random Meal",
"random-dinner": "Random Dinner",
"random-side": "Random Side",
"this-rule-will-apply": "This rule will apply {dayCriteria} {mealTypeCriteria}.",
"to-all-days": "to all days",
"on-days": "on {0}s",
"for-all-meal-types": "for all meal types",
"for-type-meal-types": "for {0} meal types",
"meal-plan-rules": "Meal Plan Rules",
"new-rule": "New Rule",
"meal-plan-rules-description": "You can create rules for auto selecting recipes for you meal plans. These rules are used by the server to determine the random pool of recipes to select from when creating meal plans. Note that if rules have the same day/type constraints then the categories of the rules will be merged. In practice, it's unnecessary to create duplicate rules, but it's possible to do so.",
"new-rule-description": "When creating a new rule for a meal plan you can restrict the rule to be applicable for a specific day of the week and/or a specific type of meal. To apply a rule to all days or all meal types you can set the rule to \"Any\" which will apply it to all the possible values for the day and/or meal type.",
"recipe-rules": "Recipe Rules",
"applies-to-all-days": "Applies to all days",
"applies-on-days": "Applies on {0}s",
"meal-plan-settings": "Meal Plan Settings"
},
"migration": {
"chowdown": {
@ -374,7 +424,48 @@
"open-timeline": "Open Timeline",
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this"
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
"recipes-with-units-or-foods-defined-cannot-be-parsed": "Recipes with units or foods defined cannot be parsed.",
"parse-ingredients": "Parse ingredients",
"edit-markdown": "Edit Markdown",
"recipe-creation": "Recipe Creation",
"select-one-of-the-various-ways-to-create-a-recipe": "Select one of the various ways to create a recipe",
"looking-for-migrations": "Looking For Migrations?",
"import-with-url": "Import with URL",
"create-recipe": "Create Recipe",
"import-with-zip": "Import with .zip",
"create-recipe-from-an-image": "Create recipe from an image",
"bulk-url-import": "Bulk URL Import",
"debug-scraper": "Debug Scraper",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Create a recipe by providing the name. All recipes must have unique names.",
"new-recipe-names-must-be-unique": "New recipe names must be unique",
"scrape-recipe": "Scrape Recipe",
"scrape-recipe-description": "Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the recipe from that site and add it to your collection.",
"import-original-keywords-as-tags": "Import original keywords as tags",
"stay-in-edit-mode": "Stay in Edit mode",
"import-from-zip": "Import from Zip",
"import-from-zip-description": "Import a single recipe that was exported from another Mealie instance.",
"zip-files-must-have-been-exported-from-mealie": ".zip files must have been exported from Mealie",
"create-a-recipe-by-uploading-a-scan": "Create a recipe by uploading a scan.",
"upload-a-png-image-from-a-recipe-book": "Upload a png image from a recipe book",
"recipe-bulk-importer": "Recipe Bulk Importer",
"recipe-bulk-importer-description": "The Bulk recipe importer allows you to import multiple recipes at once by queueing the sites on the backend and running the task in the background. This can be useful when initially migrating to Mealie, or when you want to import a large number of recipes.",
"set-categories-and-tags": "Set Categories and Tags",
"bulk-imports": "Bulk Imports",
"bulk-import-process-has-started": "Bulk Import process has started",
"bulk-import-process-has-failed": "Bulk import process has failed",
"report-deletion-failed": "Report deletion failed",
"recipe-debugger": "Recipe Debugger",
"recipe-debugger-description": "Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is not supported by Mealie or its scraper library.",
"debug": "Debug",
"tree-view": "Tree View",
"recipe-yield": "Recipe Yield"
},
"search": {
"advanced-search": "Advanced Search",
@ -388,7 +479,8 @@
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter",
"search-hint": "Press '/'"
"search-hint": "Press '/'",
"advanced": "Advanced"
},
"settings": {
"add-a-new-theme": "Add a New Theme",
@ -501,7 +593,17 @@
"note": "Note",
"label": "Label",
"linked-item-warning": "This item is linked to one or more recipe. Adjusting the units or foods will yield unexpected results when adding or removing the recipe from this list.",
"toggle-food": "Toggle Food"
"toggle-food": "Toggle Food",
"manage-labels": "Manage Labels",
"are-you-sure-you-want-to-delete-this-item": "Are you sure you want to delete this item?",
"copy-as-text": "Copy as Text",
"copy-as-markdown": "Copy as Markdown",
"delete-checked": "Delete Checked",
"toggle-label-sort": "Toggle Label Sort",
"uncheck-all-items": "Uncheck All Items",
"linked-recipes-count": "No Linked Recipes|One Linked Recipe|{count} Linked Recipes",
"items-checked-count": "No items checked|One item checked|{count} items checked",
"no-label": "No Label"
},
"sidebar": {
"all-recipes": "All Recipes",
@ -553,7 +655,8 @@
"create-a-tool": "Create a Tool",
"tool-name": "Tool Name",
"create-new-tool": "Create New Tool",
"on-hand-checkbox-label": "Show as On Hand (Checked)"
"on-hand-checkbox-label": "Show as On Hand (Checked)",
"required-tools": "Required Tools"
},
"user": {
"admin": "Admin",
@ -612,7 +715,19 @@
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later",
"favorite-recipes": "Favorite Recipes"
"favorite-recipes": "Favorite Recipes",
"email-or-username": "Email or Username",
"remember-me": "Remember Me",
"please-enter-your-email-and-password": "Please enter your email and password",
"invalid-credentials": "Invalid Credentials",
"account-locked-please-try-again-later": "Account Locked. Please try again later",
"user-favorites": "User Favorites",
"password-strength-values": {
"weak": "Weak",
"good": "Good",
"strong": "Strong",
"very-strong": "Very Strong"
}
},
"language-dialog": {
"translated": "translated",
@ -622,19 +737,70 @@
"read-the-docs": "Read the docs"
},
"data-pages": {
"seed-data": "Seed Data",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually.",
"combine-food": "Combine Food",
"source-food": "Source Food",
"target-food": "Target Food",
"create-food": "Create Food",
"food-label": "Food Label",
"edit-food": "Edit Food",
"food-data": "Food Data"
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
"seed-dialog-text": "Seed the database with common units based on your local language.",
"combine-unit-description": "Combining the selected units will merge the Source Unit and Target Unit into a single unit. The {source-unit-will-be-deleted} and all of the references to the Source Unit will be updated to point to the Target Unit.",
"combine-unit": "Combine Unit",
"source-unit": "Source Unit",
"target-unit": "Target Unit",
"merging-unit-into-unit": "Merging {0} into {1}",
"create-unit": "Create Unit",
"abbreviation": "Abbreviation",
"description": "Description",
"display-as-fraction": "Display as Fraction",
"use-abbreviation": "Use Abbreviation",
"edit-unit": "Edit Unit",
"unit-data": "Unit Data",
"use-abbv": "Use Abbv.",
"fraction": "Fraction"
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
}
"seed-dialog-text": "Seed the database with common labels based on your local language.",
"edit-label": "Edit Label",
"new-label": "New Label",
"labels": "Labels"
},
"recipes": {
"purge-exports": "Purge Exports",
"are-you-sure-you-want-to-delete-all-export-data": "Are you sure you want to delete all export data?",
"confirm-delete-recipes": "Are you sure you want to delete the following recipes? This action cannot be undone.",
"the-following-recipes-selected-length-will-be-exported": "The following recipes ({0}) will be exported.",
"settings-chosen-explanation": "Settings chosen here, excluding the locked option, will be applied to all selected recipes.",
"selected-length-recipe-s-settings-will-be-updated": "{0} recipe(s) settings will be updated.",
"recipe-data": "Recipe Data",
"recipe-data-description": "Use this section to manage the data associated with your recipes. You can perform several bulk actions on your recipes including exporting, deleting, tagging, and assigning categories.",
"recipe-columns": "Recipe Columns",
"data-exports-description": "This section provides links to available exports that are ready to download. These exports do expire, so be sure to grab them while they're still available.",
"data-exports": "Data Exports",
"tag": "Tag",
"categorize": "Categorize",
"update-settings": "Update Settings",
"tag-recipes": "Tag Recipes",
"categorize-recipes": "Categorize Recipes",
"export-recipes": "Export Recipes",
"delete-recipes": "Delete Recipes",
"source-unit-will-be-deleted": "Source Unit will be deleted"
},
"seed-data": "Seed Data",
"seed": "Seed",
"data-management": "Data Management",
"data-management-description": "Select which data set you want to make changes to.",
"select-data": "Select Data",
"select-language": "Select Language",
"columns": "Columns"
},
"user-registration": {
"user-registration": "User Registration",
@ -746,5 +912,54 @@
"mainentance": {
"actions-title": "Actions"
}
},
"profile": {
"welcome-user": "👋 Welcome, {0}",
"description": "Manage your profile, recipes, and group settings.",
"get-invite-link": "Get Invite Link",
"account-summary": "Account Summary",
"account-summary-description": "Here's a summary of your group's information",
"group-statistics": "Group Statistics",
"group-statistics-description": "Your Group Statistics provide some insight how you're using Mealie.",
"storage-capacity": "Storage Capacity",
"storage-capacity-description": "Your storage capacity is a calculation of the images and assets you have uploaded.",
"personal": "Personal",
"personal-description": "These are settings that are personal to you. Changes here won't affect other users",
"user-settings": "User Settings",
"user-settings-description": "Manage your preferences, change your password, and update your email",
"api-tokens-description": "Manage your API Tokens for access from external applications",
"group-description": "These items are shared within your group. Editing one of them will change it for the whole group!",
"group-settings": "Group Settings",
"group-settings-description": "Manage your common group settings like mealplan and privacy settings.",
"cookbooks-description": "Manage a collection of recipe categories and generate pages for them.",
"members": "Members",
"members-description": "See who's in your group and manage their permissions.",
"webhooks-description": "Setup webhooks that trigger on days that you have have mealplan scheduled.",
"notifiers": "Notifiers",
"notifiers-description": "Setup email and push notifications that trigger on specific events.",
"manage-data": "Manage Data",
"manage-data-description": "Manage your Food and Units (more options coming soon)",
"data-migrations": "Data Migrations",
"data-migrations-description": "Migrate your existing data from other applications like Nextcloud Recipes and Chowdown",
"email-sent": "Email Sent",
"error-sending-email": "Error Sending Email",
"personal-information": "Personal Information",
"preferences": "Preferences",
"show-advanced-description": "Show advanced features (API Keys, Webhooks, and Data Management)",
"back-to-profile": "Back to Profile",
"looking-for-privacy-settings": "Looking for Privacy Settings?"
},
"cookbook": {
"cookbooks": "Cookbooks",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes and tags. Creating a cookbook will add an entry to the side-bar and all the recipes with the tags and categories chosen will be displayed in the cookbook.",
"public-cookbook": "Public Cookbook",
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
"filter-options": "Filter Options",
"filter-options-description": "When require all is selected the cookbook will only include recipes that have all of the items selected. This applies to each subset of selectors and not a cross section of the selected items.",
"require-all-categories": "Require All Categories",
"require-all-tags": "Require All Tags",
"require-all-tools": "Require All Tools",
"cookbook-name": "Cookbook Name",
"cookbook-with-name": "Cookbook {0}"
}
}

View File

@ -390,6 +390,7 @@ export default {
},
},
},
optionsPath: "./vuetify.options.js",
},
// Build Configuration: https://go.nuxtjs.dev/config-build

View File

@ -4,10 +4,8 @@
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
</template>
<template #title> Cookbooks </template>
Cookbooks are another way to organize recipes by creating cross sections of recipes and tags. Creating a cookbook
will add an entry to the side-bar and all the recipes with the tags and categories chosen will be displayed in the
cookbook.
<template #title> {{ $t('cookbook.cookbooks') }} </template>
{{ $t('cookbook.description') }}
</BasePageTitle>
<BaseButton create @click="actions.createOne()" />
@ -34,35 +32,34 @@
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-card-text v-if="cookbooks">
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
<v-text-field v-model="cookbooks[index].name" :label="$t('cookbook.cookbook-name')"></v-text-field>
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" :label="$t('recipe.description')"></v-textarea>
<RecipeOrganizerSelector v-model="cookbooks[index].categories" selector-type="categories" />
<RecipeOrganizerSelector v-model="cookbooks[index].tags" selector-type="tags" />
<RecipeOrganizerSelector v-model="cookbooks[index].tools" selector-type="tools" />
<v-switch v-model="cookbooks[index].public" hide-details single-line>
<template #label>
Public Cookbook
{{ $t('cookbook.public-cookbook') }}
<HelpIcon small right class="ml-2">
Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.
{{ $t('cookbook.public-cookbook-description') }}
</HelpIcon>
</template>
</v-switch>
<div class="mt-4">
<h3 class="text-subtitle-1 d-flex align-center mb-0 pb-0">
Filter Options
{{ $t('cookbook.filter-options') }}
<HelpIcon right small class="ml-2">
When require all is selected the cookbook will only include recipes that have all of the items
selected. This applies to each subset of selectors and not a cross section of the selected items.
{{ $t('cookbook.filter-options-description') }}
</HelpIcon>
</h3>
<v-switch v-model="cookbooks[index].requireAllCategories" class="mt-0" hide-details single-line>
<template #label> Require All Categories </template>
<template #label> {{ $t('cookbook.require-all-categories') }} </template>
</v-switch>
<v-switch v-model="cookbooks[index].requireAllTags" hide-details single-line>
<template #label> Require All Tags </template>
<template #label> {{ $t('cookbook.require-all-tags') }} </template>
</v-switch>
<v-switch v-model="cookbooks[index].requireAllTools" hide-details single-line>
<template #label> Require All Tools </template>
<template #label> {{ $t('cookbook.require-all-tools') }} </template>
</v-switch>
</div>
</v-card-text>

View File

@ -4,8 +4,8 @@
<template #header>
<v-img max-height="175" max-width="175" :src="require('~/static/svgs/manage-recipes.svg')"></v-img>
</template>
<template #title> Data Management </template>
Select which data set you want to make changes to.
<template #title> {{ $t('data-pages.data-management') }} </template>
{{ $t('data-pages.data-management-description') }}
<BannerExperimental class="mt-5"></BannerExperimental>
<template #content>
<div>
@ -13,28 +13,7 @@
:btn-text="buttonText"
mode="link"
rounded
:items="[
{
text: 'Recipes',
value: 'new',
to: '/group/data/recipes',
},
{
text: 'Foods',
value: 'url',
to: '/group/data/foods',
},
{
text: 'Units',
value: 'new',
to: '/group/data/units',
},
{
text: 'Labels',
value: 'new',
to: '/group/data/labels',
},
]"
:items="DATA_TYPE_OPTIONS"
>
</BaseOverflowButton>
</div>
@ -49,7 +28,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, useRoute } from "@nuxtjs/composition-api";
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
@ -59,13 +38,37 @@ export default defineComponent({
},
},
setup() {
const { i18n } = useContext();
const buttonLookup: { [key: string]: string } = {
recipes: "Recipes",
foods: "Foods",
units: "Units",
labels: "Labels",
recipes: i18n.tc("general.recipes"),
foods: i18n.tc("general.foods"),
units: i18n.tc("general.units"),
labels: i18n.tc("data-pages.labels.labels"),
};
const DATA_TYPE_OPTIONS = [
{
text: i18n.t("general.recipes"),
value: "new",
to: "/group/data/recipes",
},
{
text: i18n.t("general.foods"),
value: "url",
to: "/group/data/foods",
},
{
text: i18n.t("general.units"),
value: "new",
to: "/group/data/units",
},
{
text: i18n.t("data-pages.labels.labels"),
value: "new",
to: "/group/data/labels",
},
];
const route = useRoute();
const buttonText = computed(() => {
@ -75,15 +78,18 @@ export default defineComponent({
return buttonLookup[last];
}
return "Select Data";
return i18n.tc("data-pages.select-data");
});
return {
buttonText,
DATA_TYPE_OPTIONS
};
},
head: {
title: "Data Management",
head() {
return {
title: this.$tc("data-pages.data-management"),
};
},
});
</script>

View File

@ -1,13 +1,13 @@
<template>
<div>
<!-- Merge Dialog -->
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.foods" title="Combine Food" @confirm="mergeFoods">
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.foods" :title="$t('data-pages.foods.combine-food')" @confirm="mergeFoods">
<v-card-text>
<div>
{{ $t("data-pages.foods.merge-dialog-text") }}
</div>
<v-autocomplete v-model="fromFood" return-object :items="foods" item-text="name" label="Source Food" />
<v-autocomplete v-model="toFood" return-object :items="foods" item-text="name" label="Target Food" />
<v-autocomplete v-model="fromFood" return-object :items="foods" item-text="name" :label="$t('data-pages.foods.source-food')" />
<v-autocomplete v-model="toFood" return-object :items="foods" item-text="name" :label="$t('data-pages.foods.target-food')" />
<template v-if="canMerge && fromFood && toFood">
<div class="text-center">
@ -32,7 +32,7 @@
v-model="locale"
:items="locales"
item-text="name"
label="Select Language"
:label="$t('data-pages.select-language')"
class="my-3"
hide-details
outlined
@ -58,7 +58,7 @@
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.foods"
title="Create Food"
:title="$t('data-pages.foods.create-food')"
:submit-text="$tc('general.save')"
@submit="createFood"
>
@ -67,17 +67,17 @@
<v-text-field
v-model="createTarget.name"
autofocus
label="Name"
:label="$t('general.name')"
:rules="[validators.required]"
></v-text-field>
<v-text-field v-model="createTarget.description" label="Description"></v-text-field>
<v-text-field v-model="createTarget.description" :label="$t('recipe.description')"></v-text-field>
<v-autocomplete
v-model="createTarget.labelId"
clearable
:items="allLabels"
item-value="id"
item-text="name"
label="Food Label"
:label="$t('data-pages.foods.food-label')"
>
</v-autocomplete>
</v-form> </v-card-text
@ -87,21 +87,21 @@
<BaseDialog
v-model="editDialog"
:icon="$globals.icons.foods"
title="Edit Food"
:title="$t('data-pages.foods.edit-food')"
:submit-text="$tc('general.save')"
@submit="editSaveFood"
>
<v-card-text v-if="editTarget">
<v-form ref="domEditFoodForm">
<v-text-field v-model="editTarget.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="editTarget.description" label="Description"></v-text-field>
<v-text-field v-model="editTarget.name" :label="$t('general.name')" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="editTarget.description" :label="$t('recipe.description')"></v-text-field>
<v-autocomplete
v-model="editTarget.labelId"
clearable
:items="allLabels"
item-value="id"
item-text="name"
label="Food Label"
:label="$t('data-pages.foods.food-label')"
>
</v-autocomplete>
</v-form> </v-card-text
@ -121,7 +121,7 @@
</BaseDialog>
<!-- Recipe Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.foods" section title="Food Data"> </BaseCardSectionTitle>
<BaseCardSectionTitle :icon="$globals.icons.foods" section :title="$tc('data-pages.foods.food-data')"> </BaseCardSectionTitle>
<CrudTable
:table-config="tableConfig"
:headers.sync="tableHeaders"
@ -154,7 +154,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, computed } from "@nuxtjs/composition-api";
import { defineComponent, onMounted, ref, computed, useContext } from "@nuxtjs/composition-api";
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
@ -168,28 +168,29 @@ export default defineComponent({
components: { MultiPurposeLabel },
setup() {
const userApi = useUserApi();
const { i18n } = useContext();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: "Id",
text: i18n.tc("general.id"),
value: "id",
show: false,
},
{
text: "Name",
text: i18n.tc("general.name"),
value: "name",
show: true,
},
{
text: "Description",
text: i18n.tc("recipe.description"),
value: "description",
show: true,
},
{
text: "Label",
text: i18n.tc("shopping-list.label"),
value: "label",
show: true,
},
@ -297,7 +298,7 @@ export default defineComponent({
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale, i18n } = useLocales();
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;

View File

@ -1,7 +1,7 @@
<template>
<div>
<!-- Create New Dialog -->
<BaseDialog v-model="state.createDialog" title="New Label" :icon="$globals.icons.tags" @submit="createLabel">
<BaseDialog v-model="state.createDialog" :title="$t('data-pages.labels.new-label')" :icon="$globals.icons.tags" @submit="createLabel">
<v-card-text>
<MultiPurposeLabel :label="createLabelData" />
@ -16,7 +16,7 @@
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.tags"
title="Edit Label"
:title="$t('data-pages.labels.edit-label')"
:submit-text="$tc('general.save')"
@submit="editSaveLabel"
>
@ -57,7 +57,7 @@
v-model="locale"
:items="locales"
item-text="name"
label="Select Language"
:label="$t('data-pages.select-language')"
class="my-3"
hide-details
outlined
@ -80,7 +80,7 @@
</BaseDialog>
<!-- Recipe Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.tags" section title="Labels"> </BaseCardSectionTitle>
<BaseCardSectionTitle :icon="$globals.icons.tags" section :title="$tc('data-pages.labels.labels')"> </BaseCardSectionTitle>
<CrudTable
:table-config="tableConfig"
:headers.sync="tableHeaders"
@ -103,7 +103,7 @@
<template #button-bottom>
<BaseButton @click="seedDialog = true">
<template #icon> {{ $globals.icons.database }} </template>
Seed
{{ $t('data-pages.seed') }}
</BaseButton>
</template>
</CrudTable>
@ -111,7 +111,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, ref } from "@nuxtjs/composition-api";
import { defineComponent, onMounted, reactive, ref, useContext } from "@nuxtjs/composition-api";
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
@ -124,18 +124,19 @@ export default defineComponent({
components: { MultiPurposeLabel },
setup() {
const userApi = useUserApi();
const { i18n } = useContext();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: "Id",
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: "Name",
text: i18n.t("general.name"),
value: "name",
show: true,
},
@ -205,7 +206,7 @@ export default defineComponent({
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale, i18n } = useLocales();
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;

View File

@ -3,12 +3,12 @@
<!-- Export Purge Confirmation Dialog -->
<BaseDialog
v-model="purgeExportsDialog"
title="Purge Exports"
:title="$t('data-pages.recipes.purge-exports')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="purgeExports()"
>
<v-card-text> Are you sure you want to delete all export data? </v-card-text>
<v-card-text> {{ $t('data-pages.recipes.are-you-sure-you-want-to-delete-all-export-data') }} </v-card-text>
</BaseDialog>
<!-- Base Dialog Object -->
@ -18,7 +18,7 @@
width="650px"
:icon="dialog.icon"
:title="dialog.title"
submit-text="Submit"
:submit-text="$t('general.submit')"
@submit="dialog.callback"
>
<v-card-text v-if="dialog.mode == MODES.tag">
@ -28,7 +28,7 @@
<RecipeOrganizerSelector v-model="toSetCategories" selector-type="categories" />
</v-card-text>
<v-card-text v-else-if="dialog.mode == MODES.delete">
<p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p>
<p class="h4">{{ $t('data-pages.recipes.confirm-delete-recipes') }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="selected">
<template #default="{ item }">
@ -42,7 +42,7 @@
</v-card>
</v-card-text>
<v-card-text v-else-if="dialog.mode == MODES.export">
<p class="h4">The following recipes ({{ selected.length }}) will be exported.</p>
<p class="h4">{{ $t('data-pages.recipes.the-following-recipes-selected-length-will-be-exported', [selected.length]) }}</p>
<v-card outlined>
<v-virtual-scroll height="400" item-height="25" :items="selected">
<template #default="{ item }">
@ -56,20 +56,19 @@
</v-card>
</v-card-text>
<v-card-text v-else-if="dialog.mode == MODES.updateSettings" class="px-12">
<p>Settings chosen here, excluding the locked option, will be applied to all selected recipes.</p>
<p>{{ $t('data-pages.recipes.settings-chosen-explanation') }}</p>
<div class="mx-auto">
<RecipeSettingsSwitches v-model="recipeSettings" />
</div>
<p class="text-center mb-0">
<i>{{ selected.length }} recipe(s) settings will be updated.</i>
<i>{{ $t('data-pages.recipes.selected-length-recipe-s-settings-will-be-updated', [selected.length]) }}</i>
</p>
</v-card-text>
</BaseDialog>
<section>
<!-- Recipe Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.primary" title="Recipe Data">
Use this section to manage the data associated with your recipes. You can perform several bulk actions on your
recipes including exporting, deleting, tagging, and assigning categories.
<BaseCardSectionTitle :icon="$globals.icons.primary" :title="$tc('data-pages.recipes.recipe-data')">
{{ $t('data-pages.recipes.recipe-data-description') }}
</BaseCardSectionTitle>
<v-card-actions class="mt-n5 mb-1">
<v-menu offset-y bottom nudge-bottom="6" :close-on-content-click="false">
@ -78,12 +77,12 @@
<v-icon left>
{{ $globals.icons.cog }}
</v-icon>
Columns
{{ $t('data-pages.columns') }}
</v-btn>
</template>
<v-card>
<v-card-title class="py-2">
<div>Recipe Columns</div>
<div>{{ $t('data-pages.recipes.recipe-columns') }}</div>
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text class="mt-n5">
@ -113,7 +112,7 @@
>
</BaseOverflowButton>
<p v-if="selected.length > 0" class="text-caption my-auto ml-5">Selected: {{ selected.length }}</p>
<p v-if="selected.length > 0" class="text-caption my-auto ml-5">{{ $tc('general.selected-count', selected.length) }}</p>
</v-card-actions>
<v-card>
<RecipeDataTable v-model="selected" :loading="loading" :recipes="allRecipes" :show-headers="headers" />
@ -134,7 +133,7 @@
<template #icon>
{{ $globals.icons.database }}
</template>
Export All
{{ $t('general.export-all') }}
</BaseButton>
</v-card-actions>
</v-card>
@ -142,9 +141,8 @@
<section class="mt-10">
<!-- Downloads Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.database" section title="Data Exports">
This section provides links to available exports that are ready to download. These exports do expire, so be sure
to grab them while they're still available.
<BaseCardSectionTitle :icon="$globals.icons.database" section :title="$tc('data-pages.recipes.data-exports')">
{{ $t('data-pages.recipes.data-exports-description') }}
</BaseCardSectionTitle>
<v-card-actions class="mt-n5 mb-1">
<BaseButton delete @click="purgeExportsDialog = true"> </BaseButton>
@ -182,7 +180,7 @@ export default defineComponent({
setup() {
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
const { $globals } = useContext();
const { $globals, i18n } = useContext();
const selected = ref<Recipe[]>([]);
@ -204,39 +202,39 @@ export default defineComponent({
});
const headerLabels = {
id: "Id",
owner: "Owner",
tags: "Tags",
categories: "Categories",
tools: "Tools",
recipeYield: "Recipe Yield",
dateAdded: "Date Added",
id: i18n.t("general.id"),
owner: i18n.t("general.owner"),
tags: i18n.t("tag.tags"),
categories: i18n.t("recipe.categories"),
tools: i18n.t("tool.tools"),
recipeYield: i18n.t("recipe.recipe-yield"),
dateAdded: i18n.t("general.date-added"),
};
const actions: MenuItem[] = [
{
icon: $globals.icons.database,
text: "Export",
text: i18n.tc("export.export"),
event: "export-selected",
},
{
icon: $globals.icons.tags,
text: "Tag",
text: i18n.tc("data-pages.recipes.tag"),
event: "tag-selected",
},
{
icon: $globals.icons.tags,
text: "Categorize",
text: i18n.tc("data-pages.recipes.categorize"),
event: "categorize-selected",
},
{
icon: $globals.icons.cog,
text: "Update Settings",
text: i18n.tc("data-pages.recipes.update-settings"),
event: "update-settings",
},
{
icon: $globals.icons.delete,
text: "Delete",
text: i18n.tc("general.delete"),
event: "delete-selected",
},
];
@ -352,7 +350,7 @@ export default defineComponent({
const dialog = reactive({
state: false,
title: "Tag Recipes",
title: i18n.t("data-pages.recipes.tag-recipes"),
mode: MODES.tag,
tag: "",
callback: () => {
@ -364,11 +362,11 @@ export default defineComponent({
function openDialog(mode: MODES) {
const titles: Record<MODES, string> = {
[MODES.tag]: "Tag Recipes",
[MODES.category]: "Categorize Recipes",
[MODES.export]: "Export Recipes",
[MODES.delete]: "Delete Recipes",
[MODES.updateSettings]: "Update Settings",
[MODES.tag]: i18n.tc("data-pages.recipes.tag-recipes"),
[MODES.category]: i18n.tc("data-pages.recipes.categorize-recipes"),
[MODES.export]: i18n.tc("data-pages.recipes.export-recipes"),
[MODES.delete]: i18n.tc("data-pages.recipes.delete-recipes"),
[MODES.updateSettings]: i18n.tc("data-pages.recipes.update-settings"),
};
const callbacks: Record<MODES, () => Promise<void>> = {
@ -420,7 +418,7 @@ export default defineComponent({
},
head() {
return {
title: "Recipe Data",
title: this.$tc("data-pages.recipes.recipe-data"),
};
},
});

View File

@ -1,23 +1,25 @@
<template>
<div>
<!-- Merge Dialog -->
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.units" title="Combine Unit" @confirm="mergeUnits">
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.units" :title="$t('data-pages.units.combine-unit')" @confirm="mergeUnits">
<v-card-text>
Combining the selected units will merge the Source Unit and Target Unit into a single unit. The
<strong> Source Unit will be deleted </strong> and all of the references to the Source Unit will be updated to
point to the Target Unit.
<i18n path="data-pages.units.combine-unit-description">
<template #source-unit-will-be-deleted>
<strong> {{ $t('data-pages.recipes.source-unit-will-be-deleted') }} </strong>
</template>
</i18n>
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="id" label="Source Unit">
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.source-unit')">
<template #selection="{ item }"> {{ item.name }}</template>
<template #item="{ item }"> {{ item.name }} </template>
</v-autocomplete>
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="id" label="Target Unit">
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.target-unit')">
<template #selection="{ item }"> {{ item.name }}</template>
<template #item="{ item }"> {{ item.name }} </template>
</v-autocomplete>
<template v-if="canMerge && fromUnit && toUnit">
<div class="text-center">Merging {{ fromUnit.name }} into {{ toUnit.name }}</div>
<div class="text-center">{{ $t('data-pages.units.merging-unit-into-unit', [fromUnit.name, toUnit.name]) }}</div>
</template>
</v-card-text>
</BaseDialog>
@ -26,7 +28,7 @@
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.units"
title="Create Unit"
:title="$t('data-pages.units.create-unit')"
:submit-text="$tc('general.save')"
@submit="createUnit"
>
@ -35,13 +37,13 @@
<v-text-field
v-model="createTarget.name"
autofocus
label="Name"
:label="$t('general.name')"
:rules="[validators.required]"
></v-text-field>
<v-text-field v-model="createTarget.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="createTarget.description" label="Description"></v-text-field>
<v-checkbox v-model="createTarget.fraction" hide-details label="Display as Fraction"></v-checkbox>
<v-checkbox v-model="createTarget.useAbbreviation" hide-details label="Use Abbreviation"></v-checkbox>
<v-text-field v-model="createTarget.abbreviation" :label="$t('data-pages.units.abbreviation')"></v-text-field>
<v-text-field v-model="createTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
<v-checkbox v-model="createTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
<v-checkbox v-model="createTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
</v-form>
</v-card-text>
</BaseDialog>
@ -50,17 +52,17 @@
<BaseDialog
v-model="editDialog"
:icon="$globals.icons.units"
title="Edit Unit"
:title="$t('data-pages.units.edit-unit')"
:submit-text="$tc('general.save')"
@submit="editSaveUnit"
>
<v-card-text v-if="editTarget">
<v-form ref="domEditUnitForm">
<v-text-field v-model="editTarget.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="editTarget.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="editTarget.description" label="Description"></v-text-field>
<v-checkbox v-model="editTarget.fraction" hide-details label="Display as Fraction"></v-checkbox>
<v-checkbox v-model="editTarget.useAbbreviation" hide-details label="Use Abbreviation"></v-checkbox>
<v-text-field v-model="editTarget.name" :label="$t('general.name')" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="editTarget.abbreviation" :label="$t('data-pages.units.abbreviation')"></v-text-field>
<v-text-field v-model="editTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
<v-checkbox v-model="editTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
<v-checkbox v-model="editTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
</v-form>
</v-card-text>
</BaseDialog>
@ -93,7 +95,7 @@
v-model="locale"
:items="locales"
item-text="name"
label="Select Language"
:label="$t('data-pages.select-language')"
class="my-3"
hide-details
outlined
@ -116,7 +118,7 @@
</BaseDialog>
<!-- Recipe Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.units" section title="Unit Data"> </BaseCardSectionTitle>
<BaseCardSectionTitle :icon="$globals.icons.units" section :title="$tc('data-pages.units.unit-data')"> </BaseCardSectionTitle>
<CrudTable
:table-config="tableConfig"
:headers.sync="tableHeaders"
@ -155,7 +157,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
@ -167,38 +169,39 @@ import { VForm } from "~/types/vuetify";
export default defineComponent({
setup() {
const userApi = useUserApi();
const { i18n } = useContext();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: "Id",
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: "Name",
text: i18n.t("general.name"),
value: "name",
show: true,
},
{
text: "Abbreviation",
text: i18n.t("data-pages.units.abbreviation"),
value: "abbreviation",
show: true,
},
{
text: "Use Abbv.",
text: i18n.t("data-pages.units.use-abbv"),
value: "useAbbreviation",
show: true,
},
{
text: "Description",
text: i18n.t("data-pages.units.description"),
value: "description",
show: false,
},
{
text: "Fraction",
text: i18n.t("data-pages.units.fraction"),
value: "fraction",
show: true,
},
@ -304,7 +307,7 @@ export default defineComponent({
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale, i18n } = useLocales();
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;

View File

@ -4,16 +4,16 @@
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
</template>
<template #title> Group Settings </template>
These items are shared within your group. Editing one of them will change it for the whole group!
<template #title> {{ $t('profile.group-settings') }} </template>
{{ $t('profile.group-description') }}
</BasePageTitle>
<section v-if="group">
<BaseCardSectionTitle class="mt-10" title="Group Preferences"></BaseCardSectionTitle>
<BaseCardSectionTitle class="mt-10" :title="$tc('group.group-preferences')"></BaseCardSectionTitle>
<v-checkbox
v-model="group.preferences.privateGroup"
class="mt-n4"
label="Private Group"
:label="$t('group.private-group')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-select
@ -28,45 +28,44 @@
</section>
<section v-if="group">
<BaseCardSectionTitle class="mt-10" title="Default Recipe Preferences">
These are the default settings when a new recipe is created in your group. These can be changed for individual
recipes in the recipe settings menu.
<BaseCardSectionTitle class="mt-10" :title="$tc('group.default-recipe-preferences')">
{{ $t('group.default-recipe-preferences-description') }}
</BaseCardSectionTitle>
<v-checkbox
v-model="group.preferences.recipePublic"
class="mt-n4"
label="Allow users outside of your group to see your recipes"
:label="$t('group.allow-users-outside-of-your-group-to-see-your-recipes')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-checkbox
v-model="group.preferences.recipeShowNutrition"
class="mt-n4"
label="Show nutrition information"
:label="$t('group.show-nutrition-information')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-checkbox
v-model="group.preferences.recipeShowAssets"
class="mt-n4"
label="Show recipe assets"
:label="$t('group.show-recipe-assets')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-checkbox
v-model="group.preferences.recipeLandscapeView"
class="mt-n4"
label="Default to landscape view"
:label="$t('group.default-to-landscape-view')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-checkbox
v-model="group.preferences.recipeDisableComments"
class="mt-n4"
label="Disable users from commenting on recipes"
:label="$t('group.disable-users-from-commenting-on-recipes')"
@change="groupActions.updatePreferences()"
></v-checkbox>
<v-checkbox
v-model="group.preferences.recipeDisableAmount"
class="mt-n4"
label="Disable organizing recipe ingredients by units and food"
:label="$t('group.disable-organizing-recipe-ingredients-by-units-and-food')"
@change="groupActions.updatePreferences()"
></v-checkbox>
</section>

View File

@ -35,25 +35,25 @@
<v-date-picker v-model="newMeal.date" :first-day-of-week="firstDayOfWeek" no-title @input="pickerMenu = false"></v-date-picker>
</v-menu>
<v-card-text>
<v-select v-model="newMeal.entryType" :return-object="false" :items="planTypeOptions" label="Entry Type">
<v-select v-model="newMeal.entryType" :return-object="false" :items="planTypeOptions" :label="$t('recipe.entry-type')">
</v-select>
<v-autocomplete
v-if="!dialog.note"
v-model="newMeal.recipeId"
label="Meal Recipe"
:label="$t('meal-plan.meal-recipe')"
:items="allRecipes"
item-text="name"
item-value="id"
:return-object="false"
></v-autocomplete>
<template v-else>
<v-text-field v-model="newMeal.title" label="Meal Title"> </v-text-field>
<v-textarea v-model="newMeal.text" rows="2" label="Meal Note"> </v-textarea>
<v-text-field v-model="newMeal.title" :label="$t('meal-plan.meal-title')"> </v-text-field>
<v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')"> </v-textarea>
</template>
</v-card-text>
<v-card-actions class="my-0 py-0">
<v-switch v-model="dialog.note" class="mt-n3" label="Note Only"></v-switch>
<v-switch v-model="dialog.note" class="mt-n3" :label="$t('meal-plan.note-only')"></v-switch>
</v-card-actions>
</v-card-text>
</BaseDialog>
@ -71,8 +71,8 @@
</div>
</div>
<div class="d-flex align-center justify-space-between">
<v-switch v-model="edit" label="Editor"></v-switch>
<ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" text="Settings" />
<v-switch v-model="edit" :label="$t('meal-plan.editor')"></v-switch>
<ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" :text="$tc('general.settings')" />
</div>
<v-row class="">
<v-col
@ -174,7 +174,7 @@
:buttons="[
{
icon: $globals.icons.diceMultiple,
text: 'Random Meal',
text: $tc('meal-plan.random-meal'),
event: 'random',
children: [
{
@ -185,19 +185,19 @@
{
icon: $globals.icons.diceMultiple,
text: 'Lunch',
text: $tc('meal-plan.lunch'),
event: 'randomLunch',
},
],
},
{
icon: $globals.icons.potSteam,
text: 'Random Dinner',
text: $tc('meal-plan.random-dinner'),
event: 'randomDinner',
},
{
icon: $globals.icons.bowlMixOutline,
text: 'Random Side',
text: $tc('meal-plan.random-side'),
event: 'randomSide',
},
{

View File

@ -4,20 +4,15 @@
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
</template>
<template #title> Meal Plan Rules </template>
You can create rules for auto selecting recipes for you meal plans. These rules are used by the server to
determine the random pool of recipes to select from when creating meal plans. Note that if rules have the same
day/type constraints then the categories of the rules will be merged. In practice, it's unnecessary to create
duplicate rules, but it's possible to do so.
<template #title> {{ $t('meal-plan.meal-plan-rules') }} </template>
{{ $t('meal-plan.meal-plan-rules-description') }}
</BasePageTitle>
<v-card>
<v-card-title class="headline"> New Rule </v-card-title>
<v-card-title class="headline"> {{ $t('meal-plan.new-rule') }} </v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
When creating a new rule for a meal plan you can restrict the rule to be applicable for a specific day of the
week and/or a specific type of meal. To apply a rule to all days or all meal types you can set the rule to "Any"
which will apply it to all the possible values for the day and/or meal type.
{{ $t('meal-plan.new-rule-description') }}
<GroupMealPlanRuleForm
class="mt-2"
@ -33,13 +28,13 @@
</v-card>
<section>
<BaseCardSectionTitle class="mt-10" title="Recipe Rules" />
<BaseCardSectionTitle class="mt-10" :title="$tc('meal-plan.recipe-rules')" />
<div>
<div v-for="(rule, idx) in allRules" :key="rule.id">
<v-card class="my-2 left-border">
<v-card-title class="headline pb-1">
{{ rule.day === "unset" ? "Applies to all days" : `Applies on ${rule.day}s` }}
{{ rule.entryType === "unset" ? "for all meal types" : ` for ${rule.entryType} meal types` }}
{{ rule.day === "unset" ? $t('meal-plan.applies-to-all-days') : $t('meal-plan.applies-on-days', [rule.day]) }}
{{ rule.entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [rule.entryType]) }}
<span class="ml-auto">
<BaseButtonGroup
:buttons="[
@ -91,7 +86,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, useAsync } from "@nuxtjs/composition-api";
import { defineComponent, ref, useAsync, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan";
import GroupMealPlanRuleForm from "~/components/Domain/Group/GroupMealPlanRuleForm.vue";
@ -182,8 +177,10 @@ export default defineComponent({
toggleEditState,
};
},
head: {
title: "Meal Plan Settings",
head() {
return {
title: this.$tc("meal-plan.meal-plan-settings"),
};
},
});
</script>

View File

@ -4,13 +4,18 @@
<template #header>
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-members.svg')"></v-img>
</template>
<template #title> Manage Members </template>
Manage the permissions of the members in your groups. <b> Manage </b> allows the user to access the
data-management page <b> Invite </b> allows the user to generate invitation links for other users. Group owners
cannot change their own permissions.
<template #title> {{ $t('group.manage-members') }} </template>
<i18n path="group.manage-members-description">
<template #manage>
<b>{{ $t('group.manage') }}</b>
</template>
<template #invite>
<b>{{ $t('group.invite') }}</b>
</template>
</i18n>
</BasePageTitle>
<v-container class="mt-4 d-flex justify-start">
<v-btn outlined rounded to="/user/profile/edit"> Looking to Update Your Profile? </v-btn>
<v-btn outlined rounded to="/user/profile/edit"> {{ $t('group.looking-to-update-your-profile') }} </v-btn>
</v-container>
<v-data-table
:headers="headers"
@ -24,7 +29,7 @@
<UserAvatar :user-id="item.id" />
</template>
<template #item.admin="{ item }">
{{ item.admin ? "Admin" : "User" }}
{{ item.admin ? $t('user.admin') : $t('user.user') }}
</template>
<template #item.manage="{ item }">
<div class="d-flex justify-center">
@ -85,9 +90,9 @@ export default defineComponent({
{ text: i18n.t("user.username"), value: "username" },
{ text: i18n.t("user.full-name"), value: "fullName" },
{ text: i18n.t("user.admin"), value: "admin" },
{ text: "Manage", value: "manage", sortable: false, align: "center" },
{ text: "Organize", value: "organize", sortable: false, align: "center" },
{ text: "Invite", value: "invite", sortable: false, align: "center" },
{ text: i18n.t("group.manage"), value: "manage", sortable: false, align: "center" },
{ text: i18n.t("settings.organize"), value: "organize", sortable: false, align: "center" },
{ text: i18n.t("group.invite"), value: "invite", sortable: false, align: "center" },
];
async function refreshMembers() {
@ -116,7 +121,7 @@ export default defineComponent({
},
head() {
return {
title: "Members",
title: this.$t("profile.members"),
};
},
});

View File

@ -4,7 +4,7 @@
<template #header>
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
</template>
<template #title> Report </template>
<template #title> {{ $t('group.report') }} </template>
</BasePageTitle>
<v-container v-if="report">
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>

View File

@ -23,7 +23,7 @@
</v-avatar>
</div>
<v-card-title class="headline justify-center pb-1"> Sign In </v-card-title>
<v-card-title class="headline justify-center pb-1"> {{ $t('user.sign-in') }} </v-card-title>
<v-card-text>
<v-form @submit.prevent="authenticate">
<v-text-field
@ -34,7 +34,7 @@
autofocus
class="rounded-lg"
name="login"
label="Email or Username"
:label="$t('user.email-or-username')"
type="text"
/>
<v-text-field
@ -46,11 +46,11 @@
rounded
class="rounded-lg"
name="password"
label="Password"
:label="$t('user.password')"
:type="inputType"
@click:append="togglePasswordShow"
/>
<v-checkbox v-model="form.remember" class="ml-2 mt-n2" label="Remember Me"></v-checkbox>
<v-checkbox v-model="form.remember" class="ml-2 mt-n2" :label="$t('user.remember-me')"></v-checkbox>
<v-card-actions class="justify-center pt-0">
<div class="max-button">
<v-btn :loading="loggingIn" color="primary" type="submit" large rounded class="rounded-xl" block>
@ -72,17 +72,17 @@
<div
v-for="link in [
{
text: 'Sponsor',
text: $t('about.sponsor'),
icon: $globals.icons.heart,
href: 'https://github.com/sponsors/hay-kot',
},
{
text: 'GitHub',
text: $t('about.github'),
icon: $globals.icons.github,
href: 'https://github.com/hay-kot/mealie',
},
{
text: 'Docs',
text: $t('about.docs'),
icon: $globals.icons.folderOutline,
href: 'https://docs.mealie.io/',
},
@ -103,7 +103,7 @@
<v-icon left>
{{ $vuetify.theme.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight }}
</v-icon>
{{ $vuetify.theme.dark ? "Light Mode" : "Dark Mode" }}
{{ $vuetify.theme.dark ? $t('settings.theme.light-mode') : $t('settings.theme.dark-mode') }}
</v-btn>
</v-container>
</template>
@ -123,7 +123,7 @@ export default defineComponent({
const isDark = useDark();
const router = useRouter();
const { $auth } = useContext();
const { $auth, i18n } = useContext();
whenever(
() => $auth.loggedIn,
@ -149,7 +149,7 @@ export default defineComponent({
async function authenticate() {
if (form.email.length === 0 || form.password.length === 0) {
alert.error("Please enter your email and password");
alert.error(i18n.t("user.please-enter-your-email-and-password") as string);
return;
}
@ -168,12 +168,12 @@ export default defineComponent({
// if ($axios.isAxiosError(error) && error.response?.status === 401) {
// @ts-ignore- see above
if (error.response?.status === 401) {
alert.error("Invalid Credentials");
alert.error(i18n.t("user.invalid-credentials") as string);
// @ts-ignore - see above
} else if (error.response?.status === 423) {
alert.error("Account Locked. Please try again later");
alert.error(i18n.t("user.account-locked-please-try-again-later") as string);
} else {
alert.error("Something Went Wrong!");
alert.error(i18n.t("events.something-went-wrong") as string);
}
}
loggingIn.value = false;

View File

@ -5,8 +5,8 @@
<template #header>
<v-img max-height="175" max-width="175" :src="require('~/static/svgs/recipes-create.svg')"></v-img>
</template>
<template #title> Recipe Creation </template>
Select one of the various ways to create a recipe
<template #title> {{ $t('recipe.recipe-creation') }} </template>
{{ $t('recipe.select-one-of-the-various-ways-to-create-a-recipe') }}
<template #content>
<div class="ml-auto">
<BaseOverflowButton v-model="subpage" rounded :items="subpages"> </BaseOverflowButton>
@ -20,7 +20,7 @@
<AdvancedOnly>
<v-container class="d-flex justify-end">
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
<v-btn outlined rounded to="/group/migrations"> {{ $t('recipe.looking-for-migrations') }}</v-btn>
</v-container>
</AdvancedOnly>
</div>
@ -34,37 +34,37 @@ import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
export default defineComponent({
components: { AdvancedOnly },
setup() {
const { $globals } = useContext();
const { $globals, i18n } = useContext();
const subpages: MenuItem[] = [
{
icon: $globals.icons.link,
text: "Import with URL",
text: i18n.tc("recipe.import-with-url"),
value: "url",
},
{
icon: $globals.icons.edit,
text: "Create Recipe",
text: i18n.tc("recipe.create-recipe"),
value: "new",
},
{
icon: $globals.icons.zip,
text: "Import with .zip",
text: i18n.tc("recipe.import-with-zip"),
value: "zip",
},
{
icon: $globals.icons.fileImage,
text: "Create recipe from an image",
text: i18n.tc("recipe.create-recipe-from-an-image"),
value: "ocr",
},
{
icon: $globals.icons.link,
text: "Bulk URL Import",
text: i18n.tc("recipe.bulk-url-import"),
value: "bulk",
},
{
icon: $globals.icons.robot,
text: "Debug Scraper",
text: i18n.tc("recipe.debug-scraper"),
value: "debug",
},
];

View File

@ -1,11 +1,9 @@
<template>
<div>
<div>
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.recipe-bulk-importer') }} </v-card-title>
<v-card-text>
The Bulk recipe importer allows you to import multiple recipes at once by queueing the sites on the backend and
running the task in the background. This can be useful when initially migrating to Mealie, or when you want to
import a large number of recipes.
{{ $t('recipe.recipe-bulk-importer-description') }}
</v-card-text>
</div>
<section class="mt-2">
@ -85,24 +83,24 @@
<RecipeDialogBulkAdd v-model="bulkDialog" @bulk-data="assignUrls" />
</v-card-actions>
<div class="px-1">
<v-checkbox v-model="showCatTags" hide-details label="Set Categories and Tags " />
<v-checkbox v-model="showCatTags" hide-details :label="$t('recipe.set-categories-and-tags')" />
</div>
<v-card-actions class="justify-end">
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
<template #icon> {{ $globals.icons.check }} </template>
Submit
{{ $t('general.submit') }}
</BaseButton>
</v-card-actions>
</section>
<section class="mt-12">
<BaseCardSectionTitle title="Bulk Imports"> </BaseCardSectionTitle>
<BaseCardSectionTitle :title="$tc('recipe.bulk-imports')"> </BaseCardSectionTitle>
<ReportTable :items="reports" @delete="deleteReport" />
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref } from "@nuxtjs/composition-api";
import { defineComponent, reactive, toRefs, ref, useContext } from "@nuxtjs/composition-api";
import { whenever } from "@vueuse/shared";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
@ -128,6 +126,7 @@ export default defineComponent({
);
const api = useUserApi();
const { i18n } = useContext();
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
const lockBulkImport = ref(false);
@ -140,10 +139,10 @@ export default defineComponent({
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
if (response?.status === 202) {
alert.success("Bulk Import process has started");
alert.success(i18n.tc("recipe.bulk-import-process-has-started"));
lockBulkImport.value = true;
} else {
alert.error("Bulk import process has failed");
alert.error(i18n.tc("recipe.bulk-import-process-has-failed"));
}
fetchReports();
@ -166,7 +165,7 @@ export default defineComponent({
if (response?.status === 200) {
fetchReports();
} else {
alert.error("Report deletion failed");
alert.error(i18n.tc("recipe.report-deletion-failed"));
}
}

View File

@ -2,11 +2,9 @@
<div>
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
<div>
<v-card-title class="headline"> Recipe Debugger </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.recipe-debugger') }} </v-card-title>
<v-card-text>
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper
and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is
not supported by Mealie or its scraper library.
{{ $t('recipe.recipe-debugger-description') }}
<v-text-field
v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')"
@ -28,14 +26,14 @@
<template #icon>
{{ $globals.icons.robot }}
</template>
Debug
{{ $t('recipe.debug') }}
</BaseButton>
</div>
</v-card-actions>
</div>
</v-form>
<section v-if="debugData">
<v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox>
<v-checkbox v-model="debugTreeView" :label="$t('recipe.tree-view')"></v-checkbox>
<LazyRecipeJsonEditor
v-model="debugData"
class="primary"

View File

@ -1,8 +1,8 @@
<template>
<div>
<v-card-title class="headline"> Create Recipe </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.create-recipe') }} </v-card-title>
<v-card-text>
Create a recipe by providing the name. All recipes must have unique names.
{{ $t('recipe.create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names') }}
<v-form ref="domCreateByName">
<v-text-field
v-model="newRecipeName"
@ -15,7 +15,7 @@
class="rounded-lg mt-2"
rounded
:rules="[validators.required]"
hint="New recipe names must be unique"
:hint="$t('recipe.new-recipe-names-must-be-unique')"
persistent-hint
></v-text-field>
</v-form>

View File

@ -1,8 +1,8 @@
<template>
<div>
<v-card-title class="headline"> Create Recipe from an Image </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.create-recipe-from-an-image') }} </v-card-title>
<v-card-text>
Create a recipe by uploading a scan.
{{ $t('recipe.create-a-recipe-by-uploading-a-scan') }}
<v-form ref="domCreateByOcr"> </v-form>
</v-card-text>
<v-card-actions class="justify-center">
@ -15,7 +15,7 @@
class="rounded-lg mt-2"
rounded
truncate-length="100"
hint="Upload a png image from a recipe book"
:hint="$t('recipe.upload-a-png-image-from-a-recipe-book')"
persistent-hint
prepend-icon=""
:prepend-inner-icon="$globals.icons.fileImage"

View File

@ -2,10 +2,9 @@
<div>
<v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags, stayInEditMode)">
<div>
<v-card-title class="headline"> Scrape Recipe </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.scrape-recipe') }} </v-card-title>
<v-card-text>
Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the
recipe from that site and add it to your collection.
{{ $t('recipe.scrape-recipe-description') }}
<v-text-field
v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')"
@ -20,8 +19,8 @@
:hint="$t('new-recipe.url-form-hint')"
persistent-hint
></v-text-field>
<v-checkbox v-model="importKeywordsAsTags" hide-details label="Import original keywords as tags" />
<v-checkbox v-model="stayInEditMode" hide-details label="Stay in Edit mode" />
<v-checkbox v-model="importKeywordsAsTags" hide-details :label="$t('recipe.import-original-keywords-as-tags')" />
<v-checkbox v-model="stayInEditMode" hide-details :label="$t('recipe.stay-in-edit-mode')" />
</v-card-text>
<v-card-actions class="justify-center">
<div style="width: 250px">

View File

@ -1,9 +1,9 @@
<template>
<v-form>
<div>
<v-card-title class="headline"> Import from Zip </v-card-title>
<v-card-title class="headline"> {{ $t('recipe.import-from-zip') }} </v-card-title>
<v-card-text>
Import a single recipe that was exported from another Mealie instance.
{{ $t('recipe.import-from-zip-description') }}
<v-file-input
v-model="newRecipeZip"
accept=".zip"
@ -13,7 +13,7 @@
class="rounded-lg mt-2"
rounded
truncate-length="100"
hint=".zip files must have been exported from Mealie"
:hint="$t('recipe.zip-files-must-have-been-exported-from-mealie')"
persistent-hint
prepend-icon=""
:prepend-inner-icon="$globals.icons.zip"

View File

@ -7,7 +7,7 @@
item-type="tags"
@delete="actions.deleteOne"
>
<template #title> Tags </template>
<template #title> {{ $t('tag.tags') }} </template>
</RecipeOrganizerPage>
</v-container>
</template>
@ -29,8 +29,10 @@ export default defineComponent({
actions,
};
},
head: {
title: "Tags",
head() {
return {
title: this.$tc("tag.tags"),
}
},
});
</script>

View File

@ -7,7 +7,7 @@
item-type="tools"
@delete="actions.deleteOne"
>
<template #title> Tools </template>
<template #title> {{ $t('tool.tools') }} </template>
</RecipeOrganizerPage>
</v-container>
</template>
@ -31,8 +31,10 @@ export default defineComponent({
actions: toolStore.actions,
};
},
head: {
title: "Tools",
head() {
return {
title: this.$tc("tool.tools"),
};
},
});
</script>

View File

@ -30,7 +30,7 @@
v-model="advanced"
color="info"
class="ma-0 pa-0"
label="Advanced"
:label="$t('search.advanced')"
@input="advanced = !advanced"
@click="advanced = !advanced"
/>
@ -75,7 +75,7 @@
:items="foods || []"
item-text="name"
:prepend-inner-icon="$globals.icons.foods"
label="Foods"
:label="$t('general.foods')"
>
<template #selection="data">
<v-chip
@ -102,7 +102,7 @@
<RecipeCardSection
class="mt-n5"
:icon="$globals.icons.search"
title="Results"
:title="$tc('search.results')"
:recipes="showRecipes.slice(0, maxResults)"
@sort="assignFuzzy"
/>

View File

@ -79,29 +79,29 @@
children: [
{
icon: $globals.icons.contentCopy,
text: 'Copy as Text',
text: $tc('shopping-list.copy-as-text'),
event: 'copy-plain',
},
{
icon: $globals.icons.contentCopy,
text: 'Copy as Markdown',
text: $tc('shopping-list.copy-as-markdown'),
event: 'copy-markdown',
},
],
},
{
icon: $globals.icons.delete,
text: 'Delete Checked',
text: $tc('shopping-list.delete-checked'),
event: 'delete',
},
{
icon: $globals.icons.tags,
text: 'Toggle Label Sort',
text: $tc('shopping-list.toggle-label-sort'),
event: 'sort-by-labels',
},
{
icon: $globals.icons.checkboxBlankOutline,
text: 'Uncheck All Items',
text: $tc('shopping-list.uncheck-all-items'),
event: 'uncheck',
},
]"
@ -122,7 +122,7 @@
{{ showChecked ? $globals.icons.chevronDown : $globals.icons.chevronRight }}
</v-icon>
</span>
{{ listItems.checked ? listItems.checked.length : 0 }} items checked
{{ $tc('shopping-list.items-checked-count', listItems.checked ? listItems.checked.length : 0) }}
</button>
<v-divider class="my-4"></v-divider>
<v-expand-transition>
@ -153,7 +153,7 @@
{{ $globals.icons.primary }}
</v-icon>
</span>
{{ shoppingList.recipeReferences ? shoppingList.recipeReferences.length : 0 }} Linked Recipes
{{ $tc('shopping-list.linked-recipes-count', shoppingList.recipeReferences ? shoppingList.recipeReferences.length : 0) }}
</div>
<v-divider class="my-4"></v-divider>
<RecipeList :recipes="listRecipes">
@ -178,7 +178,7 @@
<v-lazy>
<div class="d-flex justify-end mt-10">
<ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" />
<ButtonLink to="/group/data/labels" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
</div>
</v-lazy>
</v-container>
@ -187,7 +187,7 @@
<script lang="ts">
import draggable from "vuedraggable";
import { defineComponent, useAsync, useRoute, computed, ref, watch, onUnmounted } from "@nuxtjs/composition-api";
import { defineComponent, useAsync, useRoute, computed, ref, watch, onUnmounted, useContext } from "@nuxtjs/composition-api";
import { useIdle, useToggle } from "@vueuse/core";
import { useCopyList } from "~/composables/use-copy";
import { useUserApi } from "~/composables/api";
@ -224,6 +224,8 @@ export default defineComponent({
const route = useRoute();
const id = route.value.params.id;
const { i18n } = useContext();
// ===============================================================
// Shopping List Actions
@ -416,9 +418,9 @@ export default defineComponent({
function updateItemsByLabel() {
const items: { [prop: string]: ShoppingListItemOut[] } = {};
const noLabel = {
"No Label": [] as ShoppingListItemOut[],
};
const noLabelText = i18n.tc("shopping-list.no-label");
const noLabel = [] as ShoppingListItemOut[];
shoppingList.value?.listItems?.forEach((item) => {
if (item.checked) {
@ -432,12 +434,12 @@ export default defineComponent({
items[item.label.name] = [item];
}
} else {
noLabel["No Label"].push(item);
noLabel.push(item);
}
});
if (noLabel["No Label"].length > 0) {
items["No Label"] = noLabel["No Label"];
if (noLabel.length > 0) {
items[noLabelText] = noLabel;
}
itemsByLabel.value = items;

View File

@ -7,13 +7,13 @@
</BaseDialog>
<BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne">
<v-card-text> Are you sure you want to delete this item?</v-card-text>
<v-card-text>{{ $t('shopping-list.are-you-sure-you-want-to-delete-this-item') }}</v-card-text>
</BaseDialog>
<BasePageTitle divider>
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img>
</template>
<template #title> Shopping Lists </template>
<template #title>{{ $t('shopping-list.shopping-lists') }}</template>
</BasePageTitle>
<BaseButton create @click="createDialog = true" />
@ -33,7 +33,7 @@
</v-card>
</section>
<div class="d-flex justify-end mt-10">
<ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" />
<ButtonLink to="/group/data/labels" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
</div>
</v-container>
</template>

View File

@ -31,7 +31,7 @@
<template #default="{ state }">
<v-slide-x-transition leave-absolute hide-on-leave>
<div v-if="!state" key="personal-info">
<BaseCardSectionTitle class="mt-10" title="Personal Information"> </BaseCardSectionTitle>
<BaseCardSectionTitle class="mt-10" :title="$tc('profile.personal-information')"> </BaseCardSectionTitle>
<v-card tag="article" outlined>
<v-card-text class="pb-0">
<v-form ref="userUpdate">
@ -101,11 +101,11 @@
</ToggleState>
</section>
<section>
<BaseCardSectionTitle class="mt-10" title="Preferences"> </BaseCardSectionTitle>
<BaseCardSectionTitle class="mt-10" :title="$tc('profile.preferences')"> </BaseCardSectionTitle>
<v-checkbox
v-model="userCopy.advanced"
class="mt-n4"
label="Show advanced features (API Keys, Webhooks, and Data Management)"
:label="$t('profile.show-advanced-description')"
@change="updateUser"
></v-checkbox>
<div class="d-flex flex-wrap justify-center mt-5">
@ -113,9 +113,9 @@
<v-icon left>
{{ $globals.icons.backArrow }}
</v-icon>
Back to Profile
{{ $t('profile.back-to-profile') }}
</v-btn>
<v-btn outlined class="rounded-xl my-1 mx-1" to="/group"> Looking for Privacy Settings? </v-btn>
<v-btn outlined class="rounded-xl my-1 mx-1" to="/group"> {{ $t('profile.looking-for-privacy-settings') }} </v-btn>
</div>
</section>
</v-container>

View File

@ -3,10 +3,10 @@
<section class="d-flex flex-column align-center">
<UserAvatar size="84" :user-id="$auth.user.id" />
<h2 class="headline">👋 Welcome, {{ user.fullName }}</h2>
<h2 class="headline">{{ $t('profile.welcome-user', [user.fullName]) }}</h2>
<p class="subtitle-1 mb-0">
Manage your profile, recipes, and group settings.
<a href="https://hay-kot.github.io/mealie/" target="_blank"> Learn More </a>
{{ $t('profile.description') }}
<a href="https://hay-kot.github.io/mealie/" target="_blank"> {{ $t('general.learn-more') }} </a>
</p>
<v-card v-if="$auth.user.canInvite" flat color="background" width="100%" max-width="600px">
<v-card-actions class="d-flex justify-center">
@ -14,7 +14,7 @@
<v-icon left>
{{ $globals.icons.createAlt }}
</v-icon>
Get Invite Link
{{ $t('profile.get-invite-link') }}
</v-btn>
</v-card-actions>
<div v-show="generatedLink !== ''">
@ -40,15 +40,15 @@
</section>
<section class="my-3">
<div>
<h3 class="headline">Account Summary</h3>
<p>Here's a summary of your group's information</p>
<h3 class="headline">{{ $t('profile.account-summary') }}</h3>
<p>{{ $t('profile.account-summary-description') }}</p>
</div>
<v-row tag="section">
<v-col cols="12" sm="12" md="6">
<v-card outlined>
<v-card-title class="headline pb-0"> Group Statistics </v-card-title>
<v-card-title class="headline pb-0"> {{ $t('profile.group-statistics') }} </v-card-title>
<v-card-text class="py-0">
Your Group Statistics provide some insight how you're using Mealie.
{{ $t('profile.group-statistics-description') }}
</v-card-text>
<v-card-text class="d-flex flex-wrap justify-center align-center" style="gap: 0.8rem">
<StatsCards
@ -66,10 +66,10 @@
</v-col>
<v-col cols="12" sm="12" md="6" class="d-flex align-strart">
<v-card outlined>
<v-card-title class="headline pb-0"> Storage Capacity </v-card-title>
<v-card-title class="headline pb-0"> {{ $t('profile.storage-capacity') }} </v-card-title>
<v-card-text class="py-0">
Your storage capacity is a calculation of the images and assets you have uploaded.
<strong> This feature is currently inactive</strong>
{{ $t('profile.storage-capacity-description') }}
<strong> {{ $t('general.this-feature-is-currently-inactive') }}</strong>
</v-card-text>
<v-card-text>
<v-progress-linear :value="storageUsedPercentage" color="accent" class="rounded" height="30">
@ -85,8 +85,8 @@
<v-divider class="my-7"></v-divider>
<section>
<div>
<h3 class="headline">Personal</h3>
<p>These are settings that are personal to you. Changes here won't affect other users</p>
<h3 class="headline">{{ $t('profile.personal') }}</h3>
<p>{{ $t('profile.personal-description') }}</p>
</div>
<v-row tag="section">
<v-col cols="12" sm="12" md="6">
@ -94,8 +94,8 @@
:link="{ text: 'Manage User Profile', to: '/user/profile/edit' }"
:image="require('~/static/svgs/manage-profile.svg')"
>
<template #title> User Settings </template>
Manage your preferences, change your password, and update your email
<template #title> {{ $t('profile.user-settings') }} </template>
{{ $t('profile.user-settings-description') }}
</UserProfileLinkCard>
</v-col>
<AdvancedOnly>
@ -104,8 +104,8 @@
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
:image="require('~/static/svgs/manage-api-tokens.svg')"
>
<template #title> API Tokens </template>
Manage your API Tokens for access from external applications
<template #title> {{ $t('settings.token.api-tokens') }} </template>
{{ $t('profile.api-tokens-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
@ -114,8 +114,8 @@
<v-divider class="my-7"></v-divider>
<section>
<div>
<h3 class="headline">Group</h3>
<p>These items are shared within your group. Editing one of them will change it for the whole group!</p>
<h3 class="headline">{{ $t('group.group') }}</h3>
<p>{{ $t('profile.group-description') }}</p>
</div>
<v-row tag="section">
<v-col cols="12" sm="12" md="6">
@ -123,8 +123,8 @@
:link="{ text: 'Group Settings', to: '/group' }"
:image="require('~/static/svgs/manage-group-settings.svg')"
>
<template #title> Group Settings </template>
Manage your common group settings like mealplan and privacy settings.
<template #title> {{ $t('profile.group-settings') }} </template>
{{ $t('profile.group-settings-description') }}
</UserProfileLinkCard>
</v-col>
<v-col cols="12" sm="12" md="6">
@ -132,8 +132,8 @@
:link="{ text: 'Manage Cookbooks', to: '/group/cookbooks' }"
:image="require('~/static/svgs/manage-cookbooks.svg')"
>
<template #title> Cookbooks </template>
Manage a collection of recipe categories and generate pages for them.
<template #title> {{ $t('sidebar.cookbooks') }} </template>
{{ $t('profile.cookbooks-description') }}
</UserProfileLinkCard>
</v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
@ -141,8 +141,8 @@
:link="{ text: 'Manage Members', to: '/group/members' }"
:image="require('~/static/svgs/manage-members.svg')"
>
<template #title> Members </template>
See who's in your group and manage their permissions.
<template #title> {{ $t('profile.members') }} </template>
{{ $t('profile.members-description') }}
</UserProfileLinkCard>
</v-col>
<AdvancedOnly>
@ -151,8 +151,8 @@
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
:image="require('~/static/svgs/manage-webhooks.svg')"
>
<template #title> Webhooks </template>
Setup webhooks that trigger on days that you have have mealplan scheduled.
<template #title> {{ $t('settings.webhooks.webhooks') }} </template>
{{ $t('profile.webhooks-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
@ -162,8 +162,8 @@
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
:image="require('~/static/svgs/manage-notifiers.svg')"
>
<template #title> Notifiers </template>
Setup email and push notifications that trigger on specific events.
<template #title> {{ $t('profile.notifiers') }} </template>
{{ $t('profile.notifiers-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
@ -173,8 +173,8 @@
:link="{ text: 'Manage Data', to: '/group/data/foods' }"
:image="require('~/static/svgs/manage-recipes.svg')"
>
<template #title> Manage Data </template>
Manage your Food and Units (more options coming soon)
<template #title> {{ $t('profile.manage-data') }} </template>
{{ $t('profile.manage-data-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
@ -184,8 +184,8 @@
:link="{ text: 'Manage Data Migrations', to: '/group/migrations' }"
:image="require('~/static/svgs/manage-data-migrations.svg')"
>
<template #title> Data Migrations </template>
Migrate your existing data from other applications like Nextcloud Recipes and Chowdown
<template #title>{{ $t('profile.data-migrations') }} </template>
{{ $t('profile.data-migrations-description') }}
</UserProfileLinkCard>
</v-col>
</AdvancedOnly>
@ -213,7 +213,7 @@ export default defineComponent({
},
scrollToTop: true,
setup() {
const { $auth } = useContext();
const { $auth, i18n } = useContext();
const user = computed(() => $auth.user);
@ -247,9 +247,9 @@ export default defineComponent({
});
if (data && data.success) {
alert.success("Email Sent");
alert.success(i18n.tc("profile.email-sent"));
} else {
alert.error("Error Sending Email");
alert.error(i18n.tc("profile.error-sending-email"));
}
state.loading = false;
}
@ -276,11 +276,11 @@ export default defineComponent({
}, useAsyncKey());
const statsText: { [key: string]: string } = {
totalRecipes: "Recipes",
totalUsers: "Users",
totalCategories: "Categories",
totalTags: "Tags",
totalTools: "Tools",
totalRecipes: i18n.tc("general.recipes"),
totalUsers: i18n.tc("user.users"),
totalCategories: i18n.tc("sidebar.categories"),
totalTags: i18n.tc("sidebar.tags"),
totalTools: i18n.tc("tool.tools"),
};
function getStatsTitle(key: string) {

View File

@ -0,0 +1,48 @@
import * as locale from "vuetify/src/locale";
export default {
breakpoint: {},
icons: {},
lang: {
locales: {
"el-GR": locale.el,
"it-IT": locale.it,
"ko-KR": locale.ko,
"es-ES": locale.es,
"ja-JP": locale.ja,
"bg-BG": locale.bg,
"zh-CN": locale.zhHans,
"tr-TR": locale.tr,
"ar-SA": locale.ar,
"hu-HU": locale.hu,
"pt-PT": locale.pt,
"no-NO": locale.no,
"sv-SE": locale.sv,
"ro-RO": locale.ro,
"sk-SK": locale.sk,
"uk-UA": locale.uk,
"lt-LT": locale.lt,
"fr-CA": locale.fr,
"pl-PL": locale.pl,
"da-DK": locale.da,
"pt-BR": locale.pt,
"de-DE": locale.de,
"ca-ES": locale.ca,
"sr-SP": locale.srCyrl,
"cs-CZ": locale.cs,
"fr-FR": locale.fr,
"zh-TW": locale.zhHant,
"af-ZA": locale.af,
"sl-SI": locale.sl,
"ru-RU": locale.ru,
"he-IL": locale.he,
"nl-NL": locale.nl,
"en-US": locale.en,
"en-GB": locale.en,
"fi-FI": locale.fi,
"vi-VN": locale.vi,
},
current: "en-US",
},
theme: {},
};

View File

@ -5,6 +5,17 @@
"recipe": {
"unique-name-error": "Recipe names must be unique"
},
"mealplan": {
"no-recipes-match-your-rules": "No recipes match your rules"
},
"user": {
"user-updated": "User updated",
"password-updated": "Password updated",
"invalid-current-password": "Invalid current password"
},
"group": {
"report-deleted": "Report deleted."
},
"exceptions": {
"permission_denied": "You do not have permission to perform this action",
"no-entry-found": "The requested resource was not found",

View File

@ -90,7 +90,7 @@ class BaseUserController(_BaseController):
registered = {
**mealie_registered_exceptions(self.translator),
}
return registered.get(ex, "An unexpected error occurred.")
return registered.get(ex, self.t("generic.server-error"))
@property
def group_id(self) -> UUID4:

View File

@ -30,7 +30,7 @@ class RecipeCommentRoutes(BaseUserController):
@property
def mixins(self) -> HttpRepo:
return HttpRepo(self.repo, self.logger, self.registered_exceptions, "An unexpected error occurred.")
return HttpRepo(self.repo, self.logger, self.registered_exceptions, self.t("generic.server-error"))
def _check_comment_belongs_to_user(self, item_id: UUID4) -> None:
comment = self.repo.get_one(item_id)

View File

@ -31,7 +31,7 @@ class GroupCookbookController(BaseCrudController):
registered = {
**mealie_registered_exceptions(self.translator),
}
return registered.get(ex, "An unexpected error occurred.")
return registered.get(ex, self.t("generic.server-error"))
@cached_property
def mixins(self):

View File

@ -49,7 +49,7 @@ class GroupEventsNotifierController(BaseUserController):
@property
def mixins(self) -> HttpRepo:
return HttpRepo(self.repo, self.logger, self.registered_exceptions, "An unexpected error occurred.")
return HttpRepo(self.repo, self.logger, self.registered_exceptions, self.t("generic.server-error"))
@router.get("", response_model=GroupEventPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):

View File

@ -22,7 +22,7 @@ class GroupReportsController(BaseUserController):
def registered_exceptions(self, ex: type[Exception]) -> str:
return {
**mealie_registered_exceptions(self.translator),
}.get(ex, "An unexpected error occurred.")
}.get(ex, self.t("generic.server-error"))
@cached_property
def mixins(self):
@ -44,6 +44,6 @@ class GroupReportsController(BaseUserController):
def delete_one(self, item_id: UUID4):
try:
self.mixins.delete_one(item_id) # type: ignore
return SuccessResponse.respond("Report deleted.")
return SuccessResponse.respond(self.t("report-deleted"))
except Exception as ex:
raise HTTPException(500, ErrorResponse.respond("Failed to delete report")) from ex

View File

@ -35,7 +35,7 @@ class MultiPurposeLabelsController(BaseUserController):
@property
def mixins(self) -> HttpRepo:
return HttpRepo(self.repo, self.logger, self.registered_exceptions, "An unexpected error occurred.")
return HttpRepo(self.repo, self.logger, self.registered_exceptions, self.t("generic.server-error"))
@router.get("", response_model=MultiPurposeLabelPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):

View File

@ -30,7 +30,7 @@ class GroupMealplanController(BaseCrudController):
registered = {
**mealie_registered_exceptions(self.translator),
}
return registered.get(ex, "An unexpected error occurred.")
return registered.get(ex, self.t("generic.server-error"))
@cached_property
def mixins(self):
@ -87,7 +87,7 @@ class GroupMealplanController(BaseCrudController):
)
except IndexError as e:
raise HTTPException(
status_code=404, detail=ErrorResponse.respond(message="No recipes match your rules")
status_code=404, detail=ErrorResponse.respond(message=self.t("mealplan.no-recipes-match-your-rules"))
) from e
@router.get("", response_model=PlanEntryPagination)

View File

@ -158,7 +158,7 @@ class ShoppingListController(BaseCrudController):
@cached_property
def mixins(self) -> HttpRepo[ShoppingListCreate, ShoppingListOut, ShoppingListSave]:
return HttpRepo(self.repo, self.logger, self.registered_exceptions, "An unexpected error occurred.")
return HttpRepo(self.repo, self.logger, self.registered_exceptions, self.t("generic.server-error"))
@router.get("", response_model=ShoppingListPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):

View File

@ -62,7 +62,9 @@ class UserController(BaseUserController):
def update_password(self, password_change: ChangePassword):
"""Resets the User Password"""
if not verify_password(password_change.current_password, self.user.password):
raise HTTPException(status.HTTP_400_BAD_REQUEST, ErrorResponse.respond("Invalid current password"))
raise HTTPException(
status.HTTP_400_BAD_REQUEST, ErrorResponse.respond(self.t("user.invalid-current-password"))
)
self.user.password = hash_password(password_change.new_password)
try:
@ -73,7 +75,7 @@ class UserController(BaseUserController):
ErrorResponse.respond("Failed to update password"),
) from e
return SuccessResponse.respond("Password updated")
return SuccessResponse.respond(self.t("user.password-updated"))
@user_router.put("/{item_id}")
def update_user(self, item_id: UUID4, new_data: UserBase):
@ -99,4 +101,4 @@ class UserController(BaseUserController):
ErrorResponse.respond("Failed to update user"),
) from e
return SuccessResponse.respond("User updated")
return SuccessResponse.respond(self.t("user.user-updated"))