diff --git a/CHANGELOG.md b/CHANGELOG.md index 6569c1e..56943d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.78.0 — 2026-07-24 13:40 + +### Fixed +- Marketing/vitrine pages now honor a first-time visitor's browser language (Accept-Language) instead of always defaulting to English until they manually switch. An explicit choice from the language switcher still wins on every later visit. + ## 0.77.0 — 2026-07-24 13:00 ### Added diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md index aa979b4..6cbfcc6 100644 --- a/FEATURE_AUDIT.md +++ b/FEATURE_AUDIT.md @@ -12,6 +12,8 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real **Model Prefs gating (closed 2026-07-24):** `Settings → AI`'s Model Prefs section previously had zero gate — every user could pick a raw provider + model ID with no fencing. Now behind `isByokEnabled` (same gate as BYOK itself, since picking a specific provider/model only makes sense once you have your own key to route it to) — hidden entirely for non-BYOK users, not just locked-and-teased. `GET`/`PUT /api/v1/users/me/model-prefs` gated server-side via `requireByok()` too. +**Marketing/vitrine locale (closed 2026-07-24):** `getMarketingLocale()` (`apps/web/lib/marketing-locale.ts`) previously hard-defaulted anonymous visitors to English until they clicked the language switcher, even though the switcher's `marketing_locale` cookie mechanism and a fully French-translated marketing site both already existed. Now falls back to parsing the `Accept-Language` header (first supported tag in the browser's stated preference order) when no cookie is set; an explicit switcher choice still wins on every later visit, and logged-in visitors still get their saved `users.locale` first, unchanged. + | Feature | Status | Description | Key files | |---|---|---|---| | Recipe CRUD | Exists | Create/get/list/update/delete; update snapshots the prior version first (`recipeSnapshots`) | `apps/web/app/api/v1/recipes/**` | diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 79a6cea..d0e57a6 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.77.0"; +export const APP_VERSION = "0.78.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.78.0", + date: "2026-07-24 13:40", + fixed: [ + "Marketing/vitrine pages now honor a first-time visitor's browser language (Accept-Language) instead of always defaulting to English until they manually switch. An explicit choice from the language switcher still wins on every later visit.", + ], + }, { version: "0.77.0", date: "2026-07-24 13:00", diff --git a/apps/web/lib/marketing-locale.ts b/apps/web/lib/marketing-locale.ts index cda4ab7..f394da0 100644 --- a/apps/web/lib/marketing-locale.ts +++ b/apps/web/lib/marketing-locale.ts @@ -12,10 +12,31 @@ export { MARKETING_LOCALE_COOKIE }; * which persists to `users.locale` via an authenticated PATCH that would * just 401 for an anonymous visitor and revert the UI. */ +/** Parses an `Accept-Language` header and returns the first supported + * locale it lists, in the browser's stated preference order — ignoring + * unsupported languages (e.g. "de", "es") rather than stopping at them. */ +function localeFromAcceptLanguage(header: string | null): Locale | null { + if (!header) return null; + const tags = header + .split(",") + .map((part) => part.split(";")[0]!.trim().toLowerCase()) + .filter(Boolean); + for (const tag of tags) { + if (tag.startsWith("fr")) return "fr"; + if (tag.startsWith("en")) return "en"; + } + return null; +} + export async function getMarketingLocale(): Promise { const store = await cookies(); - const value = store.get(MARKETING_LOCALE_COOKIE)?.value; - return value === "fr" ? "fr" : "en"; + const cookieValue = store.get(MARKETING_LOCALE_COOKIE)?.value; + if (cookieValue === "fr" || cookieValue === "en") return cookieValue; + + // No explicit choice saved yet (first visit, or cookies cleared) — honor + // the visitor's browser language instead of always defaulting to English. + const headerStore = await headers(); + return localeFromAcceptLanguage(headerStore.get("accept-language")) ?? "en"; } /** A logged-in visitor's own saved locale wins (matches what they see diff --git a/apps/web/package.json b/apps/web/package.json index eb65629..df27476 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.77.0", + "version": "0.78.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index bdcc83d..1f5bfef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.77.0", + "version": "0.78.0", "private": true, "scripts": { "dev": "pnpm --filter web dev",