Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

misc: ac-desktop autodeploy, slides keymap polish, lacma pdf rev, web menuband i18n

- ac-electron/ac-desktop: new `autodeploy` subcommand that probes knot SSH on an interval, then runs push → publish → install once the network cooperates (airline-wifi resilience).
- slides/notepat-keymap/template.html: PALS watermark composited ON TOP of the Jeffrey portrait in the corner; QR row goes side-by-side with implementations instead of stacked; smaller QRs to fit the new row.
- grants/lacma-2026/lacma-2026.pdf: revised draft.
- system/public/menuband/index.html: EN/ES language-switch chip pair (rounded glass capsule, top-right) — companion to the slab/menuband Localization scaffold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+317 -52
+63
ac-electron/ac-desktop
··· 15 15 # ac-desktop open Launch the installed app 16 16 # ac-desktop kill Quit running instances 17 17 # ac-desktop icons Regenerate tray + app icons 18 + # ac-desktop autodeploy [--interval=SECS] Retry push + publish + install until network cooperates 18 19 # ac-desktop doctor Check signing creds, electron, dist, installed app 19 20 # 20 21 # Subcommands delegate to existing scripts (dev.fish, install.fish, release.fish, ··· 291 292 ok "Icons rebuilt under build/icons/" 292 293 } 293 294 295 + cmd_autodeploy() { 296 + require_mac 297 + cd_repo 298 + local interval=120 299 + for arg in "$@"; do 300 + case "$arg" in 301 + --interval=*) interval="${arg#--interval=}" ;; 302 + esac 303 + done 304 + hdr "AUTODEPLOY — retry push + publish + install until reachable" 305 + log "Probe interval: ${interval}s. Ctrl-C to stop." 306 + log "Will: 1) git push origin main 2) ac-desktop publish 3) ac-desktop install" 307 + echo "" 308 + 309 + # Step 1: push to knot. We don't merge/force — just retry until knot's 310 + # SSH is reachable. (knot.aesthetic.computer:22 is what airline wifi 311 + # typically blocks.) 312 + while true; do 313 + log "[1/3] Probing knot SSH…" 314 + if timeout 15 ssh -o BatchMode=yes -o ConnectTimeout=10 \ 315 + git@knot.aesthetic.computer 2>&1 \ 316 + | grep -qE "(does not provide shell access|Welcome|gitea|git)"; then 317 + ok "knot reachable" 318 + break 319 + fi 320 + # Some hosts hang up cleanly with no banner — accept that too if SSH exited 0. 321 + timeout 15 ssh -o BatchMode=yes -o ConnectTimeout=10 \ 322 + git@knot.aesthetic.computer true >/dev/null 2>&1 && { ok "knot reachable (silent)"; break; } 323 + warn "knot not reachable yet — sleeping ${interval}s" 324 + sleep "${interval}" 325 + done 326 + 327 + log "Pushing to origin/main…" 328 + if ! git push origin main; then 329 + err "push failed — bailing. Resolve manually then rerun: ac-desktop autodeploy" 330 + exit 1 331 + fi 332 + ok "pushed" 333 + 334 + # Step 2: publish (signs + notarizes + uploads). Will fail without 335 + # internet to Apple TSA / notary service / releases bucket — retry the 336 + # whole thing if so. 337 + while true; do 338 + log "[2/3] Running publish…" 339 + if PYTHON=/opt/homebrew/bin/python3.12 npm_config_python=/opt/homebrew/bin/python3.12 \ 340 + npm run publish:mac; then 341 + ok "published" 342 + break 343 + fi 344 + warn "publish failed (likely network) — sleeping ${interval}s and retrying" 345 + sleep "${interval}" 346 + done 347 + 348 + # Step 3: install the now-published version. 349 + log "[3/3] Installing published version locally…" 350 + fish ./install.fish --force || warn "install step failed — try manually: ac-desktop install --force" 351 + 352 + ok "autodeploy complete" 353 + cmd_status 354 + } 355 + 294 356 cmd_doctor() { 295 357 cd_repo 296 358 hdr "DOCTOR" ··· 338 400 open|launch) cmd_open "$@" ;; 339 401 kill|stop|quit) cmd_kill "$@" ;; 340 402 icons) cmd_icons "$@" ;; 403 + autodeploy|adeploy) cmd_autodeploy "$@" ;; 341 404 doctor|check) cmd_doctor "$@" ;; 342 405 *) 343 406 err "Unknown subcommand: ${CMD}"
grants/lacma-2026/lacma-2026.pdf

This is a binary file and will not be displayed.

+38 -31
slides/notepat-keymap/template.html
··· 189 189 190 190 .below-bottom { 191 191 display: flex; 192 - flex-direction: column; 193 - gap: 14px; 192 + flex-direction: row; 193 + align-items: flex-start; 194 + gap: 28px; 194 195 margin-top: 18px; 195 196 border-top: 2px solid #ddd; 196 197 padding-top: 16px; 197 198 } 198 - .below-bottom .row { 199 - display: flex; 200 - align-items: center; 201 - gap: 24px; 202 - } 203 - .impls { flex: 1; } 199 + .impls { flex: 1; min-width: 0; } 204 200 .impls .impl-list { 205 201 list-style: none; 206 202 padding: 0; margin: 0; ··· 229 225 letter-spacing: 0.02em; 230 226 } 231 227 232 - .qrs { display: flex; gap: 22px; } 228 + .qrs { display: flex; gap: 22px; flex-shrink: 0; } 233 229 .qrs .qr { display: flex; flex-direction: column; align-items: center; gap: 7px; } 234 - .qrs .qr img { width: 180px; height: 180px; image-rendering: pixelated; border: 1px solid #ddd; border-radius: 8px; background: #fff; } 235 - .qrs .qr .label { font-family: "SF Mono", Menlo, monospace; font-size: 17px; color: #111; font-weight: 700; } 230 + .qrs .qr img { width: 150px; height: 150px; image-rendering: pixelated; border: 1px solid #ddd; border-radius: 8px; background: #fff; } 231 + .qrs .qr .label { font-family: "SF Mono", Menlo, monospace; font-size: 15px; color: #111; font-weight: 700; } 236 232 237 - /* PALS watermark + Jeffrey portrait + brand. Stacked images in the 238 - top-right corner, text label to their left. */ 233 + /* PALS watermark composited ON TOP of the Jeffrey portrait, in the 234 + top-right corner with text label to their left. The portrait sits 235 + behind so pals reads as a stamped mark over jeffrey's face. */ 239 236 .brand-block { 240 237 position: absolute; 241 238 top: 18px; ··· 243 240 display: flex; align-items: flex-start; gap: 20px; 244 241 } 245 242 .brand-block .marks { 246 - display: flex; flex-direction: column; gap: 4px; align-items: center; 243 + position: relative; 244 + width: 160px; height: 160px; 247 245 } 248 - .brand-block .marks #pals { width: 124px; height: 124px; opacity: 0.95; } 249 246 .brand-block .marks #jeffrey { 250 - width: 124px; height: 124px; 251 - border-radius: 6px; 247 + position: absolute; 248 + inset: 0; 249 + width: 160px; height: 160px; 250 + border-radius: 8px; 252 251 object-fit: cover; 253 252 object-position: center top; 254 253 border: 2px solid #ddd; 255 - /* Pull up into the pals SVG's empty padding so the two marks 256 - visually stack flush. */ 257 - margin-top: -22px; 254 + z-index: 1; 255 + } 256 + .brand-block .marks #pals { 257 + position: absolute; 258 + left: 50%; 259 + top: 50%; 260 + transform: translate(-50%, -50%); 261 + width: 150px; height: 150px; 262 + opacity: 0.95; 263 + z-index: 2; 264 + /* white halo so the silhouette stays legible against the photo */ 265 + filter: drop-shadow(0 0 6px rgba(255,255,255,0.95)) 266 + drop-shadow(0 0 2px rgba(255,255,255,1)); 258 267 } 259 268 .brand-block .brand-text { 260 269 display: flex; flex-direction: column; gap: 6px; ··· 288 297 <div class="author">by <b>@jeffrey</b></div> 289 298 </div> 290 299 <div class="marks"> 291 - <img id="pals" alt=""> 292 300 <img id="jeffrey" alt="@jeffrey"> 301 + <img id="pals" alt=""> 293 302 </div> 294 303 </div> 295 304 </div> ··· 405 414 <li><b>Remote</b><span class="where">phone</span></li> 406 415 </ul> 407 416 </div> 408 - <div class="row"> 409 - <div class="qrs"> 410 - <div class="qr"> 411 - <img id="qr1" alt="notepat.com"> 412 - <div class="label">https://notepat.com</div> 413 - </div> 414 - <div class="qr"> 415 - <img id="qr2" alt="prompt.ac/menuband"> 416 - <div class="label">https://prompt.ac/menuband</div> 417 - </div> 417 + <div class="qrs"> 418 + <div class="qr"> 419 + <img id="qr1" alt="notepat.com"> 420 + <div class="label">notepat.com</div> 421 + </div> 422 + <div class="qr"> 423 + <img id="qr2" alt="prompt.ac/menuband"> 424 + <div class="label">prompt.ac/menuband</div> 418 425 </div> 419 426 </div> 420 427 </div>
+216 -21
system/public/menuband/index.html
··· 556 556 box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25) inset; 557 557 } 558 558 } 559 + 560 + /* Language switcher — corner chip pair, same vibe as kidlisp.com / 561 + help.aesthetic.computer. Tucks into the top-right so it doesn't fight 562 + the hero icon for attention. Active language is filled, the other is 563 + a flat outline; both share the precise-cursor hand on hover. */ 564 + #lang-switch { 565 + position: fixed; 566 + top: 12px; 567 + right: 16px; 568 + z-index: 4; 569 + display: inline-flex; 570 + gap: 4px; 571 + padding: 4px; 572 + border-radius: 999px; 573 + background: rgba(255, 255, 255, 0.55); 574 + backdrop-filter: blur(10px); 575 + -webkit-backdrop-filter: blur(10px); 576 + box-shadow: 577 + 0 1px 0 rgba(255, 255, 255, 0.7) inset, 578 + 0 1px 4px rgba(15, 30, 60, 0.08); 579 + } 580 + #lang-switch button { 581 + appearance: none; 582 + border: 0; 583 + background: transparent; 584 + color: var(--ink-soft); 585 + font: inherit; 586 + font-size: 12px; 587 + font-weight: 600; 588 + line-height: 1; 589 + padding: 6px 10px; 590 + border-radius: 999px; 591 + cursor: pointer; 592 + display: inline-flex; 593 + align-items: center; 594 + gap: 6px; 595 + transition: background 120ms var(--ease-apple), color 120ms var(--ease-apple); 596 + } 597 + #lang-switch button:hover { color: var(--ink-head); } 598 + #lang-switch button[aria-pressed="true"] { 599 + background: var(--tahoe-blue); 600 + color: #fff; 601 + box-shadow: 0 1px 2px rgba(10, 132, 255, 0.35); 602 + } 603 + #lang-switch button .flag { font-size: 13px; } 604 + @media (max-width: 520px) { 605 + #lang-switch button .label { display: none; } 606 + #lang-switch button { padding: 6px 8px; } 607 + } 608 + @media (prefers-color-scheme: dark) { 609 + #lang-switch { 610 + background: rgba(28, 28, 30, 0.65); 611 + box-shadow: 612 + 0 1px 0 rgba(255, 255, 255, 0.06) inset, 613 + 0 1px 6px rgba(0, 0, 0, 0.4); 614 + } 615 + #lang-switch button { color: rgba(235, 235, 245, 0.6); } 616 + #lang-switch button:hover { color: rgba(235, 235, 245, 0.95); } 617 + } 559 618 </style> 560 619 </head> 561 620 <body> 562 621 563 622 <h1 id="corner-label">menuband</h1> 564 623 624 + <div id="lang-switch" role="group" aria-label="Language"> 625 + <button type="button" data-lang="en" aria-pressed="true"> 626 + <span class="flag" aria-hidden="true">🇺🇸</span><span class="label">EN</span> 627 + </button> 628 + <button type="button" data-lang="es" aria-pressed="false"> 629 + <span class="flag" aria-hidden="true">🇪🇸</span><span class="label">ES</span> 630 + </button> 631 + </div> 632 + 565 633 <article class="window" aria-label="Menu Band"> 566 634 <div class="hero"> 567 635 <div class="icon-stage"> 568 636 <img class="app-icon" src="/menuband/icon.png" alt="Menu Band app icon"> 569 637 </div> 570 638 <h2 class="app-title">Menu Band</h2> 571 - <p class="tagline">Taking macOS' standard instruments out of the 🎸 Garage and kickin' it on the curb!</p> 639 + <p class="tagline" data-i18n="tagline">Taking macOS' standard instruments out of the 🎸 Garage and kickin' it on the curb!</p> 572 640 573 641 <div class="button-row"> 574 642 <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.9.dmg" download> 575 - Download 576 - <small>0.9 · Apple Silicon · 2.1 MB</small> 643 + <span data-i18n="downloadCta">Download</span> 644 + <small data-i18n="downloadMeta">0.9 · Apple Silicon · 2.1 MB</small> 577 645 </a> 578 646 </div> 579 - <p class="aux"><a href="https://tangled.org/@aesthetic.computer/core/tree/main/slab/menuband">view source</a> · by <a href="https://aesthetic.computer">aesthetic.computer</a></p> 647 + <p class="aux"><a href="https://tangled.org/@aesthetic.computer/core/tree/main/slab/menuband" data-i18n="viewSource">view source</a> · <span data-i18n="by">by</span> <a href="https://aesthetic.computer">aesthetic.computer</a></p> 580 648 </div> 581 649 582 650 <section class="panel"> 583 - <p>A tiny piano lives in the right side of your menu bar. Click the keys, type letters, or pipe notes to your DAW over MIDI — Menu Band publishes a virtual MIDI source named <code>Menu Band</code> that any DAW can pick as input. When nothing's listening, a built-in General MIDI synth covers hundreds of patches.</p> 651 + <p data-i18n="lede" data-i18n-html="true">A tiny piano lives in the right side of your menu bar. Click the keys, type letters, or pipe notes to your DAW over MIDI — Menu Band publishes a virtual MIDI source named <code>Menu Band</code> that any DAW can pick as input. When nothing's listening, a built-in General MIDI synth covers hundreds of patches.</p> 584 652 </section> 585 653 586 654 <section class="panel testimonial"> 587 - <blockquote>“i use it on my macbook neo. practice notepat, play ableton without leaving my menu bar.”</blockquote> 655 + <blockquote data-i18n="testimonial">“i use it on my macbook neo. practice notepat, play ableton without leaving my menu bar.”</blockquote> 588 656 <p class="attribution">— <a href="https://aesthetic.computer/@jeffrey">@jeffrey</a></p> 589 657 </section> 590 658 591 659 <section class="panel"> 592 - <h3>Three modes</h3> 660 + <h3 data-i18n="modesTitle">Three modes</h3> 593 661 <ul class="modes"> 594 - <li><b>Pointer</b><span>Mouse only. Two octaves.</span></li> 595 - <li><b>Notepat</b><span>Type to play. Two octaves.</span></li> 596 - <li><b>Ableton</b><span>Type to play. Live's M-mode layout.</span></li> 662 + <li><b data-i18n="modePointer">Pointer</b><span data-i18n="modePointerBody">Mouse only. Two octaves.</span></li> 663 + <li><b data-i18n="modeNotepat">Notepat</b><span data-i18n="modeNotepatBody">Type to play. Two octaves.</span></li> 664 + <li><b data-i18n="modeAbleton">Ableton</b><span data-i18n="modeAbletonBody">Type to play. Live's M-mode layout.</span></li> 597 665 </ul> 598 666 </section> 599 667 600 668 <section class="panel"> 601 - <h3>What's new <span class="badge">0.5</span></h3> 602 - <p><b>Snappier.</b> Visualizer pauses when the popover is hidden, and the Metal layer no longer waits for vsync — the popover opens instantly and bars track audio with less latency. Big thanks to <a href="https://github.com/estebanuribe">Esteban Uribe</a> for both performance patches.</p> 603 - <p><b>No Accessibility prompt.</b> Click the menubar piano and the keys flash their letters; type to play. No system-wide keystroke capture, no permission dialog. (The Notepat / Ableton modes still use global capture for play-while-using-other-apps; that's an opt-in toggle.)</p> 604 - <p><b>Plus:</b> a mute toggle, an accent-tinted Finder icon that follows your system color, and the popover keeps the same width when MIDI flips on.</p> 669 + <h3><span data-i18n="whatsNewTitle">What's new</span> <span class="badge">0.5</span></h3> 670 + <p data-i18n="whatsNew1" data-i18n-html="true"><b>Snappier.</b> Visualizer pauses when the popover is hidden, and the Metal layer no longer waits for vsync — the popover opens instantly and bars track audio with less latency. Big thanks to <a href="https://github.com/estebanuribe">Esteban Uribe</a> for both performance patches.</p> 671 + <p data-i18n="whatsNew2" data-i18n-html="true"><b>No Accessibility prompt.</b> Click the menubar piano and the keys flash their letters; type to play. No system-wide keystroke capture, no permission dialog. (The Notepat / Ableton modes still use global capture for play-while-using-other-apps; that's an opt-in toggle.)</p> 672 + <p data-i18n="whatsNew3" data-i18n-html="true"><b>Plus:</b> a mute toggle, an accent-tinted Finder icon that follows your system color, and the popover keeps the same width when MIDI flips on.</p> 605 673 </section> 606 674 607 675 <section class="panel"> 608 - <h3>Recent changes <span id="recent-deployed" class="commit-chip" hidden></span></h3> 676 + <h3><span data-i18n="recentTitle">Recent changes</span> <span id="recent-deployed" class="commit-chip" hidden></span></h3> 609 677 <ul id="recent-commits" class="commits"> 610 - <li class="commits-empty">loading…</li> 678 + <li class="commits-empty" data-i18n="loading">loading…</li> 611 679 </ul> 612 680 </section> 613 681 614 682 <section class="panel"> 615 - <h3>Requirements</h3> 616 - <p>macOS 11+ · Apple Silicon · ⌃⌥⌘P toggles typing mode.</p> 683 + <h3 data-i18n="requirementsTitle">Requirements</h3> 684 + <p data-i18n="requirementsBody">macOS 11+ · Apple Silicon · ⌃⌥⌘P toggles typing mode.</p> 617 685 </section> 618 686 </article> 619 687 ··· 623 691 <a href="https://aesthetic.computer">aesthetic.computer</a><span class="dot">·</span> 624 692 <a href="https://notepat.com">notepat.com</a><span class="dot">·</span> 625 693 <a href="https://kidlisp.com">kidlisp.com</a><span class="dot">·</span> 626 - <a href="/menuband/privacy">privacy</a> 694 + <a href="/menuband/privacy" data-i18n="privacy">privacy</a> 627 695 </nav> 628 696 <div class="footer-meta"> 629 697 <span>© 2026</span><span class="dot"> · </span> ··· 632 700 </footer> 633 701 634 702 <script> 703 + // ── i18n. Same plug-and-play shape as kidlisp.com / help.aesthetic.computer: 704 + // one table per language, `data-i18n` attributes on every translatable 705 + // element, and `data-i18n-html="true"` for strings that need to keep 706 + // inline markup (links, <b>, <code>). The pref is persisted under 707 + // `menuband-lang` so the user lands in their last language on return. 708 + (function menubandI18n() { 709 + const i18n = { 710 + en: { 711 + title: "Menu Band — Built-in macOS instruments, in the menu bar.", 712 + description: "Menu Band brings the built-in macOS instruments — the ones GarageBand uses — to your menu bar. Tap, type, or send to your DAW over MIDI.", 713 + tagline: "Taking macOS' standard instruments out of the 🎸 Garage and kickin' it on the curb!", 714 + downloadCta: "Download", 715 + downloadMeta: "0.9 · Apple Silicon · 2.1 MB", 716 + viewSource: "view source", 717 + by: "by", 718 + lede: 'A tiny piano lives in the right side of your menu bar. Click the keys, type letters, or pipe notes to your DAW over MIDI — Menu Band publishes a virtual MIDI source named <code>Menu Band</code> that any DAW can pick as input. When nothing\'s listening, a built-in General MIDI synth covers hundreds of patches.', 719 + testimonial: "“i use it on my macbook neo. practice notepat, play ableton without leaving my menu bar.”", 720 + modesTitle: "Three modes", 721 + modePointer: "Pointer", 722 + modePointerBody: "Mouse only. Two octaves.", 723 + modeNotepat: "Notepat", 724 + modeNotepatBody: "Type to play. Two octaves.", 725 + modeAbleton: "Ableton", 726 + modeAbletonBody: "Type to play. Live's M-mode layout.", 727 + whatsNewTitle: "What's new", 728 + whatsNew1: '<b>Snappier.</b> Visualizer pauses when the popover is hidden, and the Metal layer no longer waits for vsync — the popover opens instantly and bars track audio with less latency. Big thanks to <a href="https://github.com/estebanuribe">Esteban Uribe</a> for both performance patches.', 729 + whatsNew2: "<b>No Accessibility prompt.</b> Click the menubar piano and the keys flash their letters; type to play. No system-wide keystroke capture, no permission dialog. (The Notepat / Ableton modes still use global capture for play-while-using-other-apps; that's an opt-in toggle.)", 730 + whatsNew3: "<b>Plus:</b> a mute toggle, an accent-tinted Finder icon that follows your system color, and the popover keeps the same width when MIDI flips on.", 731 + recentTitle: "Recent changes", 732 + loading: "loading…", 733 + recentEmpty: "no recent changes", 734 + recentError: "couldn't load recent changes", 735 + requirementsTitle: "Requirements", 736 + requirementsBody: "macOS 11+ · Apple Silicon · ⌃⌥⌘P toggles typing mode.", 737 + privacy: "privacy", 738 + }, 739 + es: { 740 + title: "Menu Band — Los instrumentos integrados de macOS, en la barra de menús.", 741 + description: "Menu Band trae los instrumentos integrados de macOS — los que usa GarageBand — a tu barra de menús. Toca, escribe o envíalos a tu DAW por MIDI.", 742 + tagline: "¡Sacando los instrumentos clásicos de macOS del 🎸 Garage para tocarlos en la acera!", 743 + downloadCta: "Descargar", 744 + downloadMeta: "0.9 · Apple Silicon · 2,1 MB", 745 + viewSource: "ver código", 746 + by: "por", 747 + lede: 'Un piano diminuto vive a la derecha de tu barra de menús. Toca las teclas, escribe letras o envía notas a tu DAW por MIDI — Menu Band publica una fuente MIDI virtual llamada <code>Menu Band</code> que cualquier DAW puede tomar como entrada. Cuando nadie escucha, un sintetizador General MIDI integrado cubre cientos de timbres.', 748 + testimonial: "«lo uso en mi macbook neo. practico notepat y toco ableton sin salir de la barra de menús».", 749 + modesTitle: "Tres modos", 750 + modePointer: "Puntero", 751 + modePointerBody: "Solo ratón. Dos octavas.", 752 + modeNotepat: "Notepat", 753 + modeNotepatBody: "Escribe para tocar. Dos octavas.", 754 + modeAbleton: "Ableton", 755 + modeAbletonBody: "Escribe para tocar. Disposición M-mode de Live.", 756 + whatsNewTitle: "Novedades", 757 + whatsNew1: '<b>Más ágil.</b> El visualizador se pausa cuando el popover está oculto, y la capa Metal ya no espera al vsync — el popover abre al instante y las barras siguen el audio con menos latencia. Mil gracias a <a href="https://github.com/estebanuribe">Esteban Uribe</a> por ambos parches de rendimiento.', 758 + whatsNew2: "<b>Sin permiso de Accesibilidad.</b> Pulsa el piano de la barra y las teclas muestran sus letras; escribe para tocar. Sin captura global de teclas, sin diálogo de permisos. (Los modos Notepat / Ableton aún usan captura global para tocar mientras usas otras apps; ese es un interruptor opcional.)", 759 + whatsNew3: "<b>Además:</b> un interruptor de silencio, un icono de Finder tintado al color de acento del sistema, y el popover mantiene el mismo ancho cuando se enciende MIDI.", 760 + recentTitle: "Cambios recientes", 761 + loading: "cargando…", 762 + recentEmpty: "sin cambios recientes", 763 + recentError: "no se pudieron cargar los cambios recientes", 764 + requirementsTitle: "Requisitos", 765 + requirementsBody: "macOS 11+ · Apple Silicon · ⌃⌥⌘P alterna el modo de escritura.", 766 + privacy: "privacidad", 767 + }, 768 + }; 769 + 770 + const supported = Object.keys(i18n); 771 + function detectDefault() { 772 + const stored = localStorage.getItem("menuband-lang"); 773 + if (stored && supported.includes(stored)) return stored; 774 + const pref = (navigator.language || "en").toLowerCase(); 775 + for (const code of supported) if (pref.startsWith(code)) return code; 776 + return "en"; 777 + } 778 + 779 + let currentLang = detectDefault(); 780 + 781 + window.menubandT = function (key) { 782 + return (i18n[currentLang] && i18n[currentLang][key]) 783 + || i18n.en[key] 784 + || key; 785 + }; 786 + 787 + function applyLanguage() { 788 + document.documentElement.lang = currentLang; 789 + const pack = i18n[currentLang] || i18n.en; 790 + if (pack.title) document.title = pack.title; 791 + const desc = document.querySelector('meta[name="description"]'); 792 + if (desc && pack.description) desc.setAttribute("content", pack.description); 793 + 794 + document.querySelectorAll("[data-i18n]").forEach((el) => { 795 + const key = el.getAttribute("data-i18n"); 796 + const val = pack[key]; 797 + if (typeof val !== "string") return; 798 + if (el.getAttribute("data-i18n-html") === "true") { 799 + el.innerHTML = val; 800 + } else { 801 + el.textContent = val; 802 + } 803 + }); 804 + 805 + document.querySelectorAll("#lang-switch button").forEach((btn) => { 806 + const isActive = btn.getAttribute("data-lang") === currentLang; 807 + btn.setAttribute("aria-pressed", isActive ? "true" : "false"); 808 + }); 809 + } 810 + 811 + function setLanguage(code) { 812 + if (!supported.includes(code) || code === currentLang) return; 813 + currentLang = code; 814 + localStorage.setItem("menuband-lang", code); 815 + applyLanguage(); 816 + window.dispatchEvent(new CustomEvent("menuband-lang-change", { detail: code })); 817 + } 818 + 819 + document.querySelectorAll("#lang-switch button").forEach((btn) => { 820 + btn.addEventListener("click", () => { 821 + setLanguage(btn.getAttribute("data-lang")); 822 + }); 823 + }); 824 + 825 + applyLanguage(); 826 + })(); 827 + 635 828 // Corner label click — return to aesthetic.computer/. Mirrors the 636 829 // pattern from aesthetic-direct.html so behavior is uniform across 637 830 // AC's html-rendered corner-label paths. ··· 702 895 : 'deployed commit'; 703 896 } 704 897 const commits = (data && Array.isArray(data.recentCommits)) ? data.recentCommits : []; 898 + const t = (k) => (window.menubandT ? window.menubandT(k) : k); 705 899 if (!commits.length) { 706 - list.innerHTML = '<li class="commits-empty">no recent changes</li>'; 900 + list.innerHTML = '<li class="commits-empty">' + t('recentEmpty') + '</li>'; 707 901 return; 708 902 } 709 903 list.innerHTML = commits.slice(0, 10).map((c) => { ··· 714 908 }).join(''); 715 909 }) 716 910 .catch(() => { 717 - list.innerHTML = '<li class="commits-empty">couldn\'t load recent changes</li>'; 911 + const t = (k) => (window.menubandT ? window.menubandT(k) : k); 912 + list.innerHTML = '<li class="commits-empty">' + t('recentError') + '</li>'; 718 913 }); 719 914 })(); 720 915