feat(pages): add navigation label translation support
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m4s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m52s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m38s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m46s
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m4s
Build and Release / Unit Tests (push) Successful in 4m38s
Build and Release / Lint (push) Successful in 6m26s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 3m52s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 4m52s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 3m38s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m46s
Add translation support for navigation section labels (Value Props, Features, Pricing, Blog, Gallery, Compare, etc.). Adds TemplateDefaultLabels function that returns template-specific creative names (e.g., "Systems Analysis" for value props in Architecture Deep Dive). Auto-applies defaults when enabling pages or changing templates. Includes UI fields in languages settings and translation JSON serialization. Enables full localization of section headings.
This commit is contained in:
@@ -469,3 +469,65 @@ func TemplateDisplayNames() map[string]string {
|
|||||||
"architecture-deep-dive": "Architecture Deep Dive",
|
"architecture-deep-dive": "Architecture Deep Dive",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateDefaultLabels returns the template-specific default section labels.
|
||||||
|
// These are the creative names each template uses for its sections.
|
||||||
|
func TemplateDefaultLabels(template string) NavigationConfig {
|
||||||
|
switch template {
|
||||||
|
case "architecture-deep-dive":
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Systems Analysis",
|
||||||
|
LabelFeatures: "Technical Specifications",
|
||||||
|
LabelPricing: "Resource Allocation",
|
||||||
|
LabelBlog: "Dispatches",
|
||||||
|
LabelGallery: "Visual Index",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
case "bold-marketing":
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Why choose this",
|
||||||
|
LabelFeatures: "Capabilities",
|
||||||
|
LabelPricing: "Investment",
|
||||||
|
LabelBlog: "Blog",
|
||||||
|
LabelGallery: "Gallery",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
case "minimalist-docs":
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Why choose this",
|
||||||
|
LabelFeatures: "Capabilities",
|
||||||
|
LabelPricing: "Investment",
|
||||||
|
LabelBlog: "Blog",
|
||||||
|
LabelGallery: "Gallery",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
case "open-source-hero":
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Why choose us",
|
||||||
|
LabelFeatures: "Capabilities",
|
||||||
|
LabelPricing: "Pricing",
|
||||||
|
LabelBlog: "Blog",
|
||||||
|
LabelGallery: "Gallery",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
case "saas-conversion":
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Why",
|
||||||
|
LabelFeatures: "Features",
|
||||||
|
LabelPricing: "Pricing",
|
||||||
|
LabelBlog: "Blog",
|
||||||
|
LabelGallery: "Gallery",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// developer-tool, documentation-first, visual-showcase, cli-terminal
|
||||||
|
return NavigationConfig{
|
||||||
|
LabelValueProps: "Why choose us",
|
||||||
|
LabelFeatures: "Capabilities",
|
||||||
|
LabelPricing: "Pricing",
|
||||||
|
LabelBlog: "Blog",
|
||||||
|
LabelGallery: "Gallery",
|
||||||
|
LabelCompare: "Compare",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4614,6 +4614,8 @@
|
|||||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||||
"repo.settings.pages.trans_section_footer": "Footer",
|
"repo.settings.pages.trans_section_footer": "Footer",
|
||||||
"repo.settings.pages.trans_section_seo": "SEO",
|
"repo.settings.pages.trans_section_seo": "SEO",
|
||||||
|
"repo.settings.pages.trans_section_navigation": "Navigation Labels",
|
||||||
|
"repo.settings.pages.trans_nav_label": "Label",
|
||||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||||
"repo.settings.pages.trans_stat_value": "Value",
|
"repo.settings.pages.trans_stat_value": "Value",
|
||||||
|
|||||||
@@ -87,6 +87,31 @@ func savePagesLandingConfig(ctx *context.Context, config *pages_module.LandingCo
|
|||||||
return repo_model.UpdatePagesConfig(ctx, dbConfig)
|
return repo_model.UpdatePagesConfig(ctx, dbConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyTemplateDefaultLabels populates Navigation label fields with
|
||||||
|
// template-specific defaults. Existing non-empty labels are preserved.
|
||||||
|
func applyTemplateDefaultLabels(config *pages_module.LandingConfig) {
|
||||||
|
defaults := pages_module.TemplateDefaultLabels(config.Template)
|
||||||
|
nav := &config.Navigation
|
||||||
|
if nav.LabelValueProps == "" {
|
||||||
|
nav.LabelValueProps = defaults.LabelValueProps
|
||||||
|
}
|
||||||
|
if nav.LabelFeatures == "" {
|
||||||
|
nav.LabelFeatures = defaults.LabelFeatures
|
||||||
|
}
|
||||||
|
if nav.LabelPricing == "" {
|
||||||
|
nav.LabelPricing = defaults.LabelPricing
|
||||||
|
}
|
||||||
|
if nav.LabelBlog == "" {
|
||||||
|
nav.LabelBlog = defaults.LabelBlog
|
||||||
|
}
|
||||||
|
if nav.LabelGallery == "" {
|
||||||
|
nav.LabelGallery = defaults.LabelGallery
|
||||||
|
}
|
||||||
|
if nav.LabelCompare == "" {
|
||||||
|
nav.LabelCompare = defaults.LabelCompare
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// setCommonPagesData sets common data for all pages settings pages
|
// setCommonPagesData sets common data for all pages settings pages
|
||||||
func setCommonPagesData(ctx *context.Context) {
|
func setCommonPagesData(ctx *context.Context) {
|
||||||
config := getPagesLandingConfig(ctx)
|
config := getPagesLandingConfig(ctx)
|
||||||
@@ -141,6 +166,7 @@ func PagesPost(ctx *context.Context) {
|
|||||||
config := getPagesLandingConfig(ctx)
|
config := getPagesLandingConfig(ctx)
|
||||||
config.Enabled = true
|
config.Enabled = true
|
||||||
config.Template = template
|
config.Template = template
|
||||||
|
applyTemplateDefaultLabels(config)
|
||||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||||
ctx.ServerError("EnablePages", err)
|
ctx.ServerError("EnablePages", err)
|
||||||
return
|
return
|
||||||
@@ -161,6 +187,7 @@ func PagesPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
config := getPagesLandingConfig(ctx)
|
config := getPagesLandingConfig(ctx)
|
||||||
config.Template = template
|
config.Template = template
|
||||||
|
applyTemplateDefaultLabels(config)
|
||||||
if err := savePagesLandingConfig(ctx, config); err != nil {
|
if err := savePagesLandingConfig(ctx, config); err != nil {
|
||||||
ctx.ServerError("UpdateTemplate", err)
|
ctx.ServerError("UpdateTemplate", err)
|
||||||
return
|
return
|
||||||
@@ -766,6 +793,17 @@ type TranslationView struct {
|
|||||||
// SEO
|
// SEO
|
||||||
SEOTitle string
|
SEOTitle string
|
||||||
SEODescription string
|
SEODescription string
|
||||||
|
// Navigation labels
|
||||||
|
NavLabelValueProps string
|
||||||
|
NavLabelFeatures string
|
||||||
|
NavLabelPricing string
|
||||||
|
NavLabelBlog string
|
||||||
|
NavLabelGallery string
|
||||||
|
NavLabelCompare string
|
||||||
|
NavLabelDocs string
|
||||||
|
NavLabelReleases string
|
||||||
|
NavLabelAPI string
|
||||||
|
NavLabelIssues string
|
||||||
}
|
}
|
||||||
|
|
||||||
// overlayString extracts a string from a map
|
// overlayString extracts a string from a map
|
||||||
@@ -938,6 +976,20 @@ func parseTranslationView(t *pages_model.Translation, config *pages_module.Landi
|
|||||||
view.SEODescription = overlayString(seo, "description")
|
view.SEODescription = overlayString(seo, "description")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation labels
|
||||||
|
if nav, ok := overlay["navigation"].(map[string]any); ok {
|
||||||
|
view.NavLabelValueProps = overlayString(nav, "label_value_props")
|
||||||
|
view.NavLabelFeatures = overlayString(nav, "label_features")
|
||||||
|
view.NavLabelPricing = overlayString(nav, "label_pricing")
|
||||||
|
view.NavLabelBlog = overlayString(nav, "label_blog")
|
||||||
|
view.NavLabelGallery = overlayString(nav, "label_gallery")
|
||||||
|
view.NavLabelCompare = overlayString(nav, "label_compare")
|
||||||
|
view.NavLabelDocs = overlayString(nav, "label_docs")
|
||||||
|
view.NavLabelReleases = overlayString(nav, "label_releases")
|
||||||
|
view.NavLabelAPI = overlayString(nav, "label_api")
|
||||||
|
view.NavLabelIssues = overlayString(nav, "label_issues")
|
||||||
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1159,6 +1211,21 @@ func buildTranslationJSON(ctx *context.Context) string {
|
|||||||
overlay["seo"] = seo
|
overlay["seo"] = seo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation labels
|
||||||
|
nav := map[string]any{}
|
||||||
|
for _, key := range []string{
|
||||||
|
"label_value_props", "label_features", "label_pricing",
|
||||||
|
"label_blog", "label_gallery", "label_compare",
|
||||||
|
"label_docs", "label_releases", "label_api", "label_issues",
|
||||||
|
} {
|
||||||
|
if v := ctx.FormString("trans_nav_" + key); v != "" {
|
||||||
|
nav[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nav) > 0 {
|
||||||
|
overlay["navigation"] = nav
|
||||||
|
}
|
||||||
|
|
||||||
if len(overlay) == 0 {
|
if len(overlay) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,9 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigation labels (for translating nav items and section headers)
|
// Navigation labels (for translating nav items and section headers)
|
||||||
|
// Use template-specific defaults so AI translates the correct terms
|
||||||
|
// (e.g. "Systems Analysis" for architecture-deep-dive, not generic "Value Props")
|
||||||
|
defaults := pages_module.TemplateDefaultLabels(config.Template)
|
||||||
labelOrDefault := func(label, def string) string {
|
labelOrDefault := func(label, def string) string {
|
||||||
if label != "" {
|
if label != "" {
|
||||||
return label
|
return label
|
||||||
@@ -358,12 +361,12 @@ func buildTranslatableContent(config *pages_module.LandingConfig) string {
|
|||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
content["navigation"] = map[string]any{
|
content["navigation"] = map[string]any{
|
||||||
"label_value_props": labelOrDefault(config.Navigation.LabelValueProps, "Value Props"),
|
"label_value_props": labelOrDefault(config.Navigation.LabelValueProps, defaults.LabelValueProps),
|
||||||
"label_features": labelOrDefault(config.Navigation.LabelFeatures, "Features"),
|
"label_features": labelOrDefault(config.Navigation.LabelFeatures, defaults.LabelFeatures),
|
||||||
"label_pricing": labelOrDefault(config.Navigation.LabelPricing, "Pricing"),
|
"label_pricing": labelOrDefault(config.Navigation.LabelPricing, defaults.LabelPricing),
|
||||||
"label_blog": labelOrDefault(config.Navigation.LabelBlog, "Blog"),
|
"label_blog": labelOrDefault(config.Navigation.LabelBlog, defaults.LabelBlog),
|
||||||
"label_gallery": labelOrDefault(config.Navigation.LabelGallery, "Gallery"),
|
"label_gallery": labelOrDefault(config.Navigation.LabelGallery, defaults.LabelGallery),
|
||||||
"label_compare": labelOrDefault(config.Navigation.LabelCompare, "Compare"),
|
"label_compare": labelOrDefault(config.Navigation.LabelCompare, defaults.LabelCompare),
|
||||||
"label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"),
|
"label_docs": labelOrDefault(config.Navigation.LabelDocs, "Docs"),
|
||||||
"label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"),
|
"label_releases": labelOrDefault(config.Navigation.LabelReleases, "Releases"),
|
||||||
"label_api": labelOrDefault(config.Navigation.LabelAPI, "API"),
|
"label_api": labelOrDefault(config.Navigation.LabelAPI, "API"),
|
||||||
|
|||||||
@@ -238,7 +238,7 @@
|
|||||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_blog"}}</h5>
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_blog"}}</h5>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_headline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_headline"}}</label>
|
||||||
<input name="trans_blog_headline" value="{{if $trans}}{{$trans.BlogHeadline}}{{end}}" placeholder="{{$.Config.Blog.Headline}}">
|
<input name="trans_blog_headline" value="{{if $trans}}{{$trans.BlogHeadline}}{{end}}" placeholder="{{if $.Config.Blog.Headline}}{{$.Config.Blog.Headline}}{{else}}Latest Posts{{end}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_subheadline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_blog_subheadline"}}</label>
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_gallery"}}</h5>
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_gallery"}}</h5>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_headline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_headline"}}</label>
|
||||||
<input name="trans_gallery_headline" value="{{if $trans}}{{$trans.GalleryHeadline}}{{end}}" placeholder="{{$.Config.Gallery.Headline}}">
|
<input name="trans_gallery_headline" value="{{if $trans}}{{$trans.GalleryHeadline}}{{end}}" placeholder="{{if $.Config.Gallery.Headline}}{{$.Config.Gallery.Headline}}{{else}}Gallery{{end}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_subheadline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_gallery_subheadline"}}</label>
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_comparison"}}</h5>
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_comparison"}}</h5>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_headline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_headline"}}</label>
|
||||||
<input name="trans_comparison_headline" value="{{if $trans}}{{$trans.ComparisonHeadline}}{{end}}" placeholder="{{$.Config.Comparison.Headline}}">
|
<input name="trans_comparison_headline" value="{{if $trans}}{{$trans.ComparisonHeadline}}{{end}}" placeholder="{{if $.Config.Comparison.Headline}}{{$.Config.Comparison.Headline}}{{else}}How We Compare{{end}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_subheadline"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_comparison_subheadline"}}</label>
|
||||||
@@ -312,6 +312,49 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* ── Navigation Labels ── */}}
|
||||||
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.pages.trans_section_navigation"}}</h5>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelValueProps}}</em></label>
|
||||||
|
<input name="trans_nav_label_value_props" value="{{if $trans}}{{$trans.NavLabelValueProps}}{{end}}" placeholder="{{$.Config.Navigation.LabelValueProps}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelFeatures}}</em></label>
|
||||||
|
<input name="trans_nav_label_features" value="{{if $trans}}{{$trans.NavLabelFeatures}}{{end}}" placeholder="{{$.Config.Navigation.LabelFeatures}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelBlog}}</em></label>
|
||||||
|
<input name="trans_nav_label_blog" value="{{if $trans}}{{$trans.NavLabelBlog}}{{end}}" placeholder="{{$.Config.Navigation.LabelBlog}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelGallery}}</em></label>
|
||||||
|
<input name="trans_nav_label_gallery" value="{{if $trans}}{{$trans.NavLabelGallery}}{{end}}" placeholder="{{$.Config.Navigation.LabelGallery}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelPricing}}</em></label>
|
||||||
|
<input name="trans_nav_label_pricing" value="{{if $trans}}{{$trans.NavLabelPricing}}{{end}}" placeholder="{{$.Config.Navigation.LabelPricing}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{$.Config.Navigation.LabelCompare}}</em></label>
|
||||||
|
<input name="trans_nav_label_compare" value="{{if $trans}}{{$trans.NavLabelCompare}}{{end}}" placeholder="{{$.Config.Navigation.LabelCompare}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{if $.Config.Navigation.LabelDocs}}{{$.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}</em></label>
|
||||||
|
<input name="trans_nav_label_docs" value="{{if $trans}}{{$trans.NavLabelDocs}}{{end}}" placeholder="{{if $.Config.Navigation.LabelDocs}}{{$.Config.Navigation.LabelDocs}}{{else}}Docs{{end}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pages.trans_nav_label"}} — <em>{{if $.Config.Navigation.LabelReleases}}{{$.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}</em></label>
|
||||||
|
<input name="trans_nav_label_releases" value="{{if $trans}}{{$trans.NavLabelReleases}}{{end}}" placeholder="{{if $.Config.Navigation.LabelReleases}}{{$.Config.Navigation.LabelReleases}}{{else}}Releases{{end}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<button class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.pages.save_translation"}}</button>
|
<button class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.pages.save_translation"}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user