fix: translate dietary tags, recipe tags form, edit title, nutrition bar (v0.27.2)
These were hardcoded English strings ignoring app locale: DietaryTagPicker, the recipe tag-input label/placeholder/help/aria-label, the Edit recipe page heading, and WeeklyNutritionBar's labels/messages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
|
||||
|
||||
## 0.27.2 — 2026-07-14 13:20
|
||||
|
||||
### Fixed
|
||||
- Dietary tag chips, the recipe tag-adding form, the "Edit recipe" title, and the meal-plan nutrition-goals bar were all hardcoded in English regardless of app language — now translated.
|
||||
|
||||
## 0.27.1 — 2026-07-14 12:05
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -6,6 +6,7 @@ import { db, recipes } from "@epicure/db";
|
||||
import { and, eq } from "@epicure/db";
|
||||
import { RecipeForm } from "@/components/recipe/recipe-form";
|
||||
import { getPublicUrl } from "@/lib/storage";
|
||||
import { getMessages } from "@/lib/i18n/server";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -28,6 +29,8 @@ export default async function EditRecipePage({ params }: Params) {
|
||||
|
||||
if (!recipe) notFound();
|
||||
|
||||
const m = getMessages((session.user as { locale?: string }).locale);
|
||||
|
||||
const defaultValues = {
|
||||
title: recipe.title,
|
||||
description: recipe.description ?? undefined,
|
||||
@@ -71,7 +74,7 @@ export default async function EditRecipePage({ params }: Params) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Edit recipe</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{m.recipes.editTitle}</h1>
|
||||
<p className="text-muted-foreground mt-1">{recipe.title}</p>
|
||||
</div>
|
||||
<RecipeForm recipeId={id} defaultValues={defaultValues} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type NutritionTotals = {
|
||||
calories: number;
|
||||
@@ -45,6 +46,7 @@ type BarItem = {
|
||||
};
|
||||
|
||||
export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
const t = useTranslations("mealPlan.nutritionBar");
|
||||
const [data, setData] = useState<NutritionResponse | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,7 +62,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
|
||||
const bars: BarItem[] = [
|
||||
{
|
||||
label: "Calories",
|
||||
label: t("calories"),
|
||||
value: dailyAverage.calories,
|
||||
goal: goals.caloriesKcal,
|
||||
unit: "kcal",
|
||||
@@ -68,7 +70,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
colorClass: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
label: "Protein",
|
||||
label: t("protein"),
|
||||
value: dailyAverage.protein,
|
||||
goal: goals.proteinG,
|
||||
unit: "g",
|
||||
@@ -76,7 +78,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
colorClass: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
label: "Carbs",
|
||||
label: t("carbs"),
|
||||
value: dailyAverage.carbs,
|
||||
goal: goals.carbsG,
|
||||
unit: "g",
|
||||
@@ -84,7 +86,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
colorClass: "bg-green-500",
|
||||
},
|
||||
{
|
||||
label: "Fat",
|
||||
label: t("fat"),
|
||||
value: dailyAverage.fat,
|
||||
goal: goals.fatG,
|
||||
unit: "g",
|
||||
@@ -95,7 +97,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">Daily average vs. your goals</p>
|
||||
<p className="text-xs text-muted-foreground">{t("dailyAverageVsGoals")}</p>
|
||||
{bars.map((bar) => {
|
||||
if (bar.goal == null) return null;
|
||||
const width = Math.min(bar.percentage, 100);
|
||||
@@ -118,7 +120,7 @@ export function WeeklyNutritionBar({ weekStart }: WeeklyNutritionBarProps) {
|
||||
})}
|
||||
{unknownCount > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{unknownCount} planned {unknownCount === 1 ? "meal" : "meals"} missing nutrition data — estimate it from the recipe page for a more accurate picture.
|
||||
{t(unknownCount === 1 ? "missingDataSingular" : "missingDataPlural", { count: unknownCount })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type DietaryTags = {
|
||||
vegan?: boolean;
|
||||
vegetarian?: boolean;
|
||||
@@ -10,15 +12,7 @@ type DietaryTags = {
|
||||
kosher?: boolean;
|
||||
};
|
||||
|
||||
const TAG_LABELS: Record<keyof DietaryTags, string> = {
|
||||
vegan: "Vegan",
|
||||
vegetarian: "Vegetarian",
|
||||
glutenFree: "Gluten-free",
|
||||
dairyFree: "Dairy-free",
|
||||
nutFree: "Nut-free",
|
||||
halal: "Halal",
|
||||
kosher: "Kosher",
|
||||
};
|
||||
const TAG_KEYS: (keyof DietaryTags)[] = ["vegan", "vegetarian", "glutenFree", "dairyFree", "nutFree", "halal", "kosher"];
|
||||
|
||||
export function DietaryTagPicker({
|
||||
value,
|
||||
@@ -27,9 +21,11 @@ export function DietaryTagPicker({
|
||||
value: DietaryTags;
|
||||
onChange: (tags: DietaryTags) => void;
|
||||
}) {
|
||||
const t = useTranslations("recipe.dietary");
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(Object.entries(TAG_LABELS) as [keyof DietaryTags, string][]).map(([key, label]) => {
|
||||
{TAG_KEYS.map((key) => {
|
||||
const label = t(key);
|
||||
const active = !!value[key];
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -431,7 +431,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
|
||||
{/* Tags */}
|
||||
<div className="space-y-2">
|
||||
<Label>Tags</Label>
|
||||
<Label>{t("tags")}</Label>
|
||||
<div
|
||||
className="flex flex-wrap gap-1.5 min-h-[36px] rounded-lg border border-input bg-transparent px-2.5 py-1.5 cursor-text focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50"
|
||||
onClick={() => tagInputRef.current?.focus()}
|
||||
@@ -447,7 +447,7 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); removeTag(tag); }}
|
||||
className="hover:text-foreground transition-colors"
|
||||
aria-label={`Remove tag ${tag}`}
|
||||
aria-label={t("removeTagAriaLabel", { tag })}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
@@ -460,12 +460,12 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleTagKeyDown}
|
||||
onBlur={() => { if (tagInput.trim()) addTag(tagInput); }}
|
||||
placeholder={tags.length === 0 ? "Add tags… (press Enter)" : ""}
|
||||
placeholder={tags.length === 0 ? t("tagsPlaceholder") : ""}
|
||||
className="flex-1 min-w-[120px] bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Press Enter to add · Backspace to remove last · max 20 tags</p>
|
||||
<p className="text-xs text-muted-foreground">{t("tagsHelp")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
||||
export const APP_VERSION = "0.27.1";
|
||||
export const APP_VERSION = "0.27.2";
|
||||
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
|
||||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "0.27.2",
|
||||
date: "2026-07-14 13:20",
|
||||
fixed: [
|
||||
"Dietary tag chips, the recipe tag-adding form, the \"Edit recipe\" title, and the meal-plan nutrition-goals bar were all hardcoded in English regardless of app language — now translated.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.27.1",
|
||||
date: "2026-07-14 12:05",
|
||||
|
||||
@@ -633,6 +633,7 @@
|
||||
"new": "New recipe",
|
||||
"newTitle": "New recipe",
|
||||
"newSubtitle": "Build your recipe from scratch",
|
||||
"editTitle": "Edit recipe",
|
||||
"importUrl": "Import URL",
|
||||
"generate": "Generate",
|
||||
"batchCooking": "Batch cooking",
|
||||
@@ -695,6 +696,7 @@
|
||||
"tags": "Tags",
|
||||
"tagsHelp": "Press Enter to add · Backspace to remove last · max 20 tags",
|
||||
"tagsPlaceholder": "Add tags… (press Enter)",
|
||||
"removeTagAriaLabel": "Remove tag {tag}",
|
||||
"dietaryTags": "Dietary tags",
|
||||
"photos": "Photos",
|
||||
"ingredients": "Ingredients",
|
||||
@@ -752,6 +754,15 @@
|
||||
},
|
||||
"mealPlan": {
|
||||
"title": "Meal Plan",
|
||||
"nutritionBar": {
|
||||
"dailyAverageVsGoals": "Daily average vs. your goals",
|
||||
"calories": "Calories",
|
||||
"protein": "Protein",
|
||||
"carbs": "Carbs",
|
||||
"fat": "Fat",
|
||||
"missingDataSingular": "{count} planned meal missing nutrition data — estimate it from the recipe page for a more accurate picture.",
|
||||
"missingDataPlural": "{count} planned meals missing nutrition data — estimate it from the recipe page for a more accurate picture."
|
||||
},
|
||||
"shoppingLists": "Shopping lists",
|
||||
"print": "Print",
|
||||
"shareTitle": "Share this week's plan",
|
||||
|
||||
@@ -624,6 +624,7 @@
|
||||
"new": "Nouvelle recette",
|
||||
"newTitle": "Nouvelle recette",
|
||||
"newSubtitle": "Créez votre recette depuis zéro",
|
||||
"editTitle": "Modifier la recette",
|
||||
"importUrl": "Importer une URL",
|
||||
"generate": "Générer",
|
||||
"batchCooking": "Batch cooking",
|
||||
@@ -683,6 +684,10 @@
|
||||
"cookMins": "Cuisson (min)",
|
||||
"difficulty": "Difficulté",
|
||||
"visibilityMenuLabel": "Visibilité",
|
||||
"tags": "Tags",
|
||||
"tagsHelp": "Appuyez sur Entrée pour ajouter · Retour arrière pour supprimer le dernier · 20 tags max",
|
||||
"tagsPlaceholder": "Ajouter des tags… (Entrée)",
|
||||
"removeTagAriaLabel": "Supprimer le tag {tag}",
|
||||
"dietaryTags": "Tags alimentaires",
|
||||
"photos": "Photos",
|
||||
"ingredients": "Ingrédients",
|
||||
@@ -740,6 +745,15 @@
|
||||
},
|
||||
"mealPlan": {
|
||||
"title": "Planning repas",
|
||||
"nutritionBar": {
|
||||
"dailyAverageVsGoals": "Moyenne quotidienne vs vos objectifs",
|
||||
"calories": "Calories",
|
||||
"protein": "Protéines",
|
||||
"carbs": "Glucides",
|
||||
"fat": "Lipides",
|
||||
"missingDataSingular": "{count} repas planifié manque de données nutritionnelles — estimez-les depuis la page de la recette pour un résultat plus précis.",
|
||||
"missingDataPlural": "{count} repas planifiés manquent de données nutritionnelles — estimez-les depuis la page de la recette pour un résultat plus précis."
|
||||
},
|
||||
"shoppingLists": "Listes de courses",
|
||||
"print": "Imprimer",
|
||||
"shareTitle": "Partager le planning de cette semaine",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@epicure/web",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "epicure",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter web dev",
|
||||
|
||||
Reference in New Issue
Block a user