feat: implement backend localization infrastructure
- 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>
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
type ingredientRepo interface {
|
||||
FuzzyMatch(ctx context.Context, name string) (*ingredient.IngredientMapping, error)
|
||||
Upsert(ctx context.Context, m *ingredient.IngredientMapping) (*ingredient.IngredientMapping, error)
|
||||
UpsertTranslation(ctx context.Context, id, lang, name string, aliases json.RawMessage) error
|
||||
}
|
||||
|
||||
// Handler handles POST /ai/* recognition endpoints.
|
||||
@@ -218,7 +219,6 @@ func (h *Handler) saveClassification(ctx context.Context, c *gemini.IngredientCl
|
||||
|
||||
m := &ingredient.IngredientMapping{
|
||||
CanonicalName: c.CanonicalName,
|
||||
CanonicalNameRu: &c.CanonicalNameRu,
|
||||
Category: strPtr(c.Category),
|
||||
DefaultUnit: strPtr(c.DefaultUnit),
|
||||
CaloriesPer100g: c.CaloriesPer100g,
|
||||
@@ -234,6 +234,13 @@ func (h *Handler) saveClassification(ctx context.Context, c *gemini.IngredientCl
|
||||
slog.Warn("upsert classified ingredient", "name", c.CanonicalName, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Persist the Russian translation when Gemini provided one.
|
||||
if c.CanonicalNameRu != "" {
|
||||
if err := h.ingredientRepo.UpsertTranslation(ctx, saved.ID, "ru", c.CanonicalNameRu, json.RawMessage("[]")); err != nil {
|
||||
slog.Warn("upsert ingredient translation", "id", saved.ID, "err", err)
|
||||
}
|
||||
}
|
||||
return saved
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user