2
0

feat(i18n): add translation keys for new pages sections
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m41s
Build and Release / Unit Tests (push) Successful in 8m55s
Build and Release / Lint (push) Successful in 9m23s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1m4s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Failing after 9h0m35s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 50s
Build and Release / Build Binary (linux/arm64) (push) Failing after 32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Failing after 54s

Add locale keys for translating new landing page sections: gallery, comparison, blog, and expanded fields for stats, pricing, testimonials, and footer. Adds section headers and field labels for translation UI. Includes keys for all 29 supported languages. Enables full localization of new pages features added in recent commits.
This commit is contained in:
2026-03-17 03:52:30 -04:00
parent c5e35e3466
commit 12341079e1
32 changed files with 1781 additions and 63 deletions

View File

@@ -716,19 +716,89 @@ func PagesThemePost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages/theme")
}
// TranslationView is a flattened view of a translation for the settings UI
// TranslationView is a flattened view of a translation for the settings UI.
// Slice fields are padded to match the base config array lengths.
type TranslationView struct {
Headline string
Subheadline string
PrimaryCTA string
SecondaryCTA string
// Brand
BrandName string
BrandTagline string
// Hero
Headline string
Subheadline string
PrimaryCTA string
SecondaryCTA string
// Stats (parallel to Config.Stats)
StatsValues []string
StatsLabels []string
// Value Props (parallel to Config.ValueProps)
ValuePropTitles []string
ValuePropDescs []string
// Features (parallel to Config.Features)
FeatureTitles []string
FeatureDescs []string
// Testimonials (parallel to Config.SocialProof.Testimonials)
TestimonialQuotes []string
TestimonialRoles []string
// Pricing
PricingHeadline string
PricingSubheadline string
PlanNames []string
PlanPeriods []string
PlanCTAs []string
// CTA Section
CTAHeadline string
CTASubheadline string
CTAButton string
// Blog
BlogHeadline string
BlogSubheadline string
BlogCTAButton string
// Gallery
GalleryHeadline string
GallerySubheadline string
// Comparison
ComparisonHeadline string
ComparisonSubheadline string
// Footer
FooterCopyright string
FooterLinkLabels []string
// SEO
SEOTitle string
SEODescription string
}
// parseTranslationView parses a PagesTranslation into a flat view for the template
func parseTranslationView(t *pages_model.Translation) *TranslationView {
// overlayString extracts a string from a map
func overlayString(m map[string]any, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
// overlayStringSlice extracts a []string-field slice from an overlay array of objects.
// Each array element is a map; fieldKey is the key to extract from each map.
func overlayStringSlice(overlay map[string]any, arrayKey, fieldKey string, padLen int) []string {
result := make([]string, padLen)
arr, ok := overlay[arrayKey].([]any)
if !ok {
return result
}
for i, item := range arr {
if i >= padLen {
break
}
if m, ok := item.(map[string]any); ok {
if v, ok := m[fieldKey].(string); ok {
result[i] = v
}
}
}
return result
}
// parseTranslationView parses a PagesTranslation into a flat view for the template.
// config is used to determine array lengths for padding slices.
func parseTranslationView(t *pages_model.Translation, config *pages_module.LandingConfig) *TranslationView {
if t == nil || t.ConfigJSON == "" {
return nil
}
@@ -738,37 +808,135 @@ func parseTranslationView(t *pages_model.Translation) *TranslationView {
}
view := &TranslationView{}
// Brand
if brand, ok := overlay["brand"].(map[string]any); ok {
view.BrandName = overlayString(brand, "name")
view.BrandTagline = overlayString(brand, "tagline")
}
// Hero
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
}
view.Headline = overlayString(hero, "headline")
view.Subheadline = overlayString(hero, "subheadline")
if cta, ok := hero["primary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.PrimaryCTA = v
}
view.PrimaryCTA = overlayString(cta, "label")
}
if cta, ok := hero["secondary_cta"].(map[string]any); ok {
if v, ok := cta["label"].(string); ok {
view.SecondaryCTA = v
view.SecondaryCTA = overlayString(cta, "label")
}
}
// Stats
view.StatsValues = overlayStringSlice(overlay, "stats", "value", len(config.Stats))
view.StatsLabels = overlayStringSlice(overlay, "stats", "label", len(config.Stats))
// Value Props
view.ValuePropTitles = overlayStringSlice(overlay, "value_props", "title", len(config.ValueProps))
view.ValuePropDescs = overlayStringSlice(overlay, "value_props", "description", len(config.ValueProps))
// Features
view.FeatureTitles = overlayStringSlice(overlay, "features", "title", len(config.Features))
view.FeatureDescs = overlayStringSlice(overlay, "features", "description", len(config.Features))
// Testimonials (stored under social_proof.testimonials)
view.TestimonialQuotes = make([]string, len(config.SocialProof.Testimonials))
view.TestimonialRoles = make([]string, len(config.SocialProof.Testimonials))
if sp, ok := overlay["social_proof"].(map[string]any); ok {
if arr, ok := sp["testimonials"].([]any); ok {
for i, item := range arr {
if i >= len(config.SocialProof.Testimonials) {
break
}
if m, ok := item.(map[string]any); ok {
view.TestimonialQuotes[i] = overlayString(m, "quote")
view.TestimonialRoles[i] = overlayString(m, "role")
}
}
}
}
// Pricing
if pricing, ok := overlay["pricing"].(map[string]any); ok {
view.PricingHeadline = overlayString(pricing, "headline")
view.PricingSubheadline = overlayString(pricing, "subheadline")
if plans, ok := pricing["plans"].([]any); ok {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
for i, item := range plans {
if i >= len(config.Pricing.Plans) {
break
}
if m, ok := item.(map[string]any); ok {
view.PlanNames[i] = overlayString(m, "name")
view.PlanPeriods[i] = overlayString(m, "period")
view.PlanCTAs[i] = overlayString(m, "cta")
}
}
} else {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
}
} else {
view.PlanNames = make([]string, len(config.Pricing.Plans))
view.PlanPeriods = make([]string, len(config.Pricing.Plans))
view.PlanCTAs = make([]string, len(config.Pricing.Plans))
}
// CTA Section
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
}
view.CTAHeadline = overlayString(ctaSec, "headline")
view.CTASubheadline = overlayString(ctaSec, "subheadline")
if btn, ok := ctaSec["button"].(map[string]any); ok {
if v, ok := btn["label"].(string); ok {
view.CTAButton = v
view.CTAButton = overlayString(btn, "label")
}
}
// Blog
if blog, ok := overlay["blog"].(map[string]any); ok {
view.BlogHeadline = overlayString(blog, "headline")
view.BlogSubheadline = overlayString(blog, "subheadline")
if btn, ok := blog["cta_button"].(map[string]any); ok {
view.BlogCTAButton = overlayString(btn, "label")
}
}
// Gallery
if gallery, ok := overlay["gallery"].(map[string]any); ok {
view.GalleryHeadline = overlayString(gallery, "headline")
view.GallerySubheadline = overlayString(gallery, "subheadline")
}
// Comparison
if comp, ok := overlay["comparison"].(map[string]any); ok {
view.ComparisonHeadline = overlayString(comp, "headline")
view.ComparisonSubheadline = overlayString(comp, "subheadline")
}
// Footer
view.FooterLinkLabels = make([]string, len(config.Footer.Links))
if footer, ok := overlay["footer"].(map[string]any); ok {
view.FooterCopyright = overlayString(footer, "copyright")
if links, ok := footer["links"].([]any); ok {
for i, item := range links {
if i >= len(config.Footer.Links) {
break
}
if m, ok := item.(map[string]any); ok {
view.FooterLinkLabels[i] = overlayString(m, "label")
}
}
}
}
// SEO
if seo, ok := overlay["seo"].(map[string]any); ok {
view.SEOTitle = overlayString(seo, "title")
view.SEODescription = overlayString(seo, "description")
}
return view
}
@@ -776,6 +944,19 @@ func parseTranslationView(t *pages_model.Translation) *TranslationView {
func buildTranslationJSON(ctx *context.Context) string {
overlay := map[string]any{}
// Brand
brand := map[string]any{}
if v := ctx.FormString("trans_brand_name"); v != "" {
brand["name"] = v
}
if v := ctx.FormString("trans_brand_tagline"); v != "" {
brand["tagline"] = v
}
if len(brand) > 0 {
overlay["brand"] = brand
}
// Hero
hero := map[string]any{}
if v := ctx.FormString("trans_headline"); v != "" {
hero["headline"] = v
@@ -793,6 +974,103 @@ func buildTranslationJSON(ctx *context.Context) string {
overlay["hero"] = hero
}
// Stats (indexed)
var stats []map[string]any
for i := range 20 {
v := ctx.FormString(fmt.Sprintf("trans_stat_%d_value", i))
l := ctx.FormString(fmt.Sprintf("trans_stat_%d_label", i))
if v == "" && l == "" {
// Check if there are more by looking ahead
if ctx.FormString(fmt.Sprintf("trans_stat_%d_value", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_stat_%d_label", i+1)) == "" {
break
}
}
stats = append(stats, map[string]any{"value": v, "label": l})
}
if len(stats) > 0 {
overlay["stats"] = stats
}
// Value Props (indexed)
var valueProps []map[string]any
for i := range 20 {
t := ctx.FormString(fmt.Sprintf("trans_vp_%d_title", i))
d := ctx.FormString(fmt.Sprintf("trans_vp_%d_desc", i))
if t == "" && d == "" {
if ctx.FormString(fmt.Sprintf("trans_vp_%d_title", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_vp_%d_desc", i+1)) == "" {
break
}
}
valueProps = append(valueProps, map[string]any{"title": t, "description": d})
}
if len(valueProps) > 0 {
overlay["value_props"] = valueProps
}
// Features (indexed)
var features []map[string]any
for i := range 20 {
t := ctx.FormString(fmt.Sprintf("trans_feat_%d_title", i))
d := ctx.FormString(fmt.Sprintf("trans_feat_%d_desc", i))
if t == "" && d == "" {
if ctx.FormString(fmt.Sprintf("trans_feat_%d_title", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_feat_%d_desc", i+1)) == "" {
break
}
}
features = append(features, map[string]any{"title": t, "description": d})
}
if len(features) > 0 {
overlay["features"] = features
}
// Testimonials (indexed)
var testimonials []map[string]any
for i := range 20 {
q := ctx.FormString(fmt.Sprintf("trans_test_%d_quote", i))
r := ctx.FormString(fmt.Sprintf("trans_test_%d_role", i))
if q == "" && r == "" {
if ctx.FormString(fmt.Sprintf("trans_test_%d_quote", i+1)) == "" &&
ctx.FormString(fmt.Sprintf("trans_test_%d_role", i+1)) == "" {
break
}
}
testimonials = append(testimonials, map[string]any{"quote": q, "role": r})
}
if len(testimonials) > 0 {
overlay["social_proof"] = map[string]any{"testimonials": testimonials}
}
// Pricing
pricing := map[string]any{}
if v := ctx.FormString("trans_pricing_headline"); v != "" {
pricing["headline"] = v
}
if v := ctx.FormString("trans_pricing_subheadline"); v != "" {
pricing["subheadline"] = v
}
var plans []map[string]any
for i := range 10 {
n := ctx.FormString(fmt.Sprintf("trans_plan_%d_name", i))
p := ctx.FormString(fmt.Sprintf("trans_plan_%d_period", i))
c := ctx.FormString(fmt.Sprintf("trans_plan_%d_cta", i))
if n == "" && p == "" && c == "" {
if ctx.FormString(fmt.Sprintf("trans_plan_%d_name", i+1)) == "" {
break
}
}
plans = append(plans, map[string]any{"name": n, "period": p, "cta": c})
}
if len(plans) > 0 {
pricing["plans"] = plans
}
if len(pricing) > 0 {
overlay["pricing"] = pricing
}
// CTA Section
ctaSec := map[string]any{}
if v := ctx.FormString("trans_cta_headline"); v != "" {
ctaSec["headline"] = v
@@ -807,6 +1085,79 @@ func buildTranslationJSON(ctx *context.Context) string {
overlay["cta_section"] = ctaSec
}
// Blog
blog := map[string]any{}
if v := ctx.FormString("trans_blog_headline"); v != "" {
blog["headline"] = v
}
if v := ctx.FormString("trans_blog_subheadline"); v != "" {
blog["subheadline"] = v
}
if v := ctx.FormString("trans_blog_cta"); v != "" {
blog["cta_button"] = map[string]any{"label": v}
}
if len(blog) > 0 {
overlay["blog"] = blog
}
// Gallery
gallery := map[string]any{}
if v := ctx.FormString("trans_gallery_headline"); v != "" {
gallery["headline"] = v
}
if v := ctx.FormString("trans_gallery_subheadline"); v != "" {
gallery["subheadline"] = v
}
if len(gallery) > 0 {
overlay["gallery"] = gallery
}
// Comparison
comp := map[string]any{}
if v := ctx.FormString("trans_comparison_headline"); v != "" {
comp["headline"] = v
}
if v := ctx.FormString("trans_comparison_subheadline"); v != "" {
comp["subheadline"] = v
}
if len(comp) > 0 {
overlay["comparison"] = comp
}
// Footer
footer := map[string]any{}
if v := ctx.FormString("trans_footer_copyright"); v != "" {
footer["copyright"] = v
}
var footerLinks []map[string]any
for i := range 20 {
l := ctx.FormString(fmt.Sprintf("trans_footer_link_%d", i))
if l == "" {
if ctx.FormString(fmt.Sprintf("trans_footer_link_%d", i+1)) == "" {
break
}
}
footerLinks = append(footerLinks, map[string]any{"label": l})
}
if len(footerLinks) > 0 {
footer["links"] = footerLinks
}
if len(footer) > 0 {
overlay["footer"] = footer
}
// SEO
seo := map[string]any{}
if v := ctx.FormString("trans_seo_title"); v != "" {
seo["title"] = v
}
if v := ctx.FormString("trans_seo_description"); v != "" {
seo["description"] = v
}
if len(seo) > 0 {
overlay["seo"] = seo
}
if len(overlay) == 0 {
return ""
}
@@ -835,7 +1186,7 @@ func PagesLanguages(ctx *context.Context) {
translations, err := pages_model.GetTranslationsByRepoID(ctx, ctx.Repo.Repository.ID)
if err == nil {
for _, t := range translations {
translationMap[t.Lang] = parseTranslationView(t)
translationMap[t.Lang] = parseTranslationView(t, config)
}
}
ctx.Data["TranslationMap"] = translationMap