- Add internal/adapters/ai/types.go with neutral shared types
(Recipe, DayPlan, RecognizedItem, IngredientClassification, etc.)
- Remove types from internal/adapters/openai/ — adapter now uses ai.*
- Define Recognizer interface in recognition package
- Define MenuGenerator interface in menu package
- Define RecipeGenerator interface in recommendation package
- Handler structs now hold interfaces, not *openai.Client
- Add wire.Bind entries for the three new interface bindings
To swap OpenAI for another provider: implement the three interfaces
using ai.* types and change the wire.Bind lines in cmd/server/wire.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add internal/locale package: Parse(Accept-Language), FromContext/WithLang helpers, 12 supported languages
- Add Language middleware that reads Accept-Language header and stores lang in context
- Register Language middleware globally in server router (after CORS)
Database migrations:
- 009: create recipe_translations, saved_recipe_translations, ingredient_translations tables; migrate existing _ru data
- 010: drop legacy _ru columns (title_ru, description_ru, canonical_name_ru); update FTS index
Models: remove all _ru fields (TitleRu, DescriptionRu, NameRu, UnitRu, CanonicalNameRu)
Repositories:
- recipe: Upsert drops _ru params; GetByID does LEFT JOIN COALESCE on recipe_translations; ListMissingTranslation(lang); UpsertTranslation
- ingredient: same pattern with ingredient_translations; Search now queries translated names/aliases
- savedrecipe: List/GetByID LEFT JOIN COALESCE on saved_recipe_translations; UpsertTranslation
Gemini:
- RecipeRequest/MenuRequest gain Lang field
- buildRecipePrompt rewritten in English with target-language content instruction; image_query always in English
- GenerateMenu propagates Lang to GenerateRecipes
Handlers:
- recommendation/menu: pass locale.FromContext(ctx) as Lang
- recognition: saveClassification stores Russian translation via UpsertTranslation instead of _ru column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pgx v5 cannot scan a PostgreSQL DATE column directly into a Go string.
The WHERE clause already used week_start::text but the SELECT did not,
causing GetByWeek to return a scan error that the GenerateMenu handler
swallowed without logging (just returned 500).
- repository.go: SELECT mp.week_start::text instead of mp.week_start
- handler.go: add slog.Error before the silent 500 branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>