diff --git a/apps/web/app/(app)/collections/[id]/page.tsx b/apps/web/app/(app)/collections/[id]/page.tsx index 506f422..a9c0845 100644 --- a/apps/web/app/(app)/collections/[id]/page.tsx +++ b/apps/web/app/(app)/collections/[id]/page.tsx @@ -10,6 +10,7 @@ import { ForkCollectionButton } from "@/components/collections/fork-collection-b import { ShareCollectionButton } from "@/components/collections/share-collection-button"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { getMessages } from "@/lib/i18n/server"; type Params = { params: Promise<{ id: string }> }; @@ -19,6 +20,7 @@ export default async function CollectionPage({ 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 col = await db.query.collections.findFirst({ where: and( @@ -46,7 +48,7 @@ export default async function CollectionPage({ params }: Params) { {col.recipes.length > 0 && ( - Export as PDF + {m.collections.exportPdf} )} {isOwner && } diff --git a/apps/web/app/(app)/meal-plan/page.tsx b/apps/web/app/(app)/meal-plan/page.tsx index 7090923..90ff5cd 100644 --- a/apps/web/app/(app)/meal-plan/page.tsx +++ b/apps/web/app/(app)/meal-plan/page.tsx @@ -9,6 +9,7 @@ import { MealPlanner } from "@/components/meal-plan/meal-planner"; import { ShareMealPlanButton } from "@/components/meal-plan/share-meal-plan-button"; import { WeeklyNutritionBar } from "@/components/nutrition/weekly-nutrition-bar"; import { cn } from "@/lib/utils"; +import { getMessages, formatMessage } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "Meal Plan" }; @@ -39,6 +40,7 @@ export default async function MealPlanPage({ const { week } = await searchParams; const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; + const msgs = getMessages((session.user as { locale?: string }).locale); const monday = getMonday(week); const weekStart = toDateStr(monday); @@ -83,18 +85,18 @@ export default async function MealPlanPage({
-

Meal Plan

+

{msgs.mealPlan.title}

{label}

- Shopping lists + {msgs.mealPlan.shoppingLists} - Print + {msgs.common.print} @@ -110,17 +112,19 @@ export default async function MealPlanPage({ {sharedMemberships.length > 0 && (
-

Shared with you

+

{msgs.mealPlan.sharedWithYou}

- {sharedMemberships.map((m) => ( + {sharedMemberships.map((membership) => (
-

{`${m.mealPlan.user?.name ?? "Unknown"}'s plan`}

-

Week of {m.mealPlan.weekStart} · {m.role}

+

{formatMessage(msgs.mealPlan.sharedPlanOf, { name: membership.mealPlan.user?.name ?? "?" })}

+

+ {formatMessage(msgs.mealPlan.weekOf, { date: membership.mealPlan.weekStart })} · {msgs.shareDialog[membership.role]} +

))} diff --git a/apps/web/app/(app)/meal-plan/shared/[mealPlanId]/page.tsx b/apps/web/app/(app)/meal-plan/shared/[mealPlanId]/page.tsx index 85bc513..01ba5aa 100644 --- a/apps/web/app/(app)/meal-plan/shared/[mealPlanId]/page.tsx +++ b/apps/web/app/(app)/meal-plan/shared/[mealPlanId]/page.tsx @@ -5,6 +5,7 @@ import { auth } from "@/lib/auth/server"; import { db, mealPlans, recipes, eq, desc } from "@epicure/db"; import { getMealPlanAccessById, canWriteMealPlan } from "@/lib/meal-plan-access"; import { SharedMealPlanView } from "@/components/meal-plan/shared-meal-plan-view"; +import { getMessages, formatMessage } from "@/lib/i18n/server"; type Params = { params: Promise<{ mealPlanId: string }> }; @@ -14,6 +15,7 @@ export default async function SharedMealPlanPage({ params }: Params) { const { mealPlanId } = 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 access = await getMealPlanAccessById(mealPlanId, session.user.id); if (!access) notFound(); @@ -35,8 +37,8 @@ export default async function SharedMealPlanPage({ params }: Params) { return (
-

{`${plan.user?.name ?? "Shared"}'s Meal Plan`}

-

Week of {plan.weekStart} · {access.role}

+

{formatMessage(m.mealPlan.sharedMealPlanTitle, { name: plan.user?.name ?? "?" })}

+

{formatMessage(m.mealPlan.weekOf, { date: plan.weekStart })} · {m.shareDialog[access.role]}

} /> - View publicly + {m.recipe.viewPublicly} )} {recipe.ingredients.length > 0 && ( @@ -148,6 +149,7 @@ export default async function RecipePage({ params }: Params) { order: s.order, }))} /> + } /> - Edit + {m.recipe.edit}
diff --git a/apps/web/app/(app)/settings/ai/page.tsx b/apps/web/app/(app)/settings/ai/page.tsx index 3408a44..129e1e5 100644 --- a/apps/web/app/(app)/settings/ai/page.tsx +++ b/apps/web/app/(app)/settings/ai/page.tsx @@ -4,12 +4,14 @@ import { auth } from "@/lib/auth/server"; import { db, userAiKeys, userModelPrefs, eq } from "@epicure/db"; import { ByokManager } from "@/components/settings/byok-manager"; import { ModelPrefsForm } from "@/components/settings/model-prefs-form"; +import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "AI & Models – Settings" }; export default async function AiSettingsPage() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; + const m = getMessages((session.user as { locale?: string }).locale); const [aiKeys, modelPrefs] = await Promise.all([ db.query.userAiKeys.findMany({ @@ -25,9 +27,9 @@ export default async function AiSettingsPage() {
-

Your API Keys (BYOK)

+

{m.settings.byok.title}

- Use your own API keys instead of the app's shared quota. Keys are encrypted at rest. + {m.settings.byok.description}

k.provider)} /> @@ -35,9 +37,9 @@ export default async function AiSettingsPage() {
-

Model Preferences

+

{m.settings.modelPrefs.title}

- Choose which model to use for each task. Defaults to your first configured provider. + {m.settings.modelPrefs.description}

diff --git a/apps/web/app/(app)/settings/api-keys/page.tsx b/apps/web/app/(app)/settings/api-keys/page.tsx index 9ada60d..2088462 100644 --- a/apps/web/app/(app)/settings/api-keys/page.tsx +++ b/apps/web/app/(app)/settings/api-keys/page.tsx @@ -3,12 +3,14 @@ import { headers } from "next/headers"; import { auth } from "@/lib/auth/server"; import { db, apiKeys, eq } from "@epicure/db"; import { ApiKeysManager } from "@/components/settings/api-keys-manager"; +import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "API Keys" }; export default async function ApiKeysPage() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; + const m = getMessages((session.user as { locale?: string }).locale); const keys = await db .select({ @@ -24,9 +26,9 @@ export default async function ApiKeysPage() {
-

API Keys

+

{m.settings.apiKeysPage.title}

- Manage API keys for programmatic access to the Epicure API. + {m.settings.apiKeysPage.description}

-

Push Notifications

+

{m.settings.pushNotifications.title}

- Get notified when someone comments on your recipes or likes your content. + {m.settings.pushNotifications.description}

diff --git a/apps/web/app/(app)/settings/nutrition/page.tsx b/apps/web/app/(app)/settings/nutrition/page.tsx index 944eef8..c194700 100644 --- a/apps/web/app/(app)/settings/nutrition/page.tsx +++ b/apps/web/app/(app)/settings/nutrition/page.tsx @@ -3,12 +3,14 @@ import { headers } from "next/headers"; import { auth } from "@/lib/auth/server"; import { db, userNutritionGoals, eq } from "@epicure/db"; import { NutritionGoalsForm } from "@/components/nutrition/nutrition-goals-form"; +import { getMessages } from "@/lib/i18n/server"; export const metadata: Metadata = { title: "Nutrition – Settings" }; export default async function NutritionPage() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; + const m = getMessages((session.user as { locale?: string }).locale); const goals = await db.query.userNutritionGoals.findFirst({ where: eq(userNutritionGoals.userId, session.user.id), @@ -18,9 +20,9 @@ export default async function NutritionPage() {
-

Daily Nutrition Goals

+

{m.settings.nutritionGoals.title}

- Set your daily targets. These are shown as progress bars on your meal plan. + {m.settings.nutritionGoals.description}

-

Webhooks

+

{m.settings.webhooksPage.title}

- Receive HTTP callbacks when events happen in your Epicure account. + {m.settings.webhooksPage.description}

}; @@ -20,6 +21,7 @@ export default async function ShoppingListPage({ 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 access = await getShoppingListAccess(id, session.user.id); if (!access) notFound(); @@ -39,7 +41,8 @@ export default async function ShoppingListPage({ params }: Params) {

{list.name}

- {list.items.length} items{list.generatedAt ? " · Generated from meal plan" : ""} + {formatMessage(list.items.length === 1 ? m.shoppingLists.itemCount : m.shoppingLists.itemCountPlural, { count: list.items.length })} + {list.generatedAt ? m.shoppingLists.fromMealPlan : ""}

@@ -47,7 +50,7 @@ export default async function ShoppingListPage({ params }: Params) { {access.role === "owner" && } - Print + {m.common.print}
diff --git a/apps/web/components/collections/fork-collection-button.tsx b/apps/web/components/collections/fork-collection-button.tsx index 630a039..be22f9e 100644 --- a/apps/web/components/collections/fork-collection-button.tsx +++ b/apps/web/components/collections/fork-collection-button.tsx @@ -1,12 +1,15 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { GitFork } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; export function ForkCollectionButton({ collectionId }: { collectionId: string }) { + const t = useTranslations("collections"); + const tSocial = useTranslations("social"); const [forking, setForking] = useState(false); const router = useRouter(); @@ -16,13 +19,13 @@ export function ForkCollectionButton({ collectionId }: { collectionId: string }) const res = await fetch(`/api/v1/collections/${collectionId}/fork`, { method: "POST" }); if (!res.ok) { const data = await res.json() as { error?: string }; - throw new Error(data.error ?? "Fork failed"); + throw new Error(data.error ?? t("forkFailed")); } const { id } = await res.json() as { id: string }; - toast.success("Collection forked to your library"); + toast.success(tSocial("collectionForked")); router.push(`/collections/${id}`); } catch (err) { - toast.error(err instanceof Error ? err.message : "Fork failed"); + toast.error(err instanceof Error ? err.message : t("forkFailed")); setForking(false); } } @@ -30,7 +33,7 @@ export function ForkCollectionButton({ collectionId }: { collectionId: string }) return ( ); } diff --git a/apps/web/components/collections/share-collection-button.tsx b/apps/web/components/collections/share-collection-button.tsx index 73132b3..61f5acf 100644 --- a/apps/web/components/collections/share-collection-button.tsx +++ b/apps/web/components/collections/share-collection-button.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { UserPlus, X } from "lucide-react"; import { toast } from "sonner"; import { @@ -40,6 +41,9 @@ interface Props { } export function ShareCollectionButton({ collectionId }: Props) { + const t = useTranslations("collections"); + const ts = useTranslations("shareDialog"); + const tCommon = useTranslations("common"); const [open, setOpen] = useState(false); const [email, setEmail] = useState(""); const [role, setRole] = useState("viewer"); @@ -55,7 +59,7 @@ export function ShareCollectionButton({ collectionId }: Props) { const data = await res.json() as Member[]; setMembers(data); } catch { - toast.error("Could not load members"); + toast.error(ts("loadMembersFailed")); } finally { setLoading(false); } @@ -73,7 +77,7 @@ export function ShareCollectionButton({ collectionId }: Props) { async function handleInvite() { if (!email.trim()) { - toast.error("Enter an email address"); + toast.error(ts("enterEmail")); return; } setInviting(true); @@ -83,14 +87,14 @@ export function ShareCollectionButton({ collectionId }: Props) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), role }), }); - if (res.status === 409) { toast.error("Already a member"); return; } - if (res.status === 404) { toast.error("User not found"); return; } - if (!res.ok) { toast.error("Could not invite user"); return; } - toast.success("Invitation sent"); + if (res.status === 409) { toast.error(ts("alreadyMember")); return; } + if (res.status === 404) { toast.error(ts("userNotFound")); return; } + if (!res.ok) { toast.error(ts("inviteFailed")); return; } + toast.success(ts("invitationSent")); setEmail(""); await fetchMembers(); } catch { - toast.error("Could not invite user"); + toast.error(ts("inviteFailed")); } finally { setInviting(false); } @@ -102,11 +106,11 @@ export function ShareCollectionButton({ collectionId }: Props) { `/api/v1/collections/${collectionId}/members?memberId=${memberId}`, { method: "DELETE" }, ); - if (!res.ok) { toast.error("Could not remove member"); return; } + if (!res.ok) { toast.error(ts("removeMemberFailed")); return; } setMembers((prev) => prev.filter((m) => m.id !== memberId)); - toast.success("Member removed"); + toast.success(ts("memberRemoved")); } catch { - toast.error("Could not remove member"); + toast.error(ts("removeMemberFailed")); } } @@ -114,15 +118,15 @@ export function ShareCollectionButton({ collectionId }: Props) { <> - Share collection + {t("shareTitle")} - Invite people to view or edit this collection. + {t("shareDescription")} @@ -130,7 +134,7 @@ export function ShareCollectionButton({ collectionId }: Props) {
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleInvite(); }} @@ -141,22 +145,22 @@ export function ShareCollectionButton({ collectionId }: Props) { - Viewer - Editor + {ts("viewer")} + {ts("editor")}
{/* Members list */}
{loading && ( -

Loading members…

+

{ts("loadingMembers")}

)} {!loading && members.length === 0 && ( -

No members yet.

+

{ts("noMembers")}

)} {members.map((m) => (
- {m.role} + {ts(m.role)} - New shopping list + {t("newListTitle")}
- - setName(e.target.value)} placeholder="Weekly groceries" /> + + setName(e.target.value)} placeholder={t("listNamePlaceholder")} />
- + setWeekStart(e.target.value)} /> -

Picks Monday of the selected week

+

{t("generateFromWeekHint")}

- - + +
diff --git a/apps/web/components/meal-plan/share-meal-plan-button.tsx b/apps/web/components/meal-plan/share-meal-plan-button.tsx index 4ef32dc..e52ab76 100644 --- a/apps/web/components/meal-plan/share-meal-plan-button.tsx +++ b/apps/web/components/meal-plan/share-meal-plan-button.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { UserPlus, X } from "lucide-react"; import { toast } from "sonner"; import { @@ -40,6 +41,9 @@ interface Props { } export function ShareMealPlanButton({ weekStart }: Props) { + const t = useTranslations("mealPlan"); + const ts = useTranslations("shareDialog"); + const tCommon = useTranslations("common"); const [open, setOpen] = useState(false); const [email, setEmail] = useState(""); const [role, setRole] = useState("viewer"); @@ -55,7 +59,7 @@ export function ShareMealPlanButton({ weekStart }: Props) { const data = await res.json() as Member[]; setMembers(data); } catch { - toast.error("Could not load members"); + toast.error(ts("loadMembersFailed")); } finally { setLoading(false); } @@ -73,7 +77,7 @@ export function ShareMealPlanButton({ weekStart }: Props) { async function handleInvite() { if (!email.trim()) { - toast.error("Enter an email address"); + toast.error(ts("enterEmail")); return; } setInviting(true); @@ -83,14 +87,14 @@ export function ShareMealPlanButton({ weekStart }: Props) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), role }), }); - if (res.status === 409) { toast.error("Already a member"); return; } - if (res.status === 404) { toast.error("User not found"); return; } - if (!res.ok) { toast.error("Could not invite user"); return; } - toast.success("Invitation sent"); + if (res.status === 409) { toast.error(ts("alreadyMember")); return; } + if (res.status === 404) { toast.error(ts("userNotFound")); return; } + if (!res.ok) { toast.error(ts("inviteFailed")); return; } + toast.success(ts("invitationSent")); setEmail(""); await fetchMembers(); } catch { - toast.error("Could not invite user"); + toast.error(ts("inviteFailed")); } finally { setInviting(false); } @@ -102,11 +106,11 @@ export function ShareMealPlanButton({ weekStart }: Props) { `/api/v1/meal-plans/${weekStart}/members?memberId=${memberId}`, { method: "DELETE" }, ); - if (!res.ok) { toast.error("Could not remove member"); return; } + if (!res.ok) { toast.error(ts("removeMemberFailed")); return; } setMembers((prev) => prev.filter((m) => m.id !== memberId)); - toast.success("Member removed"); + toast.success(ts("memberRemoved")); } catch { - toast.error("Could not remove member"); + toast.error(ts("removeMemberFailed")); } } @@ -114,22 +118,22 @@ export function ShareMealPlanButton({ weekStart }: Props) { <> - Share this week's plan + {t("shareTitle")} - Invite household members to view or edit this week's meal plan. + {t("shareDescription")}
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleInvite(); }} @@ -140,21 +144,21 @@ export function ShareMealPlanButton({ weekStart }: Props) { - Viewer - Editor + {ts("viewer")} + {ts("editor")}
{loading && ( -

Loading members…

+

{ts("loadingMembers")}

)} {!loading && members.length === 0 && ( -

No members yet.

+

{ts("noMembers")}

)} {members.map((m) => (
- {m.role} + {ts(m.role)}