feat: i18n template infrastructure — render.sh reads _en vars, emits data-de/data-en

Phase 1 of i18n rollout:
- render.sh: i18n_attrs helper, reads *_en fields from site.yaml, emits
  data-de/data-en attributes on title, description, role, tagline, cta,
  tags, section titles, card titles/descriptions, bio, content
- base.html: i18n.js auto-included, title/description get i18n attrs
- All 6 templates: translatable elements get i18n attr placeholders,
  footer toggle button with machine-translation disclaimer
- ichbinotto.de pilot: added machine-translation disclaimer per m's request

Templated sites can now be translated by adding _en fields to site.yaml.
This commit is contained in:
m
2026-04-01 12:49:34 +02:00
parent 883904318e
commit 846fc04444
9 changed files with 101 additions and 24 deletions

View File

@@ -34,13 +34,42 @@ cta_text=$(yq -r '.vars.cta.text // ""' "$SITE_YAML")
cta_href=$(yq -r '.vars.cta.href // "#"' "$SITE_YAML") cta_href=$(yq -r '.vars.cta.href // "#"' "$SITE_YAML")
year=$(date +%Y) year=$(date +%Y)
# i18n: helper to generate data-de/data-en attribute string
i18n_attrs() {
local de="$1" en="$2"
if [ -n "$en" ] && [ "$en" != "null" ]; then
de=$(echo "$de" | sed 's/"/\"/g')
en=$(echo "$en" | sed 's/"/\"/g')
echo " data-de=\"${de}\" data-en=\"${en}\""
fi
}
# i18n: read English overrides
title_en=$(yq -r '.title_en // ""' "$SITE_YAML")
description_en=$(yq -r '.description_en // ""' "$SITE_YAML")
role_en=$(yq -r '.vars.role_en // ""' "$SITE_YAML")
tagline_en=$(yq -r '.vars.tagline_en // ""' "$SITE_YAML")
cta_text_en=$(yq -r '.vars.cta.text_en // ""' "$SITE_YAML")
# i18n: build attribute strings for simple fields
title_i18n=$(i18n_attrs "$title" "$title_en")
description_i18n=$(i18n_attrs "$description" "$description_en")
role_i18n=$(i18n_attrs "$role" "$role_en")
tagline_i18n=$(i18n_attrs "$tagline" "$tagline_en")
cta_i18n=$(i18n_attrs "$cta_text" "$cta_text_en")
# Build tags HTML # Build tags HTML
tags_html="" tags_html=""
tag_count=$(yq -r '.vars.tags | length' "$SITE_YAML" 2>/dev/null || echo "0") tag_count=$(yq -r '.vars.tags | length' "$SITE_YAML" 2>/dev/null || echo "0")
if [ "$tag_count" -gt 0 ]; then if [ "$tag_count" -gt 0 ]; then
for i in $(seq 0 $((tag_count - 1))); do for i in $(seq 0 $((tag_count - 1))); do
tag=$(yq -r ".vars.tags[$i]" "$SITE_YAML") tag=$(yq -r ".vars.tags[$i]" "$SITE_YAML")
tags_html="${tags_html} <span class=\"tag\">${tag}</span>\n" tag_en=$(yq -r ".vars.tags_en[$i] // \"\"" "$SITE_YAML" 2>/dev/null || echo "")
tag_i18n=""
if [ -n "$tag_en" ] && [ "$tag_en" != "null" ]; then
tag_i18n="$(i18n_attrs "$tag" "$tag_en")"
fi
tags_html="${tags_html} <span class=\"tag\"${tag_i18n}>${tag}</span>\n"
done done
fi fi
@@ -53,17 +82,25 @@ if [ "$section_count" -gt 0 ]; then
sec_title=$(yq -r ".vars.sections[$i].title // \"\"" "$SITE_YAML") sec_title=$(yq -r ".vars.sections[$i].title // \"\"" "$SITE_YAML")
if [ "$sec_type" = "features" ]; then if [ "$sec_type" = "features" ]; then
sec_title_en=$(yq -r ".vars.sections[$i].title_en // \"\"" "$SITE_YAML")
sections_html="${sections_html} <div class=\"section fade-in stagger-$((i+2))\">\n" sections_html="${sections_html} <div class=\"section fade-in stagger-$((i+2))\">\n"
[ -n "$sec_title" ] && sections_html="${sections_html} <h2>${sec_title}</h2>\n" if [ -n "$sec_title" ]; then
sec_title_i18n=$(i18n_attrs "$sec_title" "$sec_title_en")
sections_html="${sections_html} <h2${sec_title_i18n}>${sec_title}</h2>\n"
fi
sections_html="${sections_html} <div class=\"grid\">\n" sections_html="${sections_html} <div class=\"grid\">\n"
item_count=$(yq -r ".vars.sections[$i].items | length" "$SITE_YAML" 2>/dev/null || echo "0") item_count=$(yq -r ".vars.sections[$i].items | length" "$SITE_YAML" 2>/dev/null || echo "0")
for j in $(seq 0 $((item_count - 1))); do for j in $(seq 0 $((item_count - 1))); do
item_title=$(yq -r ".vars.sections[$i].items[$j].title" "$SITE_YAML") item_title=$(yq -r ".vars.sections[$i].items[$j].title" "$SITE_YAML")
item_desc=$(yq -r ".vars.sections[$i].items[$j].desc // \"\"" "$SITE_YAML") item_desc=$(yq -r ".vars.sections[$i].items[$j].desc // \"\"" "$SITE_YAML")
item_title_en=$(yq -r ".vars.sections[$i].items[$j].title_en // \"\"" "$SITE_YAML")
item_desc_en=$(yq -r ".vars.sections[$i].items[$j].desc_en // \"\"" "$SITE_YAML")
item_title_i18n=$(i18n_attrs "$item_title" "$item_title_en")
item_desc_i18n=$(i18n_attrs "$item_desc" "$item_desc_en")
sections_html="${sections_html} <div class=\"card\">\n" sections_html="${sections_html} <div class=\"card\">\n"
sections_html="${sections_html} <h3>${item_title}</h3>\n" sections_html="${sections_html} <h3${item_title_i18n}>${item_title}</h3>\n"
[ -n "$item_desc" ] && sections_html="${sections_html} <p>${item_desc}</p>\n" [ -n "$item_desc" ] && sections_html="${sections_html} <p${item_desc_i18n}>${item_desc}</p>\n"
sections_html="${sections_html} </div>\n" sections_html="${sections_html} </div>\n"
done done
@@ -71,9 +108,15 @@ if [ "$section_count" -gt 0 ]; then
elif [ "$sec_type" = "profile" ]; then elif [ "$sec_type" = "profile" ]; then
bio=$(yq -r ".vars.sections[$i].bio // \"\"" "$SITE_YAML") bio=$(yq -r ".vars.sections[$i].bio // \"\"" "$SITE_YAML")
bio_en=$(yq -r ".vars.sections[$i].bio_en // \"\"" "$SITE_YAML")
sec_title_en=$(yq -r ".vars.sections[$i].title_en // \"\"" "$SITE_YAML")
sections_html="${sections_html} <div class=\"section fade-in stagger-$((i+2))\">\n" sections_html="${sections_html} <div class=\"section fade-in stagger-$((i+2))\">\n"
[ -n "$sec_title" ] && sections_html="${sections_html} <h2>${sec_title}</h2>\n" if [ -n "$sec_title" ]; then
sections_html="${sections_html} <p class=\"bio\">${bio}</p>\n" sec_title_i18n=$(i18n_attrs "$sec_title" "$sec_title_en")
sections_html="${sections_html} <h2${sec_title_i18n}>${sec_title}</h2>\n"
fi
bio_i18n=$(i18n_attrs "$bio" "$bio_en")
sections_html="${sections_html} <p class=\"bio\"${bio_i18n}>${bio}</p>\n"
sections_html="${sections_html} </div>\n" sections_html="${sections_html} </div>\n"
fi fi
done done
@@ -82,9 +125,15 @@ fi
# Build content HTML (for editorial template) # Build content HTML (for editorial template)
content_html="" content_html=""
content=$(yq -r '.vars.content // ""' "$SITE_YAML") content=$(yq -r '.vars.content // ""' "$SITE_YAML")
content_i18n=""
if [ -n "$content" ] && [ "$content" != "null" ]; then if [ -n "$content" ] && [ "$content" != "null" ]; then
# Convert newlines to paragraphs # Convert newlines to paragraphs
content_html=$(echo "$content" | sed 's/^$/<\/p><p>/g' | sed '1s/^/<p>/' | sed '$s/$/<\/p>/') content_html=$(echo "$content" | sed 's/^$/<\/p><p>/g' | sed '1s/^/<p>/' | sed '$s/$/<\/p>/')
content_en=$(yq -r '.vars.content_en // ""' "$SITE_YAML")
if [ -n "$content_en" ] && [ "$content_en" != "null" ]; then
content_html_en=$(echo "$content_en" | sed 's/^$/<\/p><p>/g' | sed '1s/^/<p>/' | sed '$s/$/<\/p>/')
content_i18n=$(i18n_attrs "$content_html" "$content_html_en")
fi
fi fi
# Read template file and extract CSS/body sections # Read template file and extract CSS/body sections
@@ -184,7 +233,13 @@ BEGIN {
-e "s|{{cta_text}}|${cta_text}|g" \ -e "s|{{cta_text}}|${cta_text}|g" \
-e "s|{{cta_href}}|${cta_href}|g" \ -e "s|{{cta_href}}|${cta_href}|g" \
-e "s|{{year}}|${year}|g" \ -e "s|{{year}}|${year}|g" \
-e "s|{{domain}}|${domain}|g" | \ -e "s|{{domain}}|${domain}|g" \
-e "s|{{title_i18n}}|$(echo "$title_i18n" | sed 's/[&/\]/\\&/g')|g" \
-e "s|{{description_i18n}}|$(echo "$description_i18n" | sed 's/[&/\]/\\&/g')|g" \
-e "s|{{role_i18n}}|$(echo "$role_i18n" | sed 's/[&/\]/\\&/g')|g" \
-e "s|{{tagline_i18n}}|$(echo "$tagline_i18n" | sed 's/[&/\]/\\&/g')|g" \
-e "s|{{cta_i18n}}|$(echo "$cta_i18n" | sed 's/[&/\]/\\&/g')|g" \
-e "s|{{content_i18n}}|$(echo "$content_i18n" | sed 's/[&/\]/\\&/g')|g" | \
sed "s|{{tags_html}}|$(printf '%b' "$tags_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g" | \ sed "s|{{tags_html}}|$(printf '%b' "$tags_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g" | \
sed "s|{{sections_html}}|$(printf '%b' "$sections_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g" | \ sed "s|{{sections_html}}|$(printf '%b' "$sections_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g" | \
sed "s|{{content_html}}|$(printf '%b' "$content_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g" sed "s|{{content_html}}|$(printf '%b' "$content_html" | sed 's/[&/\]/\\&/g; s/$/\\/' | sed '$ s/\\$//')|g"

View File

@@ -435,7 +435,8 @@
<div class="container"> <div class="container">
<p data-de='<span class="otto">Otto</span> — mai-otto.de — ein Projekt von <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a>' <p data-de='<span class="otto">Otto</span> — mai-otto.de — ein Projekt von <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a>'
data-en='<span class="otto">Otto</span> — mai-otto.de — a project by <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a>'><span class="otto">Otto</span> — mai-otto.de — ein Projekt von <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a></p> data-en='<span class="otto">Otto</span> — mai-otto.de — a project by <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a>'><span class="otto">Otto</span> — mai-otto.de — ein Projekt von <a href="https://msbls.de" style="color:var(--text-muted);text-decoration:none;">msbls.de</a></p>
<button data-i18n-toggle style="background:none;border:1px solid var(--border);color:var(--text-muted);font-family:'Space Grotesk',sans-serif;font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;transition:color 0.2s,border-color 0.2s;" onmouseover="this.style.color='var(--text)';this.style.borderColor='var(--text-muted)'" onmouseout="this.style.color='var(--text-muted)';this.style.borderColor='var(--border)'">EN</button> <button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid var(--border);color:var(--text-muted);font-family:'Space Grotesk',sans-serif;font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;transition:color 0.2s,border-color 0.2s;" onmouseover="this.style.color='var(--text)';this.style.borderColor='var(--text-muted)'" onmouseout="this.style.color='var(--text-muted)';this.style.borderColor='var(--border)'">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-muted);font-family:'Space Grotesk',sans-serif;font-size:0.6rem;letter-spacing:0.05em;opacity:0.5;">Maschinell übersetzt</small>
</div> </div>
</footer> </footer>

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title> <title{{title_i18n}}>{{title}}</title>
<meta name="description" content="{{description}}"> <meta name="description" content="{{description}}"{{description_i18n}}>
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>{{favicon}}</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>{{favicon}}</text></svg>">
{{fonts}} {{fonts}}
@@ -18,5 +18,6 @@
</head> </head>
<body class="noise-overlay"> <body class="noise-overlay">
{{body}} {{body}}
<script src="/shared/i18n.js"></script>
</body> </body>
</html> </html>

View File

@@ -70,14 +70,17 @@
{{template_body_start}} {{template_body_start}}
<div class="container fade-in"> <div class="container fade-in">
<div class="editorial-header"> <div class="editorial-header">
<h1>{{title}}</h1> <h1{{title_i18n}}>{{title}}</h1>
<div class="subtitle">{{tagline}}</div> <div class="subtitle"{{tagline_i18n}}>{{tagline}}</div>
</div> </div>
<div class="editorial-body"> <div class="editorial-body"{{content_i18n}}>
{{content_html}} {{content_html}}
</div> </div>
<div class="footer"> <div class="footer">
{{name}} &mdash; {{year}} {{name}} &mdash; {{year}}
<br>
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div> </div>
</div> </div>
{{template_body_end}} {{template_body_end}}

View File

@@ -56,8 +56,12 @@
{{template_body_start}} {{template_body_start}}
<div class="container fade-in"> <div class="container fade-in">
<div class="emoji-big">{{emoji}}</div> <div class="emoji-big">{{emoji}}</div>
<h1>{{title}}</h1> <h1{{title_i18n}}>{{title}}</h1>
<div class="subtitle">{{tagline}}</div> <div class="subtitle"{{tagline_i18n}}>{{tagline}}</div>
{{sections_html}} {{sections_html}}
<div style="text-align:center;margin-top:2rem;">
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div>
</div> </div>
{{template_body_end}} {{template_body_end}}

View File

@@ -30,7 +30,11 @@
{{template_css_end}} {{template_css_end}}
{{template_body_start}} {{template_body_start}}
<div class="container fade-in"> <div class="container fade-in">
<h1>{{title}}</h1> <h1{{title_i18n}}>{{title}}</h1>
<div class="subtitle">{{tagline}}</div> <div class="subtitle"{{tagline_i18n}}>{{tagline}}</div>
<div style="margin-top:2rem;">
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div>
</div> </div>
{{template_body_end}} {{template_body_end}}

View File

@@ -139,8 +139,8 @@
<div class="hero fade-in"> <div class="hero fade-in">
<div class="initials">{{initials}}</div> <div class="initials">{{initials}}</div>
<h1>{{name}}</h1> <h1>{{name}}</h1>
<div class="role">{{role}}</div> <div class="role"{{role_i18n}}>{{role}}</div>
<div class="tagline">{{tagline}}</div> <div class="tagline"{{tagline_i18n}}>{{tagline}}</div>
</div> </div>
<div class="tags fade-in stagger-1"> <div class="tags fade-in stagger-1">
@@ -151,6 +151,9 @@
<div class="footer fade-in stagger-5"> <div class="footer fade-in stagger-5">
&copy; {{year}} {{name}} &copy; {{year}} {{name}}
<br>
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div> </div>
</div> </div>
{{template_body_end}} {{template_body_end}}

View File

@@ -141,8 +141,8 @@
<div class="hero fade-in"> <div class="hero fade-in">
<div class="initials">{{initials}}</div> <div class="initials">{{initials}}</div>
<h1>{{name}}</h1> <h1>{{name}}</h1>
<div class="role">{{role}}</div> <div class="role"{{role_i18n}}>{{role}}</div>
<div class="tagline">{{tagline}}</div> <div class="tagline"{{tagline_i18n}}>{{tagline}}</div>
</div> </div>
<div class="tags fade-in stagger-1"> <div class="tags fade-in stagger-1">
@@ -153,6 +153,9 @@
<div class="footer fade-in stagger-5"> <div class="footer fade-in stagger-5">
&copy; {{year}} {{name}} &copy; {{year}} {{name}}
<br>
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div> </div>
</div> </div>
{{template_body_end}} {{template_body_end}}

View File

@@ -95,18 +95,21 @@
{{template_body_start}} {{template_body_start}}
<div class="container"> <div class="container">
<div class="hero fade-in"> <div class="hero fade-in">
<h1>{{title}}</h1> <h1{{title_i18n}}>{{title}}</h1>
<div class="subtitle">{{tagline}}</div> <div class="subtitle"{{tagline_i18n}}>{{tagline}}</div>
</div> </div>
{{sections_html}} {{sections_html}}
<div class="cta-section fade-in stagger-4"> <div class="cta-section fade-in stagger-4">
<a href="{{cta_href}}" class="cta">{{cta_text}}</a> <a href="{{cta_href}}" class="cta"{{cta_i18n}}>{{cta_text}}</a>
</div> </div>
<div class="footer fade-in stagger-5"> <div class="footer fade-in stagger-5">
&copy; {{year}} {{name}} &copy; {{year}} {{name}}
<br>
<button data-i18n-toggle title="Maschinell übersetzt / Machine-translated — German is the original." style="background:none;border:1px solid currentColor;color:var(--text-dimmed,#888);font-size:0.65rem;letter-spacing:0.1em;padding:4px 12px;border-radius:4px;cursor:pointer;margin-top:12px;opacity:0.6;">EN</button>
<br><small data-de="Maschinell übersetzt" data-en="Machine-translated" style="color:var(--text-dimmed,#888);font-size:0.6rem;opacity:0.5;">Maschinell übersetzt</small>
</div> </div>
</div> </div>
{{template_body_end}} {{template_body_end}}