Skip to content

Localized screenshots everywhere: the 128-capture matrix and the background parallel generator - #96

Merged
jedijashwa merged 15 commits into
mainfrom
localized-screenshots
Aug 21, 2026
Merged

Localized screenshots everywhere: the 128-capture matrix and the background parallel generator#96
jedijashwa merged 15 commits into
mainfrom
localized-screenshots

Conversation

@jedijashwa

Copy link
Copy Markdown
Owner

Every localized page now serves screenshots in its own language — 8 scenes × light/dark × English + 7 locales, 128 captures, regenerated in ~6 minutes by a generator that no longer takes over the machine. Along the way, live auditing of the capture instances turned into the deepest localization QA pass the app has had: 78 new keys (681 total), a locale-aware duration formatter, and a fully retranslated sidebar.

The generator, rebuilt (Josh's ask: "not an hour, and run it in the background")

  • Backgrounded: instances never take focus, the cursor, or the clipboard. Scenes drive entirely through pid-targeted channels — AX selection on the sidebar outline (select-row/disclose; SwiftUI rows discard posted clicks), pid-addressed AppleEvents for the demo Location and pullmark://capture/… links (open -a resolved by bundle id and once handed the folder to a surprise third instance), posted keys and clipboard-free typing, and a capture-only URL channel for the one interaction with no AX path (the edit scene's block reveal).
  • Parallel: one instance per language on cascaded frames. -pm.captureChrome makes windows draw genuinely active chrome without focus (real makeKey() — a background app is allowed; two rounds of faked notifications raced AppKit into gray traffic lights and are memorialized in the commit log).
  • Isolated: demo suites are per-pid (a shared suite meant every fresh launch's startup wipe reset other live instances' state mid-scene — the blame-gutter saga), and per-scene sticky flags ride the argument domain. Capture runs can no longer leak state anywhere.
  • Self-verifying: every capture passes blankcheck (content actually painted) and lightcheck (active chrome) with in-place retries, and still-failing combos re-run solo in an automatic fix-up pass. Machine-checked ×128, plus per-locale human review of the risk-concentrated scenes.

Localization QA the live audit caught (all fixed + translated ×7)

  • Sidebar sections dropped the English-landmark convention: 場所, Orte, Emplacements, Ubicaciones, Locaties, Localizações, 位置 — with ~40 docs-prose references per locale renamed to match, singular "Location" unified, and old English(gloss) headings flattened.
  • CI check durations were hand-built "3m 32s" strings — now DateComponentsFormatter in the launch language (ja: 3分32秒), demo fixtures included.
  • The gate-blind classes swept: ternaries and custom view parameters feeding .help, titles, and subtitles (nav-history tooltips, checks summaries, comment badges, What's New titles, the Inline/Side-by-Side flip, Open Quickly rows, lightbox buttons, the Compare Revisions sheet, share tooltips, remote-doc reasons — 62 keys).
  • The PR state chip split from the "Open" action key (de: Offen, not Öffnen; ja: オープン, not 開く), · editing localized, and nl/es/pt-BR got real Light/Dark values instead of identity English.
  • Demo paths display as ~/Documents/PullMark Demo instead of raw /var/folders/… in titlebars, tooltips, and Open Quickly.

Samples

French rendered-diff blame (gutter, localized chrome):

PullMark in French: Result view with blame gutter

Dutch Settings → Appearance (theme cards with localized descriptors, Licht/Donker):

PullMark in Dutch: Appearance settings with theme cards

Japanese edit mode, dark (one revealed source block, localized 編集中 marker, clean demo path):

PullMark in Japanese dark mode: edit mode with one source block

Verified

  • make test: 617 tests + strings OK: 681 keys across all seven locales.
  • check-site-i18n.py: fully green for the first time — 96 pages, hreflang matrix, per-language screenshot serving, and image existence all verified.
  • Known accepted residue: European locales keep "Pull Requests" as the loanword (CJK translate it); UI command/setting names the docs cite verbatim stay English per the site's separate convention — now arguably stale since the app ships localized, flagged for a future cross-locale decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6

jedijashwa and others added 15 commits August 20, 2026 22:16
The language screenshot matrix is the audit the app-i18n wave promised,
and its first run delivered: the diff-mode picker (Rendered Diff /
Source Diff / Result), the Inline / Side by Side layout picker, the
theme and content-width card descriptors, and the review-verdict rows
all rendered English under every other language — computed display
vars and rawValue-driven pickers, the classes the check-strings gate
cannot see. All routed through String(localized:) now; 13 new keys
translated across the seven locales (617 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
ax.swift gains menuitem (press a menu item by title anywhere in the
menu bar — top-level menu names are system-localized, item titles
resolve from loc/), menukey (press by keyboard equivalent: Settings
⌘, and Quit ⌘Q have no app-owned titles), sidebar-state (is there a
native list outside the web area?), id (press by AXIdentifier — the
Open panel's OKButton; plain Return never reaches the panel's bridged
content), and menulist for discovery. list now prints identifiers.
pkey.swift accepts combined modifiers (cmd shift opt ctrl).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
--lang all iterates English + the seven locales; localized captures
land in out/<site-code>/ mirroring site/img/. Scenes act on controls
by localized accessibility title, resolved at run time from
loc/<lang>.lproj (app strings) and SwiftUI's own Localizable.loctable
(the system sidebar toggle) via loc-lookup.py — never hardcoded.
--lang pairs -AppleLanguages with -AppleLocale so dates and numbers
render natively.

The demo Location now arrives through the app's own Open panel with
pid-targeted keys and is VERIFIED via AX before any scene runs.
open -a is banned: with /Applications and dist alive under one bundle
id, Launch Services sometimes spawned a third instance for the folder
and the scene captured a folderless window. Launch failures now skip
the capture loudly, and quit_app sweeps stray dist instances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
The 14 localized pages reference /img/<code>/app-*.png; the checker
now fails when a page serves another language's app screenshots or
references an image missing on disk (a bad path silently 404s to alt
text). Image promotion follows once the capture matrix lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Josh called the half-translated sidebar: English Open Files/Locations
above a translated プルリクエスト. The landmark convention is dropped —
Open Files, Locations, and Recents are now real translations in all
seven locales (Apple's own Finder vocabulary, verified against the
system's loctables: 場所, Orte, Emplacements, Ubicaciones, Locaties,
Localizações, 位置…), Pull Requests keeps its defensible split (CJK
translated, European loanword). Site prose follows: ~40 section
references per locale renamed, singular Location unified, old
English(gloss)headings flattened, in-app prose values that named
the sections mid-sentence updated. UI command/setting labels the
site cites verbatim stay English per their separate convention.
Also repairs three German alt attributes that ASCII closing quotes
had been silently truncating.

CaptureChrome (-pm.captureChrome, generator-only): windows draw
active chrome — colored traffic lights, accent selection — without
the app ever taking focus, so the screenshot generator can drive
many backgrounded instances in parallel without stealing the machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Josh asked for a matrix that doesn't take an hour or the machine.
Scenes now drive instances entirely through pid-targeted channels:
AX row selection on the sidebar outline (rows discard posted clicks
and carry no AXPress), pid-addressed AppleEvents for the demo
Location and pullmark:// links (open -a resolved by bundle id and
sometimes spawned a third instance for the document), posted keys and
clipboard-free unicode typing, and a -pm.captureChrome drive channel
for the one page interaction with no AX path (the edit scene's block
reveal — the click listener is delegated).

CaptureChrome makes it all photograph correctly: windows draw active
chrome (colored traffic lights, accent selection, key-window
nomination so a never-activated app stops dropping key events)
without the app ever taking focus. Languages run in parallel — one
instance per language on cascaded frames — so the full 128-capture
matrix lands in one language's wall clock (~5 minutes) while the
machine stays usable. No Launch Services registration remains, so
there is nothing to unregister.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Hovering the capture instances mid-run turned up a class the gate
cannot see: ternaries and custom view parameters feeding .help,
titles, and subtitles — nav-history tooltips, checks summaries,
comment badges, the What's New sheet titles, the View menu's
Inline/Side-by-Side flip, Open Quickly row titles, lightbox buttons,
the Compare Revisions sheet, share tooltips, remote-doc reasons, and
friends. All routed through String(localized:) with whole keys
(concatenations folded, English plural hacks split into real
singular/plural pairs). CI check durations were hand-built "3m 32s"
strings — now DateComponentsFormatter in the launch language (ja:
3分32秒), demo fixtures included. 62 new keys, translated across the
seven locales: 679 total, gate green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Two captures from the first parallel run came out wrong after the
human (understandably) clicked around a capture window mid-run: the
instance activates, input lands inside, and the scene photographs
whatever resulted. Every drive step that shapes a capture now ends
with '|| return 1' (set -e is suppressed inside functions called in
a condition, so failures were silent), and the pr scene selects the
PR row explicitly instead of trusting launch selection — the folder
delivery can steal it for the folder's README even without help.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
An opus review pass over all 128 captures surfaced four systemic
classes, each now closed:

- Gray traffic lights on ~60% of captures: the repeated become-key
  blessing regressed what the one-shot version had right — AppKit
  reconciles a key-claim it knows is false and captures raced the
  flicker. Back to once per window (timer only discovers new windows).
- Missing blame gutters: pm.blame is sticky in the defaults domain
  SHARED across instances and runs, and blind toggle presses turned
  it off for whichever instance pressed second. The scene now ENSURES
  the checkbox state (ax.swift setcheck), and the generator snapshots
  and restores the flag so runs stop leaking state into the human's
  own app.
- Blank content panes (3 captures): WebKit under parallel load hadn't
  painted yet. capture() now runs blankcheck.swift (content-region
  stddev) and retries before failing the scene loudly.
- Wrong Settings tab (es themes): pressing Appearance before the
  Settings window appeared matched the MAIN window's same-titled
  toolbar menu. The scene waits for the Settings window and presses
  inside it only (ax.swift presswin skips the 1052-wide capture
  window).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Eight captures in the fixed matrix still came up blank through the
retry loop: WebKit suspends a fully occluded window's WebContent
process, sometimes before first paint, and the parallel workers'
cascaded frames overlap. With -pm.captureChrome the web view now opts
out of page-visibility process suppression, so backgrounded instances
render no matter what covers them. Normal runs are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Three more from the capture reviews: the titlebar's ' · editing'
marker was hardcoded English (now a localized key); the PR state chip
shared the "Open" key with the Open ACTION buttons, so German
conjugated it wrong (Öffnen) and ja carried the 開く nit — split into
pr-status-open with the state reading per locale (Offen, オープン,
开启, Ouverte…); and demo-session paths now display as
~/Documents/PullMark Demo instead of the raw /var/folders temp dir in
titlebars, tooltips, and Open Quickly rows — three reviewers flagged
the temp path as unfit for published screenshots. 681 keys, gate
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
The real mechanism behind every lost blame gutter: demo mode uses an
isolated defaults suite WIPED AT STARTUP, the parallel generator
launches a fresh instance per scene, and with one shared suite name
every launch nuked every live instance's state — @AppStorage observed
the wipe and reset blame mid-scene. Two rounds of toggle choreography
(blind press, ensure-state) lost to that and were removed.

Now: the generator pins -pm.blame per scene through the argument
domain (per-process, wipe-proof — the appearance pin already proved
the suite honors it), demo suites are named per-pid so instances
can't touch each other at all (with a liveness-checked janitor for
dead instances' plists), the themes scene confirms the tab switch by
window title and re-presses until it lands, capture() retries gray
lights as well as blank panes with a longer budget, and nl/es/pt-BR
get real Light/Dark values (Licht/Donker, Claro/Oscuro, Claro/Escuro)
instead of the identity English the first translation wave left.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Full-parallel runs flake at ~5% (WebKit under eightfold load) and the
same combos succeed solo essentially always — so instead of chasing
zero-flake parallelism, failed captures re-run sequentially at the end
of the run. Retry combos are consumed before re-invoking (each child
run clears the bookkeeping at exit), and PM_GEN_FIXUP guards
recursion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Every page now serves screenshots in its own language: 112 localized
captures in site/img/{zh,ja,fr,de,nl,es,pt}/ plus the 16 English ones
refreshed from the same build — all from one generator run, every
capture machine-verified (content painted, active chrome) and the
risk-concentrated scenes human-reviewed per locale. Cache-busters
bumped on all sixteen referencing pages; check-site-i18n is fully
green for the first time since the localized refs landed (96 pages,
screenshot existence and per-language serving verified).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
demoDefaultsSuiteIsNotTheRealDomain pinned the old shared suite name
(hasSuffix .demo); it now asserts the per-pid contract — the demo
prefix plus a numeric process id — and documents why the suites went
per-process. (The local run that should have caught this piped make
test through head, which masked the failing exit code — CI caught it.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
@jedijashwa
jedijashwa merged commit d3d1e7e into main Aug 21, 2026
1 check passed
@jedijashwa
jedijashwa deleted the localized-screenshots branch August 21, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant