From 17a5737dae3e8b4c455f5dfb6c6b8ae848d8e1bc Mon Sep 17 00:00:00 2001 From: Lucas Pains Date: Fri, 31 May 2024 18:14:43 -0600 Subject: [PATCH] Recipe language switcher (#305) * Added recipe language switcher * Added tests * Fixed issue where existing recipes won't edit * Fixes data restored from backups * Fixed tests * include nutrition in backups * Fixed failing tests --- CNAME | 1 - index.html | 6 +- public/locales/en/translation.json | 8 +- public/locales/pt/translation.json | 8 +- src/components/RoundButton.vue | 25 ++++++ src/pages/options.vue | 19 ++++- src/pages/recipe/[id]/edit.vue | 119 +++++++++++++++++++++++------ src/pages/recipe/[id]/index.vue | 7 +- src/pages/recipe/import-backup.vue | 10 ++- src/services/dataService.ts | 5 ++ src/services/recipe.ts | 1 + tests/display.spec.ts | 3 +- tests/edit.spec.ts | 27 +++++++ tests/options.spec.ts | 4 +- 14 files changed, 202 insertions(+), 41 deletions(-) delete mode 100644 CNAME create mode 100644 src/components/RoundButton.vue diff --git a/CNAME b/CNAME deleted file mode 100644 index e8783ec7..00000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -app.sharpcooking.net \ No newline at end of file diff --git a/index.html b/index.html index 179ae5bc..9f4e51c2 100644 --- a/index.html +++ b/index.html @@ -21,12 +21,12 @@ - - + + - + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 8feec809..56b4328d 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -66,7 +66,9 @@ "enableCloudShare": "Enable cloud share (preview)", "enableCloudShareDescription": "Allows for sharing recipes via cloud with a code. Shared recipes are available for 1 hour for free. By enabling this feature, you agree to the Sharp Cooking Privacy Policy.", "enableNutritionFacts": "Enable nutrition labels (preview)", - "enableNutritionFactsDescription": "Allows for recipes imported or inputted with nutrition facts to display a nutrition facts label." + "enableNutritionFactsDescription": "Allows for recipes imported or inputted with nutrition facts to display a nutrition facts label.", + "enableRecipeLanguageSwitcher": "Enable recipe language switcher", + "enableRecipeLanguageSwitcherDescription": "Allows for selecting which language a recipe is written in. This directly influences the parsing of recipe ingredients and steps." }, "recipe": { "id": { @@ -121,7 +123,9 @@ "carbohydrates": "Carbohydrates (g)", "fiber": "Fiber (g)", "sugar": "Sugar (g)", - "protein": "Protein (g)" + "protein": "Protein (g)", + "changeLanguage": "Change Language", + "changeLanguageTitle": "Change recipe language" }, "gallery": { "recipeImage": "Recipe Image" diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 87f04e3e..d419bae6 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -66,7 +66,9 @@ "enableCloudShare": "Ativar compartilhamento na nuvem (preview)", "enableCloudShareDescription": "Permite compartilhar receitas via nuvem com código. Receitas compartilhadas estão disponíveis gratuitamente por 1 hora. Ao ativar este recurso, você concorda com a Política de privacidade do Sharp Cooking.", "enableNutritionFacts": "Ativar informações nutricionais (preview)", - "enableNutritionFactsDescription": "Permite que receitas importadas ou inseridas com informações nutricionais exibam um rótulo de informações nutricionais." + "enableNutritionFactsDescription": "Permite que receitas importadas ou inseridas com informações nutricionais exibam um rótulo de informações nutricionais.", + "enableRecipeLanguageSwitcher": "Ativar alternador de idioma da receita", + "enableRecipeLanguageSwitcherDescription": "Permite selecionar em qual idioma a receita está escrita. Isso influencia diretamente a análise dos ingredientes e passos da receita." }, "recipe": { "id": { @@ -121,7 +123,9 @@ "carbohydrates": "Carboidratos (g)", "fiber": "Fibra alimentar (g)", "sugar": "Açúcares (g)", - "protein": "Proteínas (g)" + "protein": "Proteínas (g)", + "changeLanguage": "Alterar idioma", + "changeLanguageTitle": "Alterar idioma da receita" }, "gallery": { "recipeImage": "Imagem de Receita" diff --git a/src/components/RoundButton.vue b/src/components/RoundButton.vue new file mode 100644 index 00000000..96da80c6 --- /dev/null +++ b/src/components/RoundButton.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/pages/options.vue b/src/pages/options.vue index bb00e803..18b6d113 100644 --- a/src/pages/options.vue +++ b/src/pages/options.vue @@ -17,6 +17,7 @@ const useFractions = ref(false); const enableYoutubeVideos = ref(false); const enableCloudShare = ref(false); const enableNutritionFacts = ref(false); +const enableRecipeLanguageSwitcher = ref(false); const stepsInterval = ref(5); const stepsIntervalEditing = ref(5); const isStepsIntervalModalOpen = ref(false); @@ -35,12 +36,14 @@ onMounted(async () => { const enableYoutubeVideosValue = await getSetting("EnableYoutubeVideos", "false"); const enableCloudShareValue = await getSetting("EnableCloudShare", "false"); const enableNutritionFactsValue = await getSetting("EnableNutritionFacts", "false"); + const enableRecipeLanguageSwitcherValue = await getSetting("EnableRecipeLanguageSwitcher", "false"); stepsInterval.value = parseInt(stepsInvervalValue); useFractions.value = useFractionsValue === "true"; enableYoutubeVideos.value = enableYoutubeVideosValue === "true"; enableCloudShare.value = enableCloudShareValue === "true"; enableNutritionFacts.value = enableNutritionFactsValue === "true"; + enableRecipeLanguageSwitcher.value = enableRecipeLanguageSwitcherValue === "true"; version.value = import.meta.env.VITE_APP_VERSION; selectedLanguage.value = i18next.language; storageDescription.value = await getStorageDescription(i18next.language); @@ -117,6 +120,10 @@ function updateEnableNutritionFacts() { saveSetting("EnableNutritionFacts", `${enableNutritionFacts.value}`); } +function updateEnableRecipeLanguageSwitcher() { + saveSetting("EnableRecipeLanguageSwitcher", `${enableRecipeLanguageSwitcher.value}`); +} + function showChangeLanguageModal() { selectedLanguage.value = i18next.language; isLanguagesModalOpen.value = true; @@ -146,7 +153,7 @@ async function setSelectedLanguage() {
-
+
+ {{ t("pages.options.enableRecipeLanguageSwitcher") }} + +
+ {{ t("pages.options.enableRecipeLanguageSwitcherDescription") }} +
+
{{ t("pages.options.storageStats") }}
diff --git a/src/pages/recipe/[id]/edit.vue b/src/pages/recipe/[id]/edit.vue index 7b369f93..680803e5 100644 --- a/src/pages/recipe/[id]/edit.vue +++ b/src/pages/recipe/[id]/edit.vue @@ -24,6 +24,8 @@ import { Cropper } from 'vue-advanced-cropper'; import 'vue-advanced-cropper/dist/style.css'; import { fetchWithRetry } from "../../../services/fetchWithRetry"; import { isVideoUrlSupported, prepareUrlForEmbed } from "../../../helpers/videoHelpers"; +import RoundButton from "../../../components/RoundButton.vue"; +import i18next from "i18next"; const state = useState()!; const route = useRoute(); @@ -44,6 +46,7 @@ const item = ref({ notes: "", imageAvailable: false, multiplier: 1, + language: i18next.language, nutrition: { servingSize: 0, totalFat: 0, @@ -53,12 +56,17 @@ const item = ref({ cholesterol: 0, calories: 0, carbohydrates: 0, + fiber: 0, + sugar: 0, + transFat: 0, + unsaturatedFat: 0 } } as RecipeViewModel); const images = ref([] as Array); const isDirtyModalOpen = ref(false); const isImportFromUrlModalOpen = ref(false); const isImportFromShareModalOpen = ref(false); +const isLanguageModalOpen = ref(false); const importRecipeUrl = ref(""); const importRecipeCode = ref(""); const isImporting = ref(false); @@ -73,9 +81,12 @@ const isCropping = ref(false); const isAddVideoModalOpen = ref(false); const addVideoUrl = ref(""); const addVideoUrlError = ref(""); +const selectedLanguage = ref(""); +const availableLanguages = ref(["pt-BR", "en-US"] as Array); let enableYoutubeVideos = false; let enableNutritionFacts = false; +let enableRecipeLanguageSwitcher = false; watch( item, @@ -109,6 +120,9 @@ onMounted(async () => { const enableNutritionFactsSetting = await getSetting("EnableNutritionFacts", "false"); enableNutritionFacts = enableNutritionFactsSetting == "true"; + const enableRecipeLanguageSwitcherSetting = await getSetting("EnableRecipeLanguageSwitcher", "false"); + enableRecipeLanguageSwitcher = enableRecipeLanguageSwitcherSetting == "true"; + if (query.value.importFromUrl == "1") { isImportFromUrlModalOpen.value = true; } @@ -134,6 +148,7 @@ onMounted(async () => { : state.message.steps; recipe.notes = state.message.notes; recipe.score = 3; + recipe.language = i18next.language; } else if (id.value > 0) { recipe = (await getRecipe(id.value)) as RecipeViewModel; } @@ -150,13 +165,14 @@ onMounted(async () => { recipe.imageAvailable = images.value.length > 0; selectedImage.value = 0; } - item.value = recipe; await nextTick(); isDirty = false; } + + selectedLanguage.value = item.value.language ?? i18next.language; }); router.beforeEach(async (to, from) => { @@ -307,6 +323,7 @@ async function importRecipeFromUrl() { item.value.steps = html.steps.map((x: any) => x.raw); item.value.nutrition = html.nutrients; item.value.nutrition.servingSize = html.yields; + item.value.language = html.language; if (html.image) { images.value.push(new RecipeMedia(id.value, "img", html.image)); @@ -357,6 +374,7 @@ async function importRecipeFromCode() { item.value.notes = importRecipe.notes; item.value.ingredients = importRecipe.ingredients; item.value.steps = importRecipe.steps; + item.value.language = importRecipe.language; images.value = importRecipe.media.map((item: any) => { return new RecipeMedia(id.value, item.type, item.url); }); @@ -490,6 +508,11 @@ function addVideo() { isAddVideoModalOpen.value = false; } + +function changeLanguage() { + item.value.language = selectedLanguage.value; + isLanguageModalOpen.value = false; +}