-- +goose Up -- --------------------------------------------------------------------------- -- recipe_translations -- Stores per-language overrides for the catalog recipe fields that contain -- human-readable text (title, description, ingredients list, step descriptions). -- The base `recipes` row always holds the English (canonical) content. -- --------------------------------------------------------------------------- CREATE TABLE recipe_translations ( recipe_id UUID NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, lang VARCHAR(10) NOT NULL, title VARCHAR(500), description TEXT, ingredients JSONB, steps JSONB, PRIMARY KEY (recipe_id, lang) ); CREATE INDEX idx_recipe_translations_recipe_id ON recipe_translations (recipe_id); -- --------------------------------------------------------------------------- -- saved_recipe_translations -- Stores per-language translations for user-saved (AI-generated) recipes. -- The base `saved_recipes` row always holds the English canonical content. -- Translations are generated on demand by the AI layer and recorded here. -- --------------------------------------------------------------------------- CREATE TABLE saved_recipe_translations ( saved_recipe_id UUID NOT NULL REFERENCES saved_recipes(id) ON DELETE CASCADE, lang VARCHAR(10) NOT NULL, title VARCHAR(500), description TEXT, ingredients JSONB, steps JSONB, generated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (saved_recipe_id, lang) ); CREATE INDEX idx_saved_recipe_translations_recipe_id ON saved_recipe_translations (saved_recipe_id); -- --------------------------------------------------------------------------- -- ingredient_translations -- Stores per-language names (and optional aliases) for ingredient mappings. -- The base `ingredient_mappings` row holds the English canonical name. -- --------------------------------------------------------------------------- CREATE TABLE ingredient_translations ( ingredient_id UUID NOT NULL REFERENCES ingredient_mappings(id) ON DELETE CASCADE, lang VARCHAR(10) NOT NULL, name VARCHAR(255) NOT NULL, aliases JSONB NOT NULL DEFAULT '[]'::jsonb, PRIMARY KEY (ingredient_id, lang) ); CREATE INDEX idx_ingredient_translations_ingredient_id ON ingredient_translations (ingredient_id); -- --------------------------------------------------------------------------- -- Migrate existing Russian data from _ru columns into the translation tables. -- --------------------------------------------------------------------------- -- Recipe translations: title_ru / description_ru at the row level, plus the -- embedded name_ru / unit_ru fields inside the ingredients JSONB array, and -- description_ru inside the steps JSONB array. INSERT INTO recipe_translations (recipe_id, lang, title, description, ingredients, steps) SELECT id, 'ru', title_ru, description_ru, -- Rebuild ingredients array with Russian name/unit substituted in. CASE WHEN jsonb_array_length(ingredients) > 0 THEN ( SELECT COALESCE( jsonb_agg( jsonb_build_object( 'spoonacular_id', elem->>'spoonacular_id', 'mapping_id', elem->>'mapping_id', 'name', COALESCE(NULLIF(elem->>'name_ru', ''), elem->>'name'), 'amount', (elem->>'amount')::numeric, 'unit', COALESCE(NULLIF(elem->>'unit_ru', ''), elem->>'unit'), 'optional', (elem->>'optional')::boolean ) ), '[]'::jsonb ) FROM jsonb_array_elements(ingredients) AS elem ) ELSE NULL END, -- Rebuild steps array with Russian description substituted in. CASE WHEN jsonb_array_length(steps) > 0 THEN ( SELECT COALESCE( jsonb_agg( jsonb_build_object( 'number', (elem->>'number')::int, 'description', COALESCE(NULLIF(elem->>'description_ru', ''), elem->>'description'), 'timer_seconds', elem->'timer_seconds', 'image_url', elem->>'image_url' ) ), '[]'::jsonb ) FROM jsonb_array_elements(steps) AS elem ) ELSE NULL END FROM recipes WHERE title_ru IS NOT NULL; -- Ingredient translations: canonical_name_ru. INSERT INTO ingredient_translations (ingredient_id, lang, name) SELECT id, 'ru', canonical_name_ru FROM ingredient_mappings WHERE canonical_name_ru IS NOT NULL; -- +goose Down DROP TABLE IF EXISTS ingredient_translations; DROP TABLE IF EXISTS saved_recipe_translations; DROP TABLE IF EXISTS recipe_translations;