- 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>
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
package gemini
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync"
|
||
)
|
||
|
||
// MenuRequest contains parameters for weekly menu generation.
|
||
type MenuRequest struct {
|
||
UserGoal string
|
||
DailyCalories int
|
||
Restrictions []string
|
||
CuisinePrefs []string
|
||
AvailableProducts []string
|
||
Lang string // ISO 639-1 target language code, e.g. "en", "ru"
|
||
}
|
||
|
||
// DayPlan is the AI-generated plan for a single day.
|
||
type DayPlan struct {
|
||
Day int `json:"day"`
|
||
Meals []MealEntry `json:"meals"`
|
||
}
|
||
|
||
// MealEntry is a single meal within a day plan.
|
||
type MealEntry struct {
|
||
MealType string `json:"meal_type"` // breakfast | lunch | dinner
|
||
Recipe Recipe `json:"recipe"`
|
||
}
|
||
|
||
// GenerateMenu produces a 7-day × 3-meal plan by issuing three parallel
|
||
// GenerateRecipes calls (one per meal type). This avoids token-limit errors
|
||
// that arise from requesting 21 full recipes in a single prompt.
|
||
func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan, error) {
|
||
type mealSlot struct {
|
||
mealType string
|
||
fraction float64 // share of daily calories
|
||
}
|
||
|
||
slots := []mealSlot{
|
||
{"breakfast", 0.25},
|
||
{"lunch", 0.40},
|
||
{"dinner", 0.35},
|
||
}
|
||
|
||
type mealResult struct {
|
||
recipes []Recipe
|
||
err error
|
||
}
|
||
|
||
results := make([]mealResult, len(slots))
|
||
var wg sync.WaitGroup
|
||
|
||
for i, slot := range slots {
|
||
wg.Add(1)
|
||
go func(idx int, mealType string, fraction float64) {
|
||
defer wg.Done()
|
||
// Scale daily calories to what this meal should contribute.
|
||
mealCal := int(float64(req.DailyCalories) * fraction)
|
||
r, err := c.GenerateRecipes(ctx, RecipeRequest{
|
||
UserGoal: req.UserGoal,
|
||
DailyCalories: mealCal * 3, // prompt divides by 3 internally
|
||
Restrictions: req.Restrictions,
|
||
CuisinePrefs: req.CuisinePrefs,
|
||
Count: 7,
|
||
AvailableProducts: req.AvailableProducts,
|
||
Lang: req.Lang,
|
||
})
|
||
results[idx] = mealResult{r, err}
|
||
}(i, slot.mealType, slot.fraction)
|
||
}
|
||
wg.Wait()
|
||
|
||
for i, res := range results {
|
||
if res.err != nil {
|
||
return nil, fmt.Errorf("generate %s: %w", slots[i].mealType, res.err)
|
||
}
|
||
if len(res.recipes) == 0 {
|
||
return nil, fmt.Errorf("no %s recipes returned", slots[i].mealType)
|
||
}
|
||
// Pad to exactly 7 by repeating the last recipe.
|
||
for len(results[i].recipes) < 7 {
|
||
results[i].recipes = append(results[i].recipes, results[i].recipes[len(results[i].recipes)-1])
|
||
}
|
||
}
|
||
|
||
days := make([]DayPlan, 7)
|
||
for day := range 7 {
|
||
days[day] = DayPlan{
|
||
Day: day + 1,
|
||
Meals: []MealEntry{
|
||
{MealType: slots[0].mealType, Recipe: results[0].recipes[day]},
|
||
{MealType: slots[1].mealType, Recipe: results[1].recipes[day]},
|
||
{MealType: slots[2].mealType, Recipe: results[2].recipes[day]},
|
||
},
|
||
}
|
||
}
|
||
return days, nil
|
||
}
|