feat(i18n): translate hardcoded strings across print, public recipe, and recipe management UI
Wires dozens of components and server pages to next-intl instead of hardcoded English text, and adds a lightweight getMessages/formatMessage helper for server components (print pages, public recipe page, cook metadata) since next-intl/server isn't wired into this app's routing. Backfills missing en/fr message keys that existing code already referenced but fr.json lacked.
This commit is contained in:
@@ -3,13 +3,16 @@ import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth/server";
|
||||
import { db, recipes, eq, and } from "@epicure/db";
|
||||
import { CookingMode } from "@/components/cooking-mode/cooking-mode";
|
||||
import { getMessages, formatMessage } from "@/lib/i18n/server";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Params) {
|
||||
const { id } = await params;
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
const m = getMessages((session?.user as { locale?: string } | undefined)?.locale);
|
||||
const recipe = await db.query.recipes.findFirst({ where: eq(recipes.id, id) });
|
||||
return { title: recipe ? `Cooking: ${recipe.title}` : "Cooking Mode" };
|
||||
return { title: recipe ? formatMessage(m.cookingMode.pageTitle, { title: recipe.title }) : m.cookingMode.pageTitleFallback };
|
||||
}
|
||||
|
||||
export default async function CookPage({ params }: Params) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { CommentsSection } from "@/components/social/comments-section";
|
||||
import { getPublicUrl } from "@/lib/storage";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RecipeChatPanel } from "@/components/recipe/recipe-chat-panel";
|
||||
import { getMessages } from "@/lib/i18n/server";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -37,25 +38,18 @@ export async function generateMetadata({ params }: Params): Promise<Metadata> {
|
||||
return { title: recipe?.title ?? "Recipe" };
|
||||
}
|
||||
|
||||
const VISIBILITY_LABEL = { private: "Private", unlisted: "Unlisted", public: "Public" };
|
||||
const VISIBILITY_ICON = { private: Lock, unlisted: Link2, public: Globe };
|
||||
const DIFFICULTY_COLOR = { easy: "default", medium: "secondary", hard: "destructive" } as const;
|
||||
|
||||
const DIETARY_LABELS: Record<string, string> = {
|
||||
vegan: "Vegan",
|
||||
vegetarian: "Vegetarian",
|
||||
glutenFree: "Gluten-free",
|
||||
dairyFree: "Dairy-free",
|
||||
nutFree: "Nut-free",
|
||||
halal: "Halal",
|
||||
kosher: "Kosher",
|
||||
};
|
||||
|
||||
export default async function RecipePage({ params }: Params) {
|
||||
const { id } = await params;
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) return null;
|
||||
|
||||
const m = getMessages((session.user as { locale?: string }).locale);
|
||||
const VISIBILITY_LABEL = m.recipe.visibility;
|
||||
const DIETARY_LABELS = m.recipe.dietary;
|
||||
|
||||
const [recipe, ratingData, favoriteData] = await Promise.all([
|
||||
db.query.recipes.findFirst({
|
||||
where: and(eq(recipes.id, id), eq(recipes.authorId, session.user.id)),
|
||||
@@ -79,7 +73,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
|
||||
const activeDietaryTags = Object.entries(recipe.dietaryTags ?? {})
|
||||
.filter(([, v]) => v)
|
||||
.map(([k]) => DIETARY_LABELS[k])
|
||||
.map(([k]) => DIETARY_LABELS[k as keyof typeof DIETARY_LABELS])
|
||||
.filter(Boolean);
|
||||
|
||||
const VisibilityIcon = VISIBILITY_ICON[recipe.visibility];
|
||||
@@ -98,7 +92,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
<Play className="h-4 w-4" />
|
||||
</Link>
|
||||
} />
|
||||
<TooltipContent>Cook</TooltipContent>
|
||||
<TooltipContent>{m.recipe.cookAction}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<FavoriteButton recipeId={id} initialFavorited={isFavorited} />
|
||||
@@ -251,7 +245,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
{/* Serving scaler + ingredients */}
|
||||
{recipe.ingredients.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Ingredients</h2>
|
||||
<h2 className="text-xl font-semibold">{m.recipe.ingredients}</h2>
|
||||
<ServingScaler
|
||||
baseServings={recipe.baseServings}
|
||||
recipeTitle={recipe.title}
|
||||
@@ -272,7 +266,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold">Instructions</h2>
|
||||
<h2 className="text-xl font-semibold">{m.recipe.instructions}</h2>
|
||||
<ol className="space-y-6">
|
||||
{recipe.steps.map((step, i) => (
|
||||
<li key={step.id} className="flex gap-4">
|
||||
|
||||
Reference in New Issue
Block a user