2
0

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
This commit is contained in:
2026-03-07 13:09:46 -05:00
parent a2edcdabe7
commit 5788123e00
17 changed files with 844 additions and 15 deletions

View File

@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
repo_model "code.gitcaddy.com/server/v3/models/repo"
@@ -34,8 +35,8 @@ type aiGeneratedConfig struct {
} `json:"secondary_cta"`
} `json:"hero"`
Stats []pages_module.StatConfig `json:"stats"`
ValueProps []pages_module.ValuePropConfig `json:"value_props"`
Features []pages_module.FeatureConfig `json:"features"`
ValueProps []pages_module.ValuePropConfig `json:"value_props"`
Features []pages_module.FeatureConfig `json:"features"`
CTASection struct {
Headline string `json:"headline"`
Subheadline string `json:"subheadline"`
@@ -66,9 +67,9 @@ func GenerateLandingPageContent(ctx context.Context, repo *repo_model.Repository
"repo_description": repo.Description,
"repo_url": repoURL,
"topics": topics,
"primary_language": repo.PrimaryLanguage,
"stars": fmt.Sprintf("%d", repo.NumStars),
"forks": fmt.Sprintf("%d", repo.NumForks),
"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.
@@ -165,6 +166,14 @@ Guidelines:
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
@@ -173,3 +182,103 @@ func truncateReadme(readme string) string {
}
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)
}