Repository navigation
feat(menu): accessibility & localisation readiness sweep - #138
Merged
Merged
Conversation
MenuBarExtra hands the label view's accessibilityLabel to the NSStatusBarButton (AXTitle "Ember") but drops accessibilityValue, so VoiceOver never heard the bot's state: AXValue was empty on device. Verified with an AX probe against a minimal MenuBarExtra: the label arrives, the value doesn't, and a value set on the button directly survives SwiftUI swapping the icon image every frame. The label view now writes MenuRows.accessibilityValue onto the status button whenever it changes, retrying briefly at launch before the status bar window exists. Refs #115
- The Focus ring always glided to its new value. With Reduce Motion on it now jumps. - Clock health flagged low battery, weak Wi-Fi and a failed publish only by turning the cell orange. With Differentiate Without Color the cell's symbol becomes a warning triangle, and VoiceOver hears "Needs attention" either way. - Settings' panel previews set their captions in 60-85% white on black; Increase Contrast now takes them to near-solid white. Refs #115
- Usage rows read "Claude 5h, 47%": the label now says "5-hour window" and the value includes the reset time the row shows. - Percent sliders (brightness, scroll speed, buzzer volume) reported the raw slider position; they now report the percent shown next to them, and the duplicate text is hidden. - The coordinates' degree signs were separate VoiceOver stops. - Esc now closes the Discover Clocks sheet, as Done does with Return. - The usage window label had a fixed 20 pt width that clips longer translations. Refs #115
…nces
ValidationError carried log-style English ("server URL must not be
empty", "plist not found") as a plain String, shown verbatim in the
Connection pane's token footer and in Agents, so it could never be
translated and read like a stack trace. The message is now a
LocalizedStringResource worded for the person reading it, the error is
a LocalizedError, and the one producer state string goes through
String(localized:).
Refs #115
"1 sessions", "1 active days", "after 1 rounds" and "the next 1 hours" were plain interpolations. They now use automatic grammar agreement, like the rest of the app, so English gets the singular and a translation can supply its own plural forms. "70 %" and "+5 %" lose the space, matching US style and every other percent the app formats. Refs #115
The catalog held only the 87 EmberKit strings added by hand; the app target's ~530 SwiftUI strings were extracted at build time but never written back (Xcode only does that with the catalog open in the IDE), so nothing but the menu rows could have been translated. It now holds every extracted key, synced with xcstringstool, and every format string (%@, %lld, inflected counts) carries a comment saying what its arguments are, including the EmberKit ones that had a generic note. Refs #115
Nothing noticed when a view gained a string the catalog lacked, which is how it ended up with none of the app's strings. scripts/strings.sh does from the command line what Xcode does with the catalog open: `sync` merges the build's .stringsdata into Localizable.xcstrings (and adds EmberKit's strings, which Xcode never extracts, from compiling the package with -emit-localized-strings); `check` fails when a key is missing or a format string has no translator comment. CI runs check on its unsigned build's DerivedData. Refs #115
tarakanof
commented
Sep 26, 2026
tarakanof
left a comment
Owner
Author
There was a problem hiding this comment.
Review: #138 (a11y & l10n sweep, #115)
Verdict: OK to merge. Nothing blocking. Two should-fix items, both small.
What I verified locally (branch head, scratch worktree)
swift test --package-path macos: 446 passed.- Unsigned Debug
xcodebuild -derivedDataPath build/DerivedData: succeeded, with no compiler warnings (just the usual appintents metadata note). scripts/strings.sh check build/DerivedData: exit 0.scripts/strings.sh sync build/DerivedDataon the committed catalog left the file byte-identical, so the sync is idempotent and matches the repo's JSON format (" : "separators, sorted keys, non-ASCII kept as is).- Negative test for
check: I deleted one app key and one EmberKit key and removed the comment from an inflected key.checkexited 1 and printed all three problems plus how to fix them. - Catalog compared with the base branch: all 87 original keys are still there and none were removed. Existing plural variations (e.g.
Today %lld sessions · %@) are untouched. The only changes to existing entries are 7 generic EmberKit comments replaced with specific ones, which is intended. - DEBUG leakage: the
#Previewnames inDashboardPreviews.swiftare not in the catalog, and neither areDashboardRenderCounteror the fixtures. Those files contain no localizable calls. - Grepped for any count + plural noun without
inflect/variations. The only hit is%lld ms, which is a unit and fine. - CI: all 3 checks pass.
Findings
- should-fix:
SensorsSectionpercent is still hard-coded.Text("\(v, format: .number…sign(.always()))%")produces the key%@%%. "+5%" is correct for en, but the sign position is baked into the source, and this is the one percent in the app that doesn't go throughFormatStyle.Percent(Percent.text,PercentSliderRowboth do). SuggestText((v / 100).formatted(.percent.precision(.fractionLength(0)).sign(strategy: .always())))(orText(v/100, format: …)). That drops the%@%%key, and fr/de/tr get their own spacing and order.AgentsPane's "Green under 70%, … 90%." is prose inside a localizable key, so leaving it literal is fine. - should-fix:
StatusItemAccessibilitydepends on the private class nameNSStatusBarWindowand fails silently. If that name changes,setValuereturns false 20 times (bounded at 5 s, good) and then gives up, and nothing logs it. That's the exact regression this PR fixes, and it would come back unnoticed. Either drop the class-name filter (walk everyNSApp.windowscontent view for anNSStatusBarButton; Ember has one status item, so it's just as correct and cheap), or at leastLogger/assertionFailurewhen the retries run out. - nit: nobody has confirmed on a device that
.task(id:)runs on aMenuBarExtralabel and that the AXValue gets set. The PR lists this as a pending on-device check (the osascript line). Please run it before merging, sinceswift testand CI can't cover it. - nit:
checkdoesn't catch the reverse drift. Keys still in the catalog after their code is gone (staleapp keys,manualEmberKit keys that no longer exist) pass silently, and sync never prunes manual keys. A warning list incheckwould do. - nit:
strings.shwithout a DerivedData argument is fine. With a reused local DerivedData, though,find … -name '*.stringsdata'also picks up leftovers from files that have since been deleted, sosynccan re-add dead keys. This doesn't affect CI, which starts from a freshbuild/DerivedData. A RUNBOOK note ("pass a clean DerivedData") would cover it. - nit: junk keys
"","—","5h","7d"are in the catalog with no comment orshouldTranslate.""/"—"should beText(verbatim:)orshouldTranslate: false.5h/7dwould be better with a comment ("abbreviation: 5-hour window").
Checked, no issues
ValidationError→LocalizedError+LocalizedStringResource. Callers (ConnectionPane,FeedError) now resolve it withString(localized:), and the new test pins the sentences.- The Reduce Motion ring, the Differentiate Without Color triangle with "Needs attention" in the AX value, and the Increase Contrast
ink()are all correct and don't change layout in the default settings. PercentSliderRow: VoiceOver now reads the value from the slider, and the duplicate text is hidden. Correct.- The usage label changed from
.frame(width: 20)tominWidth: 20+.fixedSize(). It still lays out at 20 pt for "5h"/"7d" and grows instead of truncating. The two rows only drift apart once a localized label is wider than 20 pt, which is acceptable. .onExitCommandon Discover Clocks, and the hidden°signs, are fine.- en plurals via
^[…](inflect: true)are correct, and each one has a translator comment.
…isses StatusItemAccessibility looked for the button only inside a window of the private class NSStatusBarWindow; if macOS renamed it, VoiceOver would silently lose the menu-bar state again. It now walks every window's view tree for an NSStatusBarButton (Ember has one status item) and logs an error when the launch-time retries run out. Refs #115
… keys
The humidity offset was the one percent built by hand ("+5%" baked
into a "%@%%" key); it now goes through the percent format style, so
the locale decides spacing and sign placement. "" and "—" were
catalog keys only because they were passed where a localized key is
expected; they're verbatim now. "5h"/"7d" get translator comments.
Refs #115
check only caught keys missing from the catalog, not the reverse, and sync never deletes, so dead keys could pile up unnoticed. check now lists them as warnings (they don't fail the build: removing one is a translator-facing decision). RUNBOOK notes that sync wants a clean DerivedData, since a reused one keeps deleted files' .stringsdata. Refs #115
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #115
Last sweep over the macOS app for accessibility and localisation, after #132/#134/#136/#137. Most of the groundwork was already in place: stock controls, text styles, chart descriptors, labelled canvases, and
PhaseBadgepairing dot with text. This PR fills the gaps the audit turned up.Accessibility
MenuBarExtraforwards the label'saccessibilityLabelto theNSStatusBarButtonas AXTitle and dropsaccessibilityValue. That's why AXValue was empty on device. I confirmed it with an AX probe against a minimal MenuBarExtra. The label view now writesMenuRows.accessibilityValueonto the button (StatusItemAccessibility) whenever it changes. The same probe showed a value set on the button survives SwiftUI swapping the icon image. MENU-BOT.md is updated.Localisation
Localizable.xcstringshad only the 87 hand-added EmberKit keys. The app target's ~530 extracted strings had never been written back. It now has all 612 keys, synced withxcstringstool syncand kept in the repo's JSON format.%@,%lld, inflected counts) now have one, including EmberKit keys that only had the generic note.^[…](inflect: true).ValidationError(shown in the Connection pane) and the producer "plist not found" state were log-styleStrings. They are nowLocalizedStringResourcesentences, andValidationErroris aLocalizedError.Tooling / CI
scripts/strings.sh sync|check [DerivedData]:syncmerges the build's.stringsdatainto the catalog, and adds EmberKit's strings (compiled with-emit-localized-strings, since Xcode never extracts a package's strings) as manual entries.checkfails when a key is missing or a format string has no comment.build/DerivedDataand runscheck. This is the "missing view string" test. A Swift test can't see the app target's strings, so it's a build step instead. RUNBOOK documents the workflow.Audit checklist (in place of an XCUI audit)
performAccessibilityAuditwould need a UI-test target and a launched app, which would re-register the producer LaunchAgents. Instead, every view was checked by hand for:Not done
InfoPlist.xcstrings.Test evidence
swift test --package-path macos: 446 passed. New test:validationErrorsAreUserFacingSentences.xcodebuild: succeeded with no new warnings.scripts/strings.sh check: passes. Before the sync it listed the missing keys.shellcheck scripts/strings.sh: clean.osascript -e 'tell application "System Events" to tell process "Ember" to get value of menu bar item 1 of menu bar 2'. It should return e.g. "Idle" or "Claude on m4 — Running". I didn't launch the Debug app here because doing so re-registers the producers.