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="inputCategories" selector-type="categories" />
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" /> <RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
<!-- TODO Make this localizable --> <!-- TODO: proper pluralization of inputDay -->
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }} {{ $t('meal-plan.this-rule-will-apply', {
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }} 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> </div>
</template> </template>

View File

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

View File

@ -53,7 +53,7 @@
<v-icon left> <v-icon left>
{{ $globals.icons.chefHat }} {{ $globals.icons.chefHat }}
</v-icon> </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-item>
</v-list> </v-list>
</v-menu> </v-menu>

View File

@ -50,7 +50,7 @@
<div class="d-flex justify-center flex-wrap"> <div class="d-flex justify-center flex-wrap">
<BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true"> <BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true">
<template #icon> {{ $globals.icons.chefHat }} </template> <template #icon> {{ $globals.icons.chefHat }} </template>
I Made This {{ $t('recipe.made-this') }}
</BaseButton> </BaseButton>
</div> </div>
<div class="d-flex justify-center flex-wrap"> <div class="d-flex justify-center flex-wrap">
@ -63,7 +63,7 @@
<v-icon left> <v-icon left>
{{ $globals.icons.calendar }} {{ $globals.icons.calendar }}
</v-icon> </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> </v-chip>
</div> </div>
</div> </div>

View File

@ -25,12 +25,10 @@
</v-card-actions> </v-card-actions>
<AdvancedOnly> <AdvancedOnly>
<v-card v-if="isEditForm" flat class="ma-2 mb-2"> <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-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs {{ $t('recipe.api-extras-description') }}
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.
<v-row v-for="(_, key) in recipe.extras" :key="key" class="mt-1"> <v-row v-for="(_, key) in recipe.extras" :key="key" class="mt-1">
<v-col cols="8"> <v-col cols="8">
<v-text-field v-model="recipe.extras[key]" dense :label="key"> <v-text-field v-model="recipe.extras[key]" dense :label="key">
@ -45,7 +43,7 @@
</v-card-text> </v-card-text>
<v-card-actions class="d-flex"> <v-card-actions class="d-flex">
<div style="max-width: 200px"> <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> </div>
<BaseButton create small class="ml-5" @click="createApiExtra" /> <BaseButton create small class="ml-5" @click="createApiExtra" />
</v-card-actions> </v-card-actions>

View File

@ -39,7 +39,7 @@
<template #icon> <template #icon>
{{ $globals.icons.foods }} {{ $globals.icons.foods }}
</template> </template>
Parse {{ $t('recipe.parse') }}
</BaseButton> </BaseButton>
</span> </span>
</template> </template>
@ -53,7 +53,7 @@
<script lang="ts"> <script lang="ts">
import draggable from "vuedraggable"; 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 { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe"; import { Recipe } from "~/lib/api/types/recipe";
@ -75,6 +75,7 @@ export default defineComponent({
setup(props) { setup(props) {
const { user } = usePageUser(); const { user } = usePageUser();
const { imageKey } = usePageState(props.recipe.slug); const { imageKey } = usePageState(props.recipe.slug);
const { i18n } = useContext();
const drag = ref(false); const drag = ref(false);
@ -95,11 +96,11 @@ export default defineComponent({
const parserToolTip = computed(() => { const parserToolTip = computed(() => {
if (props.recipe.settings.disableAmount) { 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) { } 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) { function addIngredient(ingredients: Array<string> | null = null) {

View File

@ -6,7 +6,7 @@
:disable-amount="recipe.settings.disableAmount" :disable-amount="recipe.settings.disableAmount"
/> />
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0"> <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-list-item v-for="(tool, index) in recipe.tools" :key="index" dense>
<v-checkbox <v-checkbox
v-model="recipe.tools[index].onHand" v-model="recipe.tools[index].onHand"

View File

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

View File

@ -38,7 +38,7 @@
<!-- Recipe Tools Edit --> <!-- Recipe Tools Edit -->
<v-card v-if="isEditForm" class="mt-2"> <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-divider class="mx-2" />
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" /> <RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" />

View File

@ -9,7 +9,7 @@
{{ toastAlert.text }} {{ toastAlert.text }}
<template #action="{ attrs }"> <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> </template>
</v-snackbar> </v-snackbar>
<v-snackbar <v-snackbar

View File

@ -106,8 +106,9 @@ export default defineComponent({
}, },
submitText: { submitText: {
type: String, type: String,
// TODO Figure out how to localize this default value default: function () {
default: () => "Create", return this.$t("general.create");
}
}, },
keepOpen: { keepOpen: {
default: false, default: false,
@ -117,6 +118,8 @@ export default defineComponent({
setup(props, context) { setup(props, context) {
const dialog = computed<boolean>({ const dialog = computed<boolean>({
get() { 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; return props.value;
}, },
set(val) { set(val) {

View File

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

View File

@ -1,16 +1,18 @@
import { useContext } from "@nuxtjs/composition-api";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { alert } from "./use-toast"; import { alert } from "./use-toast";
export function useCopy() { export function useCopy() {
const { copy, copied, isSupported } = useClipboard(); const { copy, copied, isSupported } = useClipboard();
const { i18n } = useContext();
function copyText(text: string) { function copyText(text: string) {
if (!isSupported) { if (!isSupported) {
alert.error("Clipboard not supported"); alert.error(i18n.tc("general.clipboard-not-supported"));
return; return;
} }
copy(text); copy(text);
alert.success("Copied to clipboard"); alert.success(i18n.tc("general.copied-to-clipboard"));
} }
return { copyText, copied }; return { copyText, copied };
@ -18,10 +20,11 @@ export function useCopy() {
export function useCopyList() { export function useCopyList() {
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
const { i18n } = useContext();
function checkClipboard() { function checkClipboard() {
if (!isSupported) { if (!isSupported) {
alert.error("Your browser does not support clipboard"); alert.error(i18n.tc("general.your-browser-does-not-support-clipboard"));
return false; return false;
} }
@ -51,7 +54,7 @@ export function useCopyList() {
function copyText(text: string, len: number) { function copyText(text: string, len: number) {
copy(text).then(() => { 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 { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook"; import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
@ -25,6 +25,8 @@ export const useCookbooks = function () {
const api = useUserApi(); const api = useUserApi();
const loading = ref(false); const loading = ref(false);
const { i18n } = useContext();
const actions = { const actions = {
getAll() { getAll() {
loading.value = true; loading.value = true;
@ -54,7 +56,7 @@ export const useCookbooks = function () {
async createOne() { async createOne() {
loading.value = true; loading.value = true;
const { data } = await api.cookbooks.createOne({ 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) { if (data && cookbookStore?.value) {
cookbookStore.value.push(data); cookbookStore.value.push(data);

View File

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

View File

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

View File

@ -21,7 +21,8 @@
"production": "Production", "production": "Production",
"support": "Support", "support": "Support",
"version": "Version", "version": "Version",
"unknown-version": "unknown" "unknown-version": "unknown",
"sponsor": "Sponsor"
}, },
"asset": { "asset": {
"assets": "Assets", "assets": "Assets",
@ -163,7 +164,17 @@
"transfer": "Transfer", "transfer": "Transfer",
"copy": "Copy", "copy": "Copy",
"color": "Color", "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": { "group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?", "are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
@ -187,7 +198,25 @@
"settings": { "settings": {
"keep-my-recipes-private": "Keep My Recipes Private", "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." "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": { "meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan", "create-a-new-meal-plan": "Create a New Meal Plan",
@ -222,7 +251,28 @@
"lunch": "Lunch", "lunch": "Lunch",
"dinner": "Dinner", "dinner": "Dinner",
"type-any": "Any", "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": { "migration": {
"chowdown": { "chowdown": {
@ -374,7 +424,48 @@
"open-timeline": "Open Timeline", "open-timeline": "Open Timeline",
"made-this": "I Made This", "made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?", "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": { "search": {
"advanced-search": "Advanced Search", "advanced-search": "Advanced Search",
@ -388,7 +479,8 @@
"search-mealie": "Search Mealie (press /)", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...", "search-placeholder": "Search...",
"tag-filter": "Tag Filter", "tag-filter": "Tag Filter",
"search-hint": "Press '/'" "search-hint": "Press '/'",
"advanced": "Advanced"
}, },
"settings": { "settings": {
"add-a-new-theme": "Add a New Theme", "add-a-new-theme": "Add a New Theme",
@ -501,7 +593,17 @@
"note": "Note", "note": "Note",
"label": "Label", "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.", "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": { "sidebar": {
"all-recipes": "All Recipes", "all-recipes": "All Recipes",
@ -553,7 +655,8 @@
"create-a-tool": "Create a Tool", "create-a-tool": "Create a Tool",
"tool-name": "Tool Name", "tool-name": "Tool Name",
"create-new-tool": "Create New Tool", "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": { "user": {
"admin": "Admin", "admin": "Admin",
@ -612,7 +715,19 @@
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user", "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": "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", "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": { "language-dialog": {
"translated": "translated", "translated": "translated",
@ -622,19 +737,70 @@
"read-the-docs": "Read the docs" "read-the-docs": "Read the docs"
}, },
"data-pages": { "data-pages": {
"seed-data": "Seed Data",
"foods": { "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-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}", "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-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": { "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": { "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": "User Registration", "user-registration": "User Registration",
@ -746,5 +912,54 @@
"mainentance": { "mainentance": {
"actions-title": "Actions" "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 // Build Configuration: https://go.nuxtjs.dev/config-build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,16 @@
<template #header> <template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img> <v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
</template> </template>
<template #title> Group Settings </template> <template #title> {{ $t('profile.group-settings') }} </template>
These items are shared within your group. Editing one of them will change it for the whole group! {{ $t('profile.group-description') }}
</BasePageTitle> </BasePageTitle>
<section v-if="group"> <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-checkbox
v-model="group.preferences.privateGroup" v-model="group.preferences.privateGroup"
class="mt-n4" class="mt-n4"
label="Private Group" :label="$t('group.private-group')"
@change="groupActions.updatePreferences()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-select <v-select
@ -28,45 +28,44 @@
</section> </section>
<section v-if="group"> <section v-if="group">
<BaseCardSectionTitle class="mt-10" title="Default Recipe Preferences"> <BaseCardSectionTitle class="mt-10" :title="$tc('group.default-recipe-preferences')">
These are the default settings when a new recipe is created in your group. These can be changed for individual {{ $t('group.default-recipe-preferences-description') }}
recipes in the recipe settings menu.
</BaseCardSectionTitle> </BaseCardSectionTitle>
<v-checkbox <v-checkbox
v-model="group.preferences.recipePublic" v-model="group.preferences.recipePublic"
class="mt-n4" 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()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-checkbox <v-checkbox
v-model="group.preferences.recipeShowNutrition" v-model="group.preferences.recipeShowNutrition"
class="mt-n4" class="mt-n4"
label="Show nutrition information" :label="$t('group.show-nutrition-information')"
@change="groupActions.updatePreferences()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-checkbox <v-checkbox
v-model="group.preferences.recipeShowAssets" v-model="group.preferences.recipeShowAssets"
class="mt-n4" class="mt-n4"
label="Show recipe assets" :label="$t('group.show-recipe-assets')"
@change="groupActions.updatePreferences()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-checkbox <v-checkbox
v-model="group.preferences.recipeLandscapeView" v-model="group.preferences.recipeLandscapeView"
class="mt-n4" class="mt-n4"
label="Default to landscape view" :label="$t('group.default-to-landscape-view')"
@change="groupActions.updatePreferences()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-checkbox <v-checkbox
v-model="group.preferences.recipeDisableComments" v-model="group.preferences.recipeDisableComments"
class="mt-n4" class="mt-n4"
label="Disable users from commenting on recipes" :label="$t('group.disable-users-from-commenting-on-recipes')"
@change="groupActions.updatePreferences()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
<v-checkbox <v-checkbox
v-model="group.preferences.recipeDisableAmount" v-model="group.preferences.recipeDisableAmount"
class="mt-n4" 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()" @change="groupActions.updatePreferences()"
></v-checkbox> ></v-checkbox>
</section> </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-date-picker v-model="newMeal.date" :first-day-of-week="firstDayOfWeek" no-title @input="pickerMenu = false"></v-date-picker>
</v-menu> </v-menu>
<v-card-text> <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-select>
<v-autocomplete <v-autocomplete
v-if="!dialog.note" v-if="!dialog.note"
v-model="newMeal.recipeId" v-model="newMeal.recipeId"
label="Meal Recipe" :label="$t('meal-plan.meal-recipe')"
:items="allRecipes" :items="allRecipes"
item-text="name" item-text="name"
item-value="id" item-value="id"
:return-object="false" :return-object="false"
></v-autocomplete> ></v-autocomplete>
<template v-else> <template v-else>
<v-text-field v-model="newMeal.title" label="Meal Title"> </v-text-field> <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="Meal Note"> </v-textarea> <v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')"> </v-textarea>
</template> </template>
</v-card-text> </v-card-text>
<v-card-actions class="my-0 py-0"> <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-actions>
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
@ -71,8 +71,8 @@
</div> </div>
</div> </div>
<div class="d-flex align-center justify-space-between"> <div class="d-flex align-center justify-space-between">
<v-switch v-model="edit" label="Editor"></v-switch> <v-switch v-model="edit" :label="$t('meal-plan.editor')"></v-switch>
<ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" text="Settings" /> <ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" :text="$tc('general.settings')" />
</div> </div>
<v-row class=""> <v-row class="">
<v-col <v-col
@ -174,7 +174,7 @@
:buttons="[ :buttons="[
{ {
icon: $globals.icons.diceMultiple, icon: $globals.icons.diceMultiple,
text: 'Random Meal', text: $tc('meal-plan.random-meal'),
event: 'random', event: 'random',
children: [ children: [
{ {
@ -185,19 +185,19 @@
{ {
icon: $globals.icons.diceMultiple, icon: $globals.icons.diceMultiple,
text: 'Lunch', text: $tc('meal-plan.lunch'),
event: 'randomLunch', event: 'randomLunch',
}, },
], ],
}, },
{ {
icon: $globals.icons.potSteam, icon: $globals.icons.potSteam,
text: 'Random Dinner', text: $tc('meal-plan.random-dinner'),
event: 'randomDinner', event: 'randomDinner',
}, },
{ {
icon: $globals.icons.bowlMixOutline, icon: $globals.icons.bowlMixOutline,
text: 'Random Side', text: $tc('meal-plan.random-side'),
event: 'randomSide', event: 'randomSide',
}, },
{ {

View File

@ -4,20 +4,15 @@
<template #header> <template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img> <v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
</template> </template>
<template #title> Meal Plan Rules </template> <template #title> {{ $t('meal-plan.meal-plan-rules') }} </template>
You can create rules for auto selecting recipes for you meal plans. These rules are used by the server to {{ $t('meal-plan.meal-plan-rules-description') }}
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.
</BasePageTitle> </BasePageTitle>
<v-card> <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-divider class="mx-2"></v-divider>
<v-card-text> <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 {{ $t('meal-plan.new-rule-description') }}
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.
<GroupMealPlanRuleForm <GroupMealPlanRuleForm
class="mt-2" class="mt-2"
@ -33,13 +28,13 @@
</v-card> </v-card>
<section> <section>
<BaseCardSectionTitle class="mt-10" title="Recipe Rules" /> <BaseCardSectionTitle class="mt-10" :title="$tc('meal-plan.recipe-rules')" />
<div> <div>
<div v-for="(rule, idx) in allRules" :key="rule.id"> <div v-for="(rule, idx) in allRules" :key="rule.id">
<v-card class="my-2 left-border"> <v-card class="my-2 left-border">
<v-card-title class="headline pb-1"> <v-card-title class="headline pb-1">
{{ rule.day === "unset" ? "Applies to all days" : `Applies on ${rule.day}s` }} {{ rule.day === "unset" ? $t('meal-plan.applies-to-all-days') : $t('meal-plan.applies-on-days', [rule.day]) }}
{{ rule.entryType === "unset" ? "for all meal types" : ` for ${rule.entryType} meal types` }} {{ rule.entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [rule.entryType]) }}
<span class="ml-auto"> <span class="ml-auto">
<BaseButtonGroup <BaseButtonGroup
:buttons="[ :buttons="[
@ -91,7 +86,7 @@
</template> </template>
<script lang="ts"> <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 { useUserApi } from "~/composables/api";
import { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan"; import { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan";
import GroupMealPlanRuleForm from "~/components/Domain/Group/GroupMealPlanRuleForm.vue"; import GroupMealPlanRuleForm from "~/components/Domain/Group/GroupMealPlanRuleForm.vue";
@ -182,8 +177,10 @@ export default defineComponent({
toggleEditState, toggleEditState,
}; };
}, },
head: { head() {
title: "Meal Plan Settings", return {
title: this.$tc("meal-plan.meal-plan-settings"),
};
}, },
}); });
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,9 @@
<div> <div>
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)"> <v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
<div> <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> <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 {{ $t('recipe.recipe-debugger-description') }}
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.
<v-text-field <v-text-field
v-model="recipeUrl" v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')" :label="$t('new-recipe.recipe-url')"
@ -28,14 +26,14 @@
<template #icon> <template #icon>
{{ $globals.icons.robot }} {{ $globals.icons.robot }}
</template> </template>
Debug {{ $t('recipe.debug') }}
</BaseButton> </BaseButton>
</div> </div>
</v-card-actions> </v-card-actions>
</div> </div>
</v-form> </v-form>
<section v-if="debugData"> <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 <LazyRecipeJsonEditor
v-model="debugData" v-model="debugData"
class="primary" class="primary"

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <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> <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-form ref="domCreateByName">
<v-text-field <v-text-field
v-model="newRecipeName" v-model="newRecipeName"
@ -15,7 +15,7 @@
class="rounded-lg mt-2" class="rounded-lg mt-2"
rounded rounded
:rules="[validators.required]" :rules="[validators.required]"
hint="New recipe names must be unique" :hint="$t('recipe.new-recipe-names-must-be-unique')"
persistent-hint persistent-hint
></v-text-field> ></v-text-field>
</v-form> </v-form>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <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> <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-form ref="domCreateByOcr"> </v-form>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center"> <v-card-actions class="justify-center">
@ -15,7 +15,7 @@
class="rounded-lg mt-2" class="rounded-lg mt-2"
rounded rounded
truncate-length="100" 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 persistent-hint
prepend-icon="" prepend-icon=""
:prepend-inner-icon="$globals.icons.fileImage" :prepend-inner-icon="$globals.icons.fileImage"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,13 @@
</BaseDialog> </BaseDialog>
<BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne"> <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> </BaseDialog>
<BasePageTitle divider> <BasePageTitle divider>
<template #header> <template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img> <v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img>
</template> </template>
<template #title> Shopping Lists </template> <template #title>{{ $t('shopping-list.shopping-lists') }}</template>
</BasePageTitle> </BasePageTitle>
<BaseButton create @click="createDialog = true" /> <BaseButton create @click="createDialog = true" />
@ -33,7 +33,7 @@
</v-card> </v-card>
</section> </section>
<div class="d-flex justify-end mt-10"> <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> </div>
</v-container> </v-container>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class RecipeCommentRoutes(BaseUserController):
@property @property
def mixins(self) -> HttpRepo: 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: def _check_comment_belongs_to_user(self, item_id: UUID4) -> None:
comment = self.repo.get_one(item_id) comment = self.repo.get_one(item_id)

View File

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

View File

@ -49,7 +49,7 @@ class GroupEventsNotifierController(BaseUserController):
@property @property
def mixins(self) -> HttpRepo: 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) @router.get("", response_model=GroupEventPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): 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: def registered_exceptions(self, ex: type[Exception]) -> str:
return { return {
**mealie_registered_exceptions(self.translator), **mealie_registered_exceptions(self.translator),
}.get(ex, "An unexpected error occurred.") }.get(ex, self.t("generic.server-error"))
@cached_property @cached_property
def mixins(self): def mixins(self):
@ -44,6 +44,6 @@ class GroupReportsController(BaseUserController):
def delete_one(self, item_id: UUID4): def delete_one(self, item_id: UUID4):
try: try:
self.mixins.delete_one(item_id) # type: ignore self.mixins.delete_one(item_id) # type: ignore
return SuccessResponse.respond("Report deleted.") return SuccessResponse.respond(self.t("report-deleted"))
except Exception as ex: except Exception as ex:
raise HTTPException(500, ErrorResponse.respond("Failed to delete report")) from ex raise HTTPException(500, ErrorResponse.respond("Failed to delete report")) from ex

View File

@ -35,7 +35,7 @@ class MultiPurposeLabelsController(BaseUserController):
@property @property
def mixins(self) -> HttpRepo: 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) @router.get("", response_model=MultiPurposeLabelPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):

View File

@ -30,7 +30,7 @@ class GroupMealplanController(BaseCrudController):
registered = { registered = {
**mealie_registered_exceptions(self.translator), **mealie_registered_exceptions(self.translator),
} }
return registered.get(ex, "An unexpected error occurred.") return registered.get(ex, self.t("generic.server-error"))
@cached_property @cached_property
def mixins(self): def mixins(self):
@ -87,7 +87,7 @@ class GroupMealplanController(BaseCrudController):
) )
except IndexError as e: except IndexError as e:
raise HTTPException( 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 ) from e
@router.get("", response_model=PlanEntryPagination) @router.get("", response_model=PlanEntryPagination)

View File

@ -158,7 +158,7 @@ class ShoppingListController(BaseCrudController):
@cached_property @cached_property
def mixins(self) -> HttpRepo[ShoppingListCreate, ShoppingListOut, ShoppingListSave]: 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) @router.get("", response_model=ShoppingListPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):

View File

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