2
0
Files
gitcaddy-server/routers/web/repo/setting/pages.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

771 lines
25 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"fmt"
"net/http"
"slices"
"strings"
pages_model "code.gitcaddy.com/server/v3/models/pages"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/git"
"code.gitcaddy.com/server/v3/modules/json"
"code.gitcaddy.com/server/v3/modules/log"
pages_module "code.gitcaddy.com/server/v3/modules/pages"
"code.gitcaddy.com/server/v3/modules/templates"
"code.gitcaddy.com/server/v3/services/context"
pages_service "code.gitcaddy.com/server/v3/services/pages"
)
const (
tplRepoSettingsPages templates.TplName = "repo/settings/pages"
tplRepoSettingsPagesBrand templates.TplName = "repo/settings/pages_brand"
tplRepoSettingsPagesHero templates.TplName = "repo/settings/pages_hero"
tplRepoSettingsPagesContent templates.TplName = "repo/settings/pages_content"
tplRepoSettingsPagesSocial templates.TplName = "repo/settings/pages_social"
tplRepoSettingsPagesPricing templates.TplName = "repo/settings/pages_pricing"
tplRepoSettingsPagesFooter templates.TplName = "repo/settings/pages_footer"
tplRepoSettingsPagesTheme templates.TplName = "repo/settings/pages_theme"
tplRepoSettingsPagesLanguages templates.TplName = "repo/settings/pages_languages"
)
// getPagesLandingConfig loads the landing page configuration
func getPagesLandingConfig(ctx *context.Context) *pages_module.LandingConfig {
config, err := pages_service.GetPagesConfig(ctx, ctx.Repo.Repository)
if err != nil {
// Return default config
return &pages_module.LandingConfig{
Enabled: false,
Template: "open-source-hero",
Brand: pages_module.BrandConfig{Name: ctx.Repo.Repository.Name},
Hero: pages_module.HeroConfig{
Headline: ctx.Repo.Repository.Name,
Subheadline: ctx.Repo.Repository.Description,
},
Theme: pages_module.ThemeConfig{Mode: "auto"},
}
}
return config
}
// savePagesLandingConfig saves the landing page configuration
func savePagesLandingConfig(ctx *context.Context, config *pages_module.LandingConfig) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
if repo_model.IsErrPagesConfigNotExist(err) {
// Create new config
dbConfig = &repo_model.PagesConfig{
RepoID: ctx.Repo.Repository.ID,
Enabled: config.Enabled,
Template: repo_model.PagesTemplate(config.Template),
ConfigJSON: string(configJSON),
}
return repo_model.CreatePagesConfig(ctx, dbConfig)
}
return err
}
// Update existing config
dbConfig.Enabled = config.Enabled
dbConfig.Template = repo_model.PagesTemplate(config.Template)
dbConfig.ConfigJSON = string(configJSON)
return repo_model.UpdatePagesConfig(ctx, dbConfig)
}
// setCommonPagesData sets common data for all pages settings pages
func setCommonPagesData(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
ctx.Data["Config"] = config
ctx.Data["PagesEnabled"] = config.Enabled
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
ctx.Data["PagesTemplates"] = pages_module.ValidTemplates()
ctx.Data["PagesTemplateNames"] = pages_module.TemplateDisplayNames()
ctx.Data["AIEnabled"] = ai.IsEnabled()
}
// Pages shows the repository pages settings (General page)
func Pages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesGeneral"] = true
setCommonPagesData(ctx)
// Get pages config
config, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil && !repo_model.IsErrPagesConfigNotExist(err) {
ctx.ServerError("GetPagesConfig", err)
return
}
if config != nil {
ctx.Data["PagesEnabled"] = config.Enabled
ctx.Data["PagesTemplate"] = config.Template
}
// Get pages domains
domains, err := repo_model.GetPagesDomains(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetPagesDomains", err)
return
}
ctx.Data["PagesDomains"] = domains
ctx.HTML(http.StatusOK, tplRepoSettingsPages)
}
func PagesPost(ctx *context.Context) {
action := ctx.FormString("action")
switch action {
case "enable":
template := ctx.FormString("template")
if template == "" || !pages_module.IsValidTemplate(template) {
template = "open-source-hero"
}
config := getPagesLandingConfig(ctx)
config.Enabled = true
config.Template = template
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("EnablePages", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.enabled_success"))
case "disable":
config := getPagesLandingConfig(ctx)
config.Enabled = false
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("DisablePages", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.disabled_success"))
case "update_template":
template := ctx.FormString("template")
if template == "" || !pages_module.IsValidTemplate(template) {
template = "open-source-hero"
}
config := getPagesLandingConfig(ctx)
config.Template = template
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("UpdateTemplate", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
case "add_domain":
domain := ctx.FormString("domain")
if domain == "" {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_required"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
return
}
sslExternal := ctx.FormBool("ssl_external")
_, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, domain, sslExternal)
if err != nil {
if repo_model.IsErrPagesDomainAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_exists"))
} else {
ctx.ServerError("AddPagesDomain", err)
return
}
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_added"))
}
case "delete_domain":
domainID := ctx.FormInt64("domain_id")
if err := repo_model.DeletePagesDomain(ctx, domainID); err != nil {
ctx.ServerError("DeletePagesDomain", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_deleted"))
case "activate_ssl":
domainID := ctx.FormInt64("domain_id")
if err := repo_model.ActivatePagesDomainSSL(ctx, domainID); err != nil {
ctx.ServerError("ActivatePagesDomainSSL", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ssl_activated"))
case "verify_domain":
domainID := ctx.FormInt64("domain_id")
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_verification_failed"))
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_verified"))
}
case "ai_generate":
readme := loadRawReadme(ctx, ctx.Repo.Repository)
generated, err := pages_service.GenerateLandingPageContent(ctx, ctx.Repo.Repository, readme)
if err != nil {
log.Error("AI landing page generation failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.settings.pages.ai_generate_failed"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
return
}
// Merge AI-generated content into existing config, preserving settings
config := getPagesLandingConfig(ctx)
config.Brand.Name = generated.Brand.Name
config.Brand.Tagline = generated.Brand.Tagline
config.Hero = generated.Hero
config.Stats = generated.Stats
config.ValueProps = generated.ValueProps
config.Features = generated.Features
config.CTASection = generated.CTASection
config.SEO = generated.SEO
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ai_generate_success"))
default:
ctx.NotFound(nil)
return
}
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
}
func PagesBrand(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.brand")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesBrand"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesBrand)
}
func PagesBrandPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Brand.Name = ctx.FormString("brand_name")
config.Brand.LogoURL = ctx.FormString("brand_logo_url")
config.Brand.Tagline = ctx.FormString("brand_tagline")
config.Brand.FaviconURL = ctx.FormString("brand_favicon_url")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/brand")
}
func PagesHero(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.hero")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesHero"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesHero)
}
func PagesHeroPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Hero.Headline = ctx.FormString("headline")
config.Hero.Subheadline = ctx.FormString("subheadline")
config.Hero.ImageURL = ctx.FormString("image_url")
config.Hero.VideoURL = ctx.FormString("video_url")
config.Hero.CodeExample = ctx.FormString("code_example")
config.Hero.PrimaryCTA.Label = ctx.FormString("primary_cta_label")
config.Hero.PrimaryCTA.URL = ctx.FormString("primary_cta_url")
config.Hero.PrimaryCTA.Variant = ctx.FormString("primary_cta_variant")
config.Hero.SecondaryCTA.Label = ctx.FormString("secondary_cta_label")
config.Hero.SecondaryCTA.URL = ctx.FormString("secondary_cta_url")
config.Hero.SecondaryCTA.Variant = ctx.FormString("secondary_cta_variant")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/hero")
}
func PagesContent(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.content")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesContent"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesContent)
}
func PagesContentPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Advanced.PublicReleases = ctx.FormBool("public_releases")
config.Navigation.ShowDocs = ctx.FormBool("nav_show_docs")
config.Navigation.ShowAPI = ctx.FormBool("nav_show_api")
config.Navigation.ShowRepository = ctx.FormBool("nav_show_repository")
config.Navigation.ShowReleases = ctx.FormBool("nav_show_releases")
config.Navigation.ShowIssues = ctx.FormBool("nav_show_issues")
config.Blog.Enabled = ctx.FormBool("blog_enabled")
config.Blog.Headline = ctx.FormString("blog_headline")
config.Blog.Subheadline = ctx.FormString("blog_subheadline")
if maxPosts := ctx.FormInt("blog_max_posts"); maxPosts > 0 {
config.Blog.MaxPosts = maxPosts
}
config.Stats = nil
for i := range 10 {
value := ctx.FormString(fmt.Sprintf("stat_value_%d", i))
label := ctx.FormString(fmt.Sprintf("stat_label_%d", i))
if value == "" && label == "" {
continue
}
config.Stats = append(config.Stats, pages_module.StatConfig{Value: value, Label: label})
}
config.ValueProps = nil
for i := range 10 {
title := ctx.FormString(fmt.Sprintf("valueprop_title_%d", i))
desc := ctx.FormString(fmt.Sprintf("valueprop_desc_%d", i))
icon := ctx.FormString(fmt.Sprintf("valueprop_icon_%d", i))
if title == "" && desc == "" {
continue
}
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{Title: title, Description: desc, Icon: icon})
}
config.Features = nil
for i := range 20 {
title := ctx.FormString(fmt.Sprintf("feature_title_%d", i))
desc := ctx.FormString(fmt.Sprintf("feature_desc_%d", i))
icon := ctx.FormString(fmt.Sprintf("feature_icon_%d", i))
imageURL := ctx.FormString(fmt.Sprintf("feature_image_%d", i))
if title == "" && desc == "" {
continue
}
config.Features = append(config.Features, pages_module.FeatureConfig{Title: title, Description: desc, Icon: icon, ImageURL: imageURL})
}
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/content")
}
func PagesSocial(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.social")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesSocial"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesSocial)
}
func PagesSocialPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.SocialProof.Logos = nil
for i := range 20 {
logo := ctx.FormString(fmt.Sprintf("logo_%d", i))
if logo == "" {
continue
}
config.SocialProof.Logos = append(config.SocialProof.Logos, logo)
}
config.SocialProof.Testimonials = nil
for i := range 10 {
quote := ctx.FormString(fmt.Sprintf("testimonial_quote_%d", i))
author := ctx.FormString(fmt.Sprintf("testimonial_author_%d", i))
role := ctx.FormString(fmt.Sprintf("testimonial_role_%d", i))
avatar := ctx.FormString(fmt.Sprintf("testimonial_avatar_%d", i))
if quote == "" && author == "" {
continue
}
config.SocialProof.Testimonials = append(config.SocialProof.Testimonials, pages_module.TestimonialConfig{Quote: quote, Author: author, Role: role, Avatar: avatar})
}
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/social")
}
func PagesPricing(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.pricing")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesPricing"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesPricing)
}
func PagesPricingPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Pricing.Headline = ctx.FormString("pricing_headline")
config.Pricing.Subheadline = ctx.FormString("pricing_subheadline")
config.Pricing.Plans = nil
for i := range 5 {
name := ctx.FormString(fmt.Sprintf("plan_name_%d", i))
price := ctx.FormString(fmt.Sprintf("plan_price_%d", i))
if name == "" && price == "" {
continue
}
featuresText := ctx.FormString(fmt.Sprintf("plan_%d_features", i))
var features []string
for f := range strings.SplitSeq(featuresText, "\n") {
f = strings.TrimSpace(f)
if f != "" {
features = append(features, f)
}
}
config.Pricing.Plans = append(config.Pricing.Plans, pages_module.PricingPlanConfig{
Name: name, Price: price, Period: ctx.FormString(fmt.Sprintf("plan_period_%d", i)),
Features: features, CTA: ctx.FormString(fmt.Sprintf("plan_cta_%d", i)),
Featured: ctx.FormBool(fmt.Sprintf("plan_featured_%d", i)),
})
}
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/pricing")
}
func PagesFooter(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.footer")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesFooter"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesFooter)
}
func PagesFooterPost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.CTASection.Headline = ctx.FormString("cta_headline")
config.CTASection.Subheadline = ctx.FormString("cta_subheadline")
config.CTASection.Button.Label = ctx.FormString("cta_button_label")
config.CTASection.Button.URL = ctx.FormString("cta_button_url")
config.CTASection.Button.Variant = ctx.FormString("cta_button_variant")
config.Footer.Copyright = ctx.FormString("footer_copyright")
config.Footer.Links = nil
for i := range 10 {
label := ctx.FormString(fmt.Sprintf("footer_link_label_%d", i))
url := ctx.FormString(fmt.Sprintf("footer_link_url_%d", i))
if label == "" && url == "" {
continue
}
config.Footer.Links = append(config.Footer.Links, pages_module.FooterLink{Label: label, URL: url})
}
config.Footer.Social = nil
for i := range 10 {
platform := ctx.FormString(fmt.Sprintf("social_platform_%d", i))
url := ctx.FormString(fmt.Sprintf("social_url_%d", i))
if platform == "" && url == "" {
continue
}
config.Footer.Social = append(config.Footer.Social, pages_module.SocialLink{Platform: platform, URL: url})
}
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/footer")
}
func PagesTheme(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.theme")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesTheme"] = true
setCommonPagesData(ctx)
ctx.HTML(http.StatusOK, tplRepoSettingsPagesTheme)
}
func PagesThemePost(ctx *context.Context) {
config := getPagesLandingConfig(ctx)
config.Theme.PrimaryColor = ctx.FormString("primary_color")
config.Theme.AccentColor = ctx.FormString("accent_color")
config.Theme.Mode = ctx.FormString("theme_mode")
config.SEO.Title = ctx.FormString("seo_title")
config.SEO.Description = ctx.FormString("seo_description")
keywords := ctx.FormString("seo_keywords")
if keywords != "" {
config.SEO.Keywords = strings.Split(keywords, ",")
} else {
config.SEO.Keywords = nil
}
config.SEO.OGImage = ctx.FormString("og_image")
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.saved"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/theme")
}
// TranslationView is a flattened view of a translation for the settings UI
type TranslationView struct {
Headline string
Subheadline string
PrimaryCTA string
SecondaryCTA string
CTAHeadline string
CTASubheadline string
CTAButton string
}
// parseTranslationView parses a PagesTranslation into a flat view for the template
func parseTranslationView(t *pages_model.Translation) *TranslationView {
if t == nil || t.ConfigJSON == "" {
return nil
}
var overlay map[string]any
if err := json.Unmarshal([]byte(t.ConfigJSON), &overlay); err != nil {
return nil
}
view := &TranslationView{}
if hero, ok := overlay["hero"].(map[string]any); ok {
if v, ok := hero["headline"].(string); ok {
view.Headline = v
}
if v, ok := hero["subheadline"].(string); ok {
view.Subheadline = v
}
if cta, ok := hero["primary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.PrimaryCTA = v
}
}
if cta, ok := hero["secondary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.SecondaryCTA = v
}
}
}
if ctaSec, ok := overlay["cta_section"].(map[string]any); ok {
if v, ok := ctaSec["headline"].(string); ok {
view.CTAHeadline = v
}
if v, ok := ctaSec["subheadline"].(string); ok {
view.CTASubheadline = v
}
if btn, ok := ctaSec["button"].(map[string]any); ok {
if v, ok := btn["label"].(string); ok {
view.CTAButton = v
}
}
}
return view
}
// buildTranslationJSON builds a JSON overlay string from form values
func buildTranslationJSON(ctx *context.Context) string {
overlay := map[string]any{}
hero := map[string]any{}
if v := ctx.FormString("trans_headline"); v != "" {
hero["headline"] = v
}
if v := ctx.FormString("trans_subheadline"); v != "" {
hero["subheadline"] = v
}
if v := ctx.FormString("trans_primary_cta"); v != "" {
hero["primary_cta"] = map[string]any{"label": v}
}
if v := ctx.FormString("trans_secondary_cta"); v != "" {
hero["secondary_cta"] = map[string]any{"label": v}
}
if len(hero) > 0 {
overlay["hero"] = hero
}
ctaSec := map[string]any{}
if v := ctx.FormString("trans_cta_headline"); v != "" {
ctaSec["headline"] = v
}
if v := ctx.FormString("trans_cta_subheadline"); v != "" {
ctaSec["subheadline"] = v
}
if v := ctx.FormString("trans_cta_button"); v != "" {
ctaSec["button"] = map[string]any{"label": v}
}
if len(ctaSec) > 0 {
overlay["cta_section"] = ctaSec
}
if len(overlay) == 0 {
return ""
}
data, _ := json.Marshal(overlay)
return string(data)
}
func PagesLanguages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.pages.languages")
ctx.Data["PageIsSettingsPages"] = true
ctx.Data["PageIsSettingsPagesLanguages"] = true
setCommonPagesData(ctx)
config := getPagesLandingConfig(ctx)
ctx.Data["LanguageNames"] = pages_module.LanguageDisplayNames()
// Build a function to check if a language is enabled
enabledLangs := config.I18n.Languages
ctx.Data["IsLangEnabled"] = func(code string) bool {
return slices.Contains(enabledLangs, code)
}
// Load translations into a map[lang]*TranslationView
translationMap := make(map[string]*TranslationView)
translations, err := pages_model.GetTranslationsByRepoID(ctx, ctx.Repo.Repository.ID)
if err == nil {
for _, t := range translations {
translationMap[t.Lang] = parseTranslationView(t)
}
}
ctx.Data["TranslationMap"] = translationMap
ctx.HTML(http.StatusOK, tplRepoSettingsPagesLanguages)
}
func PagesLanguagesPost(ctx *context.Context) {
action := ctx.FormString("action")
config := getPagesLandingConfig(ctx)
switch action {
case "update_i18n":
config.I18n.DefaultLang = ctx.FormString("default_lang")
if config.I18n.DefaultLang == "" {
config.I18n.DefaultLang = "en"
}
selectedLangs := ctx.Req.Form["languages"]
// Ensure default language is always in the list
if !slices.Contains(selectedLangs, config.I18n.DefaultLang) {
selectedLangs = append([]string{config.I18n.DefaultLang}, selectedLangs...)
}
config.I18n.Languages = selectedLangs
if err := savePagesLandingConfig(ctx, config); err != nil {
ctx.ServerError("SavePagesConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.languages_saved"))
case "save_translation":
targetLang := ctx.FormString("target_lang")
if targetLang == "" {
ctx.Flash.Error("Language is required")
break
}
configJSON := buildTranslationJSON(ctx)
if configJSON == "" {
ctx.Flash.Warning(ctx.Tr("repo.settings.pages.translation_empty"))
break
}
existing, err := pages_model.GetTranslation(ctx, ctx.Repo.Repository.ID, targetLang)
if err != nil {
ctx.ServerError("GetTranslation", err)
return
}
if existing != nil {
existing.ConfigJSON = configJSON
existing.AutoGenerated = false
if err := pages_model.UpdateTranslation(ctx, existing); err != nil {
ctx.ServerError("UpdateTranslation", err)
return
}
} else {
t := &pages_model.Translation{
RepoID: ctx.Repo.Repository.ID,
Lang: targetLang,
ConfigJSON: configJSON,
}
if err := pages_model.CreateTranslation(ctx, t); err != nil {
ctx.ServerError("CreateTranslation", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.translation_saved"))
case "delete_translation":
targetLang := ctx.FormString("target_lang")
if err := pages_model.DeleteTranslation(ctx, ctx.Repo.Repository.ID, targetLang); err != nil {
ctx.ServerError("DeleteTranslation", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.translation_deleted"))
case "ai_translate":
targetLang := ctx.FormString("target_lang")
if targetLang == "" {
ctx.Flash.Error("Language is required")
break
}
translated, err := pages_service.TranslateLandingPageContent(ctx, ctx.Repo.Repository, config, targetLang)
if err != nil {
log.Error("AI translation failed: %v", err)
ctx.Flash.Error(fmt.Sprintf("AI translation failed: %v", err))
break
}
// Save or update the translation
existing, err := pages_model.GetTranslation(ctx, ctx.Repo.Repository.ID, targetLang)
if err != nil {
ctx.ServerError("GetTranslation", err)
return
}
if existing != nil {
existing.ConfigJSON = translated
existing.AutoGenerated = true
if err := pages_model.UpdateTranslation(ctx, existing); err != nil {
ctx.ServerError("UpdateTranslation", err)
return
}
} else {
t := &pages_model.Translation{
RepoID: ctx.Repo.Repository.ID,
Lang: targetLang,
ConfigJSON: translated,
AutoGenerated: true,
}
if err := pages_model.CreateTranslation(ctx, t); err != nil {
ctx.ServerError("CreateTranslation", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ai_translate_success"))
}
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/languages")
}
// loadRawReadme loads the raw README content from the repository for AI consumption
func loadRawReadme(ctx *context.Context, repo *repo_model.Repository) string {
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return ""
}
defer gitRepo.Close()
branch := repo.DefaultBranch
if branch == "" {
branch = "main"
}
commit, err := gitRepo.GetBranchCommit(branch)
if err != nil {
return ""
}
for _, name := range []string{"README.md", "readme.md", "README", "README.txt"} {
entry, err := commit.GetTreeEntryByPath(name)
if err != nil {
continue
}
reader, err := entry.Blob().DataAsync()
if err != nil {
continue
}
content := make([]byte, entry.Blob().Size())
_, _ = reader.Read(content)
reader.Close()
return string(content)
}
return ""
}