2
0
Files
gitcaddy-server/services/pages/generate.go
logikonline 5788123e00 feat(pages): add multi-language support for landing pages
Implement internationalization system for landing pages:
- Database model for storing language-specific translations
- Language configuration with default and enabled languages
- Language switcher in navigation across all templates
- Translation management UI in settings
- Support for 15 languages including English, Spanish, German, French, Japanese, Chinese
- Auto-detection and manual language selection
- AI-powered translation generation capability
2026-03-07 13:09:46 -05:00

285 lines
9.3 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package pages
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/json"
pages_module "code.gitcaddy.com/server/v3/modules/pages"
)
// aiGeneratedConfig holds the AI-generated landing page content
type aiGeneratedConfig struct {
Brand struct {
Name string `json:"name"`
Tagline string `json:"tagline"`
} `json:"brand"`
Hero struct {
Headline string `json:"headline"`
Subheadline string `json:"subheadline"`
PrimaryCTA struct {
Label string `json:"label"`
URL string `json:"url"`
} `json:"primary_cta"`
SecondaryCTA struct {
Label string `json:"label"`
URL string `json:"url"`
} `json:"secondary_cta"`
} `json:"hero"`
Stats []pages_module.StatConfig `json:"stats"`
ValueProps []pages_module.ValuePropConfig `json:"value_props"`
Features []pages_module.FeatureConfig `json:"features"`
CTASection struct {
Headline string `json:"headline"`
Subheadline string `json:"subheadline"`
ButtonLabel string `json:"button_label"`
ButtonURL string `json:"button_url"`
} `json:"cta_section"`
SEO struct {
Title string `json:"title"`
Description string `json:"description"`
} `json:"seo"`
}
// GenerateLandingPageContent uses AI to auto-generate landing page content from repo metadata.
func GenerateLandingPageContent(ctx context.Context, repo *repo_model.Repository, readme string) (*pages_module.LandingConfig, error) {
if !ai.IsEnabled() {
return nil, errors.New("AI service is not enabled")
}
repoURL := repo.HTMLURL()
topics := strings.Join(repo.Topics, ", ")
client := ai.GetClient()
resp, err := client.ExecuteTask(ctx, &ai.ExecuteTaskRequest{
RepoID: repo.ID,
Task: "landing_page_generate",
Context: map[string]string{
"repo_name": repo.Name,
"repo_description": repo.Description,
"repo_url": repoURL,
"topics": topics,
"primary_language": getPrimaryLanguageName(repo),
"stars": strconv.Itoa(repo.NumStars),
"forks": strconv.Itoa(repo.NumForks),
"readme": truncateReadme(readme),
"instruction": `You are a landing page copywriter. Analyze this open-source repository and generate compelling landing page content.
Return valid JSON with this exact structure:
{
"brand": {"name": "Project Name", "tagline": "Short tagline"},
"hero": {
"headline": "Compelling headline (max 10 words)",
"subheadline": "Supporting text explaining the value (1-2 sentences)",
"primary_cta": {"label": "Get Started", "url": "` + repoURL + `"},
"secondary_cta": {"label": "View on GitHub", "url": "` + repoURL + `"}
},
"stats": [
{"value": "...", "label": "..."}
],
"value_props": [
{"title": "...", "description": "...", "icon": "one of: zap,shield,rocket,check,star,heart,lock,globe,clock,gear,code,terminal,package,database,cloud,cpu,graph,people,tools,light-bulb"}
],
"features": [
{"title": "...", "description": "...", "icon": "same icon options as value_props"}
],
"cta_section": {
"headline": "Ready to get started?",
"subheadline": "...",
"button_label": "Get Started Free",
"button_url": "` + repoURL + `"
},
"seo": {
"title": "SEO title (50-60 chars)",
"description": "Meta description (150-160 chars)"
}
}
Guidelines:
- Generate 3-4 stats based on actual repo data (stars, forks, etc.) or compelling project metrics
- Generate exactly 3 value propositions that highlight the project's key strengths
- Generate 3-6 features based on what the README describes
- Use action-oriented, benefit-focused copy
- Keep it professional but engaging
- Icon choices should match the content semantically`,
},
})
if err != nil {
return nil, fmt.Errorf("AI generation failed: %w", err)
}
if !resp.Success {
return nil, fmt.Errorf("AI generation error: %s", resp.Error)
}
var generated aiGeneratedConfig
if err := json.Unmarshal([]byte(resp.Result), &generated); err != nil {
return nil, fmt.Errorf("failed to parse AI response: %w", err)
}
// Build LandingConfig from AI response
config := pages_module.DefaultConfig()
config.Brand.Name = generated.Brand.Name
config.Brand.Tagline = generated.Brand.Tagline
config.Hero.Headline = generated.Hero.Headline
config.Hero.Subheadline = generated.Hero.Subheadline
config.Hero.PrimaryCTA = pages_module.CTAButton{
Label: generated.Hero.PrimaryCTA.Label,
URL: generated.Hero.PrimaryCTA.URL,
}
config.Hero.SecondaryCTA = pages_module.CTAButton{
Label: generated.Hero.SecondaryCTA.Label,
URL: generated.Hero.SecondaryCTA.URL,
}
if len(generated.Stats) > 0 {
config.Stats = generated.Stats
}
if len(generated.ValueProps) > 0 {
config.ValueProps = generated.ValueProps
}
if len(generated.Features) > 0 {
config.Features = generated.Features
}
config.CTASection = pages_module.CTASectionConfig{
Headline: generated.CTASection.Headline,
Subheadline: generated.CTASection.Subheadline,
Button: pages_module.CTAButton{
Label: generated.CTASection.ButtonLabel,
URL: generated.CTASection.ButtonURL,
},
}
if generated.SEO.Title != "" {
config.SEO.Title = generated.SEO.Title
}
if generated.SEO.Description != "" {
config.SEO.Description = generated.SEO.Description
}
return config, nil
}
// getPrimaryLanguageName returns the primary language name for the repo, or empty string
func getPrimaryLanguageName(repo *repo_model.Repository) string {
if repo.PrimaryLanguage != nil {
return repo.PrimaryLanguage.Language
}
return ""
}
// truncateReadme limits README content to avoid sending too much to the AI
func truncateReadme(readme string) string {
const maxLen = 4000
if len(readme) <= maxLen {
return readme
}
return readme[:maxLen] + "\n... (truncated)"
}
// TranslateLandingPageContent uses AI to translate landing page content to a target language.
// Returns a JSON overlay string that can be deep-merged onto the base config.
func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig, targetLang string) (string, error) {
if !ai.IsEnabled() {
return "", errors.New("AI service is not enabled")
}
langNames := pages_module.LanguageDisplayNames()
langName := langNames[targetLang]
if langName == "" {
langName = targetLang
}
// Build the source content to translate
sourceContent := buildTranslatableContent(config)
client := ai.GetClient()
resp, err := client.ExecuteTask(ctx, &ai.ExecuteTaskRequest{
RepoID: repo.ID,
Task: "landing_page_translate",
Context: map[string]string{
"target_language": langName,
"target_code": targetLang,
"source_content": sourceContent,
"instruction": `You are a professional translator. Translate the following landing page content to ` + langName + ` (` + targetLang + `).
Return valid JSON as a partial config overlay. Only include the fields that have translatable text content.
Do NOT translate URLs, brand names, or technical terms. Keep the same JSON structure.
Return this exact JSON structure (only include fields with actual translated text):
{
"hero": {
"headline": "translated headline",
"subheadline": "translated subheadline",
"primary_cta": {"label": "translated label"},
"secondary_cta": {"label": "translated label"}
},
"stats": [{"value": "keep original", "label": "translated label"}],
"value_props": [{"title": "translated", "description": "translated"}],
"features": [{"title": "translated", "description": "translated"}],
"cta_section": {
"headline": "translated",
"subheadline": "translated",
"button": {"label": "translated"}
},
"seo": {
"title": "translated",
"description": "translated"
}
}
Important:
- Maintain the exact same number of items in arrays (stats, value_props, features)
- Keep icon names, URLs, and image_urls unchanged
- Use natural, marketing-quality translations (not literal/robotic)
- Adapt idioms and expressions for the target culture`,
},
})
if err != nil {
return "", fmt.Errorf("AI translation failed: %w", err)
}
if !resp.Success {
return "", fmt.Errorf("AI translation error: %s", resp.Error)
}
// Validate it's valid JSON
var check map[string]any
if err := json.Unmarshal([]byte(resp.Result), &check); err != nil {
return "", fmt.Errorf("AI returned invalid JSON: %w", err)
}
return resp.Result, nil
}
// buildTranslatableContent extracts translatable text from a config for the AI
func buildTranslatableContent(config *pages_module.LandingConfig) string {
data, _ := json.Marshal(map[string]any{
"hero": map[string]any{
"headline": config.Hero.Headline,
"subheadline": config.Hero.Subheadline,
"primary_cta": map[string]string{"label": config.Hero.PrimaryCTA.Label},
"secondary_cta": map[string]string{"label": config.Hero.SecondaryCTA.Label},
},
"stats": config.Stats,
"value_props": config.ValueProps,
"features": config.Features,
"cta_section": map[string]any{
"headline": config.CTASection.Headline,
"subheadline": config.CTASection.Subheadline,
"button": map[string]string{"label": config.CTASection.Button.Label},
},
"seo": map[string]any{
"title": config.SEO.Title,
"description": config.SEO.Description,
},
})
return string(data)
}