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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user