package openai import ( "context" "encoding/json" "fmt" "strings" "github.com/food-ai/backend/internal/adapters/ai" ) // recipeTranslationPayload is the compact shape sent to and received from the // translation prompt. Only human-readable text fields are included; numeric, // enum, and English-only fields (image_query) are omitted. type recipeTranslationPayload struct { Title string `json:"title"` Description string `json:"description"` Ingredients []ingredientTranslationEntry `json:"ingredients"` Steps []stepTranslationEntry `json:"steps"` Tags []string `json:"tags"` } type ingredientTranslationEntry struct { Name string `json:"name"` Unit string `json:"unit"` } type stepTranslationEntry struct { Description string `json:"description"` } // TranslateRecipes translates the human-readable text fields of the given // recipes from English into targetLang. Non-text fields (nutrition, timers, // image_query, difficulty, cuisine, amounts) are copied unchanged. // On error the caller should fall back to the original English recipes. func (c *Client) TranslateRecipes(requestContext context.Context, recipes []ai.Recipe, targetLang string) ([]ai.Recipe, error) { targetLangName := langNames[targetLang] if targetLangName == "" { return recipes, nil // unknown language — nothing to do } // Build compact payload with only the fields that need translation. payloads := make([]recipeTranslationPayload, len(recipes)) for index, recipe := range recipes { ingredients := make([]ingredientTranslationEntry, len(recipe.Ingredients)) for ingredientIndex, ingredient := range recipe.Ingredients { ingredients[ingredientIndex] = ingredientTranslationEntry{ Name: ingredient.Name, Unit: ingredient.Unit, } } steps := make([]stepTranslationEntry, len(recipe.Steps)) for stepIndex, step := range recipe.Steps { steps[stepIndex] = stepTranslationEntry{Description: step.Description} } payloads[index] = recipeTranslationPayload{ Title: recipe.Title, Description: recipe.Description, Ingredients: ingredients, Steps: steps, Tags: recipe.Tags, } } payloadJSON, marshalError := json.Marshal(payloads) if marshalError != nil { return nil, fmt.Errorf("marshal translation payload: %w", marshalError) } prompt := fmt.Sprintf( `Translate the following recipe text fields from English to %s. Return ONLY a valid JSON array in the exact same structure. Do not add, remove, or reorder elements. Do not translate the array structure itself — only the string values. %s`, targetLangName, string(payloadJSON), ) var lastErr error for attempt := range maxRetries { messages := []map[string]string{{"role": "user", "content": prompt}} if attempt > 0 { messages = append(messages, map[string]string{ "role": "user", "content": "Previous response was not valid JSON. Return ONLY a JSON array with no text before or after.", }) } responseText, generateError := c.generateContent(requestContext, messages) if generateError != nil { return nil, generateError } responseText = strings.TrimSpace(responseText) if strings.HasPrefix(responseText, "```") { responseText = strings.TrimPrefix(responseText, "```json") responseText = strings.TrimPrefix(responseText, "```") responseText = strings.TrimSuffix(responseText, "```") responseText = strings.TrimSpace(responseText) } var translated []recipeTranslationPayload if parseError := json.Unmarshal([]byte(responseText), &translated); parseError != nil { lastErr = fmt.Errorf("attempt %d parse translation JSON: %w", attempt+1, parseError) continue } if len(translated) != len(recipes) { lastErr = fmt.Errorf("attempt %d: expected %d translated recipes, got %d", attempt+1, len(recipes), len(translated)) continue } // Merge translated text fields back into copies of the original recipes. result := make([]ai.Recipe, len(recipes)) for recipeIndex, recipe := range recipes { result[recipeIndex] = recipe // copy all fields (nutrition, timers, image_query, etc.) translatedPayload := translated[recipeIndex] result[recipeIndex].Title = translatedPayload.Title result[recipeIndex].Description = translatedPayload.Description result[recipeIndex].Tags = translatedPayload.Tags for ingredientIndex := range result[recipeIndex].Ingredients { if ingredientIndex < len(translatedPayload.Ingredients) { result[recipeIndex].Ingredients[ingredientIndex].Name = translatedPayload.Ingredients[ingredientIndex].Name result[recipeIndex].Ingredients[ingredientIndex].Unit = translatedPayload.Ingredients[ingredientIndex].Unit } } for stepIndex := range result[recipeIndex].Steps { if stepIndex < len(translatedPayload.Steps) { result[recipeIndex].Steps[stepIndex].Description = translatedPayload.Steps[stepIndex].Description } } } return result, nil } return nil, fmt.Errorf("failed to parse valid translation JSON after %d attempts: %w", maxRetries, lastErr) }