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:
Arnaud
2026-07-14 13:20:38 +02:00
parent 8ef97da2c2
commit 1237330a0c
10 changed files with 62 additions and 24 deletions
+5
View File
@@ -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
+4 -4
View File
@@ -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>
+8 -1
View File
@@ -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",
+11
View File
@@ -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",
+14
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
"version": "0.27.1",
"version": "0.27.2",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "epicure",
"version": "0.27.1",
"version": "0.27.2",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",