Skip to content

feat(applications): match macOS apps by their localized display name - #590

Merged
Xoshbin merged 3 commits into
Xoshbin:mainfrom
kai-osthoff:feat/macos-localized-app-names
Aug 7, 2026
Merged

feat(applications): match macOS apps by their localized display name#590
Xoshbin merged 3 commits into
Xoshbin:mainfrom
kai-osthoff:feat/macos-localized-app-names

Conversation

@kai-osthoff

Copy link
Copy Markdown
Contributor

Problem

macOS localizes bundle display names. On a German system Photos.app presents as "Fotos", Notes.app as "Notizen", System Settings.app as "Systemeinstellungen". Asyar indexes the bundle file name, so those apps are unreachable under the name the user actually sees.

On one German install this affects twelve stock system apps — Fotos, Notizen, Erinnerungen, Kalender, Nachrichten, Karten, Musik, Wetter, Rechner, Vorschau, Kontakte, Systemeinstellungen — plus any localized third-party app. For non-English users this makes application search substantially less useful.

The name has to come from the OS. CFBundleDisplayName in Info.plist reads "Photos" even on a localized system, and the lproj resources aren't reliably parseable (Photos.app/Contents/Resources/de.lproj carries no readable InfoPlist.strings at all). NSFileManager.displayName(atPath:) is the supported route; objc2-foundation is already a dependency, so this adds none.

Both names stay searchable

Replacing name outright would trade one bug for another — users who know apps by their English names would stop finding them, and macOS itself keeps both (kMDItemDisplayName = Fotos.app, kMDItemAlternateNames = Photos.app).

SearchableItem::search_names() returns the display name plus the bundle file name when the two differ, and the skim pre-filter scores all of them and keeps the best hit. This has to happen in the pre-filter: merged_search uses search() as its gate, so a name only reachable via keywords would be filtered out before ranking ever sees it. The classifier's keywords are extended too, otherwise a file-name match would classify as "no name hit" and rank below genuine misses.

The file name is derived from path, which is already stored — no new field, no schema change, no regenerated TS bindings, no migration. Only name is rendered, so results still read "Fotos".

App ids are deliberately unaffected

build_app_id() embeds the name, and the resulting id keys usage stats, user-assigned aliases (item_aliases.object_id) and item shortcuts. Letting a localized display name reach it would have changed the id of every localized app on the first scan after upgrade and silently dropped all three.

So the id keeps receiving the file name, via a new bundle_file_name() helper that is deliberately separate from display_name(). Existing ids stay byte-identical. A test pins this down so the two don't get merged back together later.

This also de-duplicates the file_stem() logic that was copy-pasted across sync_application_index and list_applications.

Cost

NSFileManager resolution measured over a real install: 246 apps in 64 ms (261 µs/app), on the index sync that already walks the filesystem and extracts icons — both far more expensive. No cache added: it would raise an invalidation question (language change, app update, rename) for a cost that doesn't currently warrant one. Happy to add one if you'd rather.

Testing

cargo test --lib → 3176 passed, 9 ignored. cargo clippy clean, cargo fmt --check clean.

One pre-existing failure, unrelated to this change: file_index::deep::mdfind::tests::probe_succeeds_on_a_real_mac fails when the test process can't reach Spotlight. Verified by stashing this branch's changes and re-running it — it fails identically on a clean tree.

Note on locale coverage: no test can assert "Fotos" without failing on an English CI runner. The search-side tests therefore run against synthetic items and are locale-independent, while the NSFileManager test only asserts what must hold in every locale (non-empty, extension stripped). Confirming the German behaviour end-to-end needs a localized machine.

kai-osthoff and others added 3 commits August 7, 2026 09:33
macOS localizes bundle display names: Photos.app presents as "Fotos" on
a German system, Notes.app as "Notizen", System Settings.app as
"Systemeinstellungen". Asyar indexed the bundle file name instead, so on
any non-English system those apps could not be found under the name the
user sees — twelve stock system apps on a German install alone.

The scan now resolves the name through NSFileManager, which is the only
reliable source: the localized string does not live in Info.plist
(CFBundleDisplayName reads "Photos" even on a localized system) and the
lproj resources are not reliably parseable.

Both spellings stay searchable. SearchableItem::search_names() returns
the display name plus the bundle file name when they differ, and the
skim pre-filter scores all of them, keeping the best hit. The file name
is derived from `path`, so it needs no extra storage and no schema
change. Only `name` is ever rendered, so results still read "Fotos".

Ranking follows: the non-display name is added to the classifier's
keywords, otherwise an app matched via its file name would classify as
"no name hit" and rank below genuine misses.

App ids are deliberately unaffected. build_app_id() embeds the name, and
the id keys usage stats, user-assigned aliases and item shortcuts — so
it keeps receiving the file name via the new bundle_file_name() helper.
Existing ids stay byte-identical and no data is lost on upgrade. A test
pins this down.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@Xoshbin

Xoshbin commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Hey @kai-osthoff, really nice work on this one 🎉

The localized-name problem is a genuinely annoying one to get right, and I like the design: keeping bundle_file_name() (identity, used for the app_ id) strictly separate from display_name() (presentation, what the user sees) is exactly the right call — it means usage stats, aliases, and shortcuts survive a language change untouched, which is easy to get wrong here. The search_names() addition so both the localized name and the on-disk name stay searchable is a thoughtful touch too — that's the kind of detail that's easy to skip and would've quietly broken search for anyone used to typing the English app name. Test coverage was solid, and everything (cargo test, clippy --all-targets, fmt) passed clean.

Two small things worth flagging, both handled:

Merge conflict with #589 — main moved forward after this branch was cut, and #589 touched the same file (both added a test at the same anchor line). Nothing wrong on your end, just timing. It's resolved now — all tests from both PRs pass together.
One edge case in search_names() — it derives an alternate search name from the app's path, but Windows UWP apps are indexed under a synthetic shell:AppsFolder<AUMID> identifier rather than a real file path. Since AUMIDs are dotted (e.g. Microsoft.WindowsCalculator_...!App), the file-stem logic was cutting them at that dot and picking up the publisher prefix as a "name" — so searching "Microsoft" would've surfaced every Microsoft-published app. Small guard fix, verified with a test that reproduces it and confirms the fix (search for "microsoft" now correctly only returns apps whose real name contains it, like "Microsoft Word" — Calculator no longer shows up for that query, but still shows up correctly for "calculator").
Good PR, appreciate the care put into the identity/presentation split.

Merging now.

@Xoshbin
Xoshbin merged commit de2e276 into Xoshbin:main Aug 7, 2026
6 checks passed
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.

2 participants