|
|
|
@@ -209,12 +209,55 @@ func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repositor
|
|
|
|
|
|
|
|
|
|
// Validate it's valid JSON
|
|
|
|
|
result := extractJSON(resp.Result)
|
|
|
|
|
var check map[string]any
|
|
|
|
|
if err := json.Unmarshal([]byte(result), &check); err != nil {
|
|
|
|
|
var parsed map[string]any
|
|
|
|
|
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
|
|
|
|
|
return "", fmt.Errorf("AI returned invalid JSON: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
// Strip placeholder values the AI sometimes returns for empty inputs
|
|
|
|
|
// (e.g. "<UNKNOWN>", "<EMPTY>", "N/A") so they don't end up saved as translations.
|
|
|
|
|
stripPlaceholders(parsed)
|
|
|
|
|
|
|
|
|
|
cleaned, err := json.Marshal(parsed)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("re-marshal translation: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return string(cleaned), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stripPlaceholders walks a translation overlay and removes string values that
|
|
|
|
|
// look like AI placeholders for empty input (e.g. "<UNKNOWN>", "<EMPTY>", "N/A").
|
|
|
|
|
// This keeps stale "<UNKNOWN>" strings out of the saved translation overlay.
|
|
|
|
|
func stripPlaceholders(v any) {
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
case map[string]any:
|
|
|
|
|
for k, child := range t {
|
|
|
|
|
if s, ok := child.(string); ok && isPlaceholder(s) {
|
|
|
|
|
delete(t, k)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
stripPlaceholders(child)
|
|
|
|
|
}
|
|
|
|
|
case []any:
|
|
|
|
|
for _, item := range t {
|
|
|
|
|
stripPlaceholders(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isPlaceholder(s string) bool {
|
|
|
|
|
switch strings.TrimSpace(strings.ToUpper(s)) {
|
|
|
|
|
case "<UNKNOWN>", "<EMPTY>", "<NULL>", "N/A", "NONE":
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setIfNotEmpty assigns v to m[key] only if v is non-empty.
|
|
|
|
|
func setIfNotEmpty(m map[string]any, key, v string) {
|
|
|
|
|
if v != "" {
|
|
|
|
|
m[key] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildTranslatableContent extracts translatable text from a config for the AI
|
|
|
|
@@ -223,18 +266,26 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
|
|
|
|
|
// Brand
|
|
|
|
|
if config.Brand.Name != "" || config.Brand.Tagline != "" {
|
|
|
|
|
content["brand"] = map[string]any{
|
|
|
|
|
"name": config.Brand.Name,
|
|
|
|
|
"tagline": config.Brand.Tagline,
|
|
|
|
|
brand := map[string]any{}
|
|
|
|
|
setIfNotEmpty(brand, "name", config.Brand.Name)
|
|
|
|
|
setIfNotEmpty(brand, "tagline", config.Brand.Tagline)
|
|
|
|
|
if len(brand) > 0 {
|
|
|
|
|
content["brand"] = brand
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hero
|
|
|
|
|
content["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},
|
|
|
|
|
// Hero — only include non-empty fields so the AI doesn't return "<UNKNOWN>"
|
|
|
|
|
hero := map[string]any{}
|
|
|
|
|
setIfNotEmpty(hero, "headline", config.Hero.Headline)
|
|
|
|
|
setIfNotEmpty(hero, "subheadline", config.Hero.Subheadline)
|
|
|
|
|
if config.Hero.PrimaryCTA.Label != "" {
|
|
|
|
|
hero["primary_cta"] = map[string]string{"label": config.Hero.PrimaryCTA.Label}
|
|
|
|
|
}
|
|
|
|
|
if config.Hero.SecondaryCTA.Label != "" {
|
|
|
|
|
hero["secondary_cta"] = map[string]string{"label": config.Hero.SecondaryCTA.Label}
|
|
|
|
|
}
|
|
|
|
|
if len(hero) > 0 {
|
|
|
|
|
content["hero"] = hero
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stats, Value Props, Features (already struct-serializable)
|
|
|
|
@@ -264,39 +315,58 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
if len(config.SocialProof.Testimonials) > 0 {
|
|
|
|
|
testimonials := make([]map[string]string, 0, len(config.SocialProof.Testimonials))
|
|
|
|
|
for _, t := range config.SocialProof.Testimonials {
|
|
|
|
|
testimonials = append(testimonials, map[string]string{
|
|
|
|
|
"quote": t.Quote,
|
|
|
|
|
"role": t.Role,
|
|
|
|
|
})
|
|
|
|
|
tm := map[string]string{}
|
|
|
|
|
if t.Quote != "" {
|
|
|
|
|
tm["quote"] = t.Quote
|
|
|
|
|
}
|
|
|
|
|
if t.Role != "" {
|
|
|
|
|
tm["role"] = t.Role
|
|
|
|
|
}
|
|
|
|
|
if len(tm) > 0 {
|
|
|
|
|
testimonials = append(testimonials, tm)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(testimonials) > 0 {
|
|
|
|
|
content["social_proof"] = map[string]any{"testimonials": testimonials}
|
|
|
|
|
}
|
|
|
|
|
content["social_proof"] = map[string]any{"testimonials": testimonials}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pricing
|
|
|
|
|
if config.Pricing.Headline != "" || len(config.Pricing.Plans) > 0 {
|
|
|
|
|
pricing := map[string]any{
|
|
|
|
|
"headline": config.Pricing.Headline,
|
|
|
|
|
"subheadline": config.Pricing.Subheadline,
|
|
|
|
|
}
|
|
|
|
|
pricing := map[string]any{}
|
|
|
|
|
setIfNotEmpty(pricing, "headline", config.Pricing.Headline)
|
|
|
|
|
setIfNotEmpty(pricing, "subheadline", config.Pricing.Subheadline)
|
|
|
|
|
if len(config.Pricing.Plans) > 0 {
|
|
|
|
|
plans := make([]map[string]string, 0, len(config.Pricing.Plans))
|
|
|
|
|
for _, p := range config.Pricing.Plans {
|
|
|
|
|
plans = append(plans, map[string]string{
|
|
|
|
|
"name": p.Name,
|
|
|
|
|
"period": p.Period,
|
|
|
|
|
"cta": p.CTA,
|
|
|
|
|
})
|
|
|
|
|
plan := map[string]string{}
|
|
|
|
|
if p.Name != "" {
|
|
|
|
|
plan["name"] = p.Name
|
|
|
|
|
}
|
|
|
|
|
if p.Period != "" {
|
|
|
|
|
plan["period"] = p.Period
|
|
|
|
|
}
|
|
|
|
|
if p.CTA != "" {
|
|
|
|
|
plan["cta"] = p.CTA
|
|
|
|
|
}
|
|
|
|
|
plans = append(plans, plan)
|
|
|
|
|
}
|
|
|
|
|
pricing["plans"] = plans
|
|
|
|
|
}
|
|
|
|
|
content["pricing"] = pricing
|
|
|
|
|
if len(pricing) > 0 {
|
|
|
|
|
content["pricing"] = pricing
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CTA Section
|
|
|
|
|
content["cta_section"] = map[string]any{
|
|
|
|
|
"headline": config.CTASection.Headline,
|
|
|
|
|
"subheadline": config.CTASection.Subheadline,
|
|
|
|
|
"button": map[string]string{"label": config.CTASection.Button.Label},
|
|
|
|
|
cta := map[string]any{}
|
|
|
|
|
setIfNotEmpty(cta, "headline", config.CTASection.Headline)
|
|
|
|
|
setIfNotEmpty(cta, "subheadline", config.CTASection.Subheadline)
|
|
|
|
|
if config.CTASection.Button.Label != "" {
|
|
|
|
|
cta["button"] = map[string]string{"label": config.CTASection.Button.Label}
|
|
|
|
|
}
|
|
|
|
|
if len(cta) > 0 {
|
|
|
|
|
content["cta_section"] = cta
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Blog
|
|
|
|
@@ -306,9 +376,9 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
blogHeadline = "Latest Posts"
|
|
|
|
|
}
|
|
|
|
|
blog := map[string]any{
|
|
|
|
|
"headline": blogHeadline,
|
|
|
|
|
"subheadline": config.Blog.Subheadline,
|
|
|
|
|
"headline": blogHeadline,
|
|
|
|
|
}
|
|
|
|
|
setIfNotEmpty(blog, "subheadline", config.Blog.Subheadline)
|
|
|
|
|
if config.Blog.CTAButton.Label != "" {
|
|
|
|
|
blog["cta_button"] = map[string]string{"label": config.Blog.CTAButton.Label}
|
|
|
|
|
}
|
|
|
|
@@ -321,10 +391,11 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
if galleryHeadline == "" {
|
|
|
|
|
galleryHeadline = "Gallery"
|
|
|
|
|
}
|
|
|
|
|
content["gallery"] = map[string]any{
|
|
|
|
|
"headline": galleryHeadline,
|
|
|
|
|
"subheadline": config.Gallery.Subheadline,
|
|
|
|
|
gallery := map[string]any{
|
|
|
|
|
"headline": galleryHeadline,
|
|
|
|
|
}
|
|
|
|
|
setIfNotEmpty(gallery, "subheadline", config.Gallery.Subheadline)
|
|
|
|
|
content["gallery"] = gallery
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Comparison
|
|
|
|
@@ -333,10 +404,11 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
if compHeadline == "" {
|
|
|
|
|
compHeadline = "How We Compare"
|
|
|
|
|
}
|
|
|
|
|
content["comparison"] = map[string]any{
|
|
|
|
|
"headline": compHeadline,
|
|
|
|
|
"subheadline": config.Comparison.Subheadline,
|
|
|
|
|
comparison := map[string]any{
|
|
|
|
|
"headline": compHeadline,
|
|
|
|
|
}
|
|
|
|
|
setIfNotEmpty(comparison, "subheadline", config.Comparison.Subheadline)
|
|
|
|
|
content["comparison"] = comparison
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cross-Promote
|
|
|
|
@@ -345,32 +417,40 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|
|
|
|
if cpHeadline == "" {
|
|
|
|
|
cpHeadline = "Related Offerings"
|
|
|
|
|
}
|
|
|
|
|
content["cross_promote"] = map[string]any{
|
|
|
|
|
"headline": cpHeadline,
|
|
|
|
|
"subheadline": config.CrossPromote.Subheadline,
|
|
|
|
|
crossPromote := map[string]any{
|
|
|
|
|
"headline": cpHeadline,
|
|
|
|
|
}
|
|
|
|
|
setIfNotEmpty(crossPromote, "subheadline", config.CrossPromote.Subheadline)
|
|
|
|
|
content["cross_promote"] = crossPromote
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Footer
|
|
|
|
|
if config.Footer.Copyright != "" || len(config.Footer.Links) > 0 {
|
|
|
|
|
footer := map[string]any{
|
|
|
|
|
"copyright": config.Footer.Copyright,
|
|
|
|
|
}
|
|
|
|
|
footer := map[string]any{}
|
|
|
|
|
setIfNotEmpty(footer, "copyright", config.Footer.Copyright)
|
|
|
|
|
if len(config.Footer.Links) > 0 {
|
|
|
|
|
links := make([]map[string]string, 0, len(config.Footer.Links))
|
|
|
|
|
for _, l := range config.Footer.Links {
|
|
|
|
|
links = append(links, map[string]string{"label": l.Label})
|
|
|
|
|
if l.Label != "" {
|
|
|
|
|
links = append(links, map[string]string{"label": l.Label})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(links) > 0 {
|
|
|
|
|
footer["links"] = links
|
|
|
|
|
}
|
|
|
|
|
footer["links"] = links
|
|
|
|
|
}
|
|
|
|
|
content["footer"] = footer
|
|
|
|
|
if len(footer) > 0 {
|
|
|
|
|
content["footer"] = footer
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SEO
|
|
|
|
|
if config.SEO.Title != "" || config.SEO.Description != "" {
|
|
|
|
|
content["seo"] = map[string]any{
|
|
|
|
|
"title": config.SEO.Title,
|
|
|
|
|
"description": config.SEO.Description,
|
|
|
|
|
seo := map[string]any{}
|
|
|
|
|
setIfNotEmpty(seo, "title", config.SEO.Title)
|
|
|
|
|
setIfNotEmpty(seo, "description", config.SEO.Description)
|
|
|
|
|
if len(seo) > 0 {
|
|
|
|
|
content["seo"] = seo
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|