App search localization breaks when Core Spotlight keeps metadata created under an earlier locale. The app may switch from English to Japanese immediately, yet system search still shows an English title and description. Tapping that result can also open a route whose content no longer matches the indexed text. The fix is to treat the Spotlight index as a rebuildable projection, not as live localized UI. Keep language-neutral content identities, generate localized metadata from the current translation version, replace stale records deliberately, and test search from a cold launch. This guide defines that contract and gives you an implementation sequence you can adapt to an existing iOS app.
Why Spotlight copy becomes stale
Core Spotlight does not call your localization layer every time a person searches. Your app creates a CSSearchableItem, fills an attribute set, and submits that item to an on-device index. Apple describes this flow in its guide to adding app content to Spotlight indexes. The title or description submitted at that moment is therefore a snapshot.
An app-specific language picker makes the boundary visible. A language change refreshes views that observe app state, but the index lives outside that view tree. It will not update merely because Locale, a string bundle, or a dependency container changed. The same stale result can survive a relaunch because the indexed record has already been persisted by the system.
The metadata surface is broader than one title. Apple's CSSearchableItemAttributeSet reference includes descriptive and searchable fields such as title, content description, and keywords. If those fields are generated in different code paths, one record can become mixed-language even when the visible screen is correct.
A Stack Overflow author asked why a Core Spotlight content description did not localize through NSLocalizedString. That question is a practitioner report about one implementation, not evidence of a platform defect. It exposes the common mistaken assumption that using a localization function once makes the stored result react to later locale changes.
Model the index as a localized projection
Start with a language-neutral source record. It should contain the stable content identity, routing information, privacy state, and values needed to build the searchable representation. Do not use a translated title as the identifier. Translations can change, and two languages may produce identical display text.
Add projection state separately:
struct SpotlightProjectionState: Codable {
let contentID: String
let indexedLocale: String
let sourceRevision: Int
let translationRevision: Int
let indexedAt: Date
}
struct SearchableSource {
let contentID: String
let route: String
let titleKey: String
let descriptionKey: String
let keywordKeys: [String]
let isSearchable: Bool
}
contentID remains stable across languages. indexedLocale records the effective app language used to build the current snapshot. The two revision fields let you distinguish a content edit from a translation update. The route remains language-neutral so a result does not hardcode an obsolete locale into navigation.
Use one deterministic identifier format, such as article:<contentID>, unless your product intentionally keeps several language variants searchable at the same time. Most apps with an explicit language choice should expose one current-language record per content item. Keeping every language in Spotlight can produce duplicate results and reveal text the person did not choose.
Resolve locale at the indexing boundary
Do not let every feature decide which locale to index. Give the indexer an explicit locale supplied by the same resolver used by the visible app. A typical precedence contract is:
- Use a valid app-specific language selected by the user.
- Otherwise resolve the first supported language from the ordered system preferences.
- Otherwise use the app's declared fallback language.
Pass the resolved identifier into the index builder. Avoid reading an ambient process locale deep inside a background task because it may differ from the app's effective choice. The indexer should also use a bundle or localization service that can load strings for that explicit locale.
func makeSearchableItem(
source: SearchableSource,
locale: Locale,
strings: LocalizedStringProvider
) -> CSSearchableItem {
let attributes = CSSearchableItemAttributeSet(contentType: .content)
attributes.title = strings.value(source.titleKey, locale: locale)
attributes.contentDescription = strings.value(source.descriptionKey, locale: locale)
attributes.keywords = source.keywordKeys.map {
strings.value($0, locale: locale)
}
return CSSearchableItem(
uniqueIdentifier: "article:\(source.contentID)",
domainIdentifier: "articles",
attributeSet: attributes
)
}
This example is an implementation pattern, not an Apple guarantee. Adapt content types, fields, and error handling to your data. The function takes locale as an input instead of reading hidden mutable state.
Keywords deserve their own review. Translating an English keyword list literally may not match the terms used in another language. Keep keyword keys or curated locale-specific terms in the localization workflow, and apply the same source revision rules used for titles and descriptions. Do not stuff unrelated synonyms into the index merely to increase matches.
Rebuild after language and copy changes
A full rebuild is appropriate when the effective app language changes because every localized field may be stale. Smaller item-level updates are appropriate when one content record changes. Both operations should use the same builder, identifier policy, and projection-state store.
The sequence for a language change should be explicit:
- Save and validate the new app language.
- Refresh app-owned UI state.
- Mark the current Spotlight projection stale.
- Read the searchable source records in bounded batches.
- Build every item with the new effective locale and current translation revision.
- Submit replacement items to the index.
- Persist projection state only after the submission succeeds.
- Remove records for content that is no longer searchable.
Apple describes CSSearchableIndex as an on-device index and exposes item-management operations in the CSSearchableIndex reference. Use those operations behind one adapter. That gives the rest of the app a small interface such as rebuild(locale:), upsert(contentIDs:), and remove(contentIDs:) instead of allowing arbitrary features to mutate Spotlight.
Replacement is safer when identifiers are stable. Submitting a new item for the same content identity updates the projection without creating a second result for the new language. If an earlier implementation included the locale in each unique identifier, a language switch can leave old records beside new ones. Migrate that scheme by deleting the old identifier family after the new projection succeeds.
For a large catalog, avoid blocking the language-picker screen until every item is indexed. Commit the user preference and refresh visible UI first. Run the index rebuild as cancellable background work, persist progress by batch, and restart safely after termination. Search may remain briefly stale, but the app should not pretend indexing completed before the submission callback succeeds.
Keep routing independent from indexed language
A Spotlight result must navigate by stable identity. When the app opens from system search, extract the content ID, resolve the current app locale, load the current record, and render it through the normal localized route. Do not trust indexed display text as source data, and do not parse a translated title to recover an identifier.
The route handler also needs deletion behavior. A result may be tapped after the underlying content was archived, access was revoked, or a signed-in account changed. In that case, show a current localized recovery screen and schedule removal of the stale item. Sending the user to an unrelated default screen hides the index defect and makes verification harder.
Cold launch is the critical case. The app may receive the search activity before all services have restored the selected language. Initialize locale resolution before rendering the destination, or hold navigation until both identity and language state are ready. This is different from encoding a locale in the result. The result carries stable content identity; the app supplies current presentation policy.
Handle failure without corrupting state
Treat index submission as a fallible synchronization operation. If one batch fails, do not mark the whole projection current. Keep the previous successful state, record the failed identifiers, and retry from a controlled trigger such as the next foreground activation. Avoid an unbounded immediate retry loop.
Use these decision rules:
- If locale resolution fails, index with the declared app fallback and record that fallback in projection state.
- If one source record cannot produce required metadata, skip that record and log its stable ID without exposing its content.
- If a batch submission fails, retain the prior projection revision and retry the failed batch later.
- If content becomes private or the account signs out, prioritize deletion over rebuilding localized copy.
- If translation assets roll back, compare revision numbers and rebuild rather than assuming a newer timestamp means correct text.
Deletion is part of localization correctness. A stale item from another account or an earlier privacy state is more serious than a stale translation. Keep account and access boundaries in the domain or identifier policy, and clear the relevant records when those boundaries change.
Do not log full titles, descriptions, private keywords, or routes containing personal data. Diagnostics need content IDs, locale identifiers, revision numbers, operation type, duration, and the returned error. That is enough to identify a broken batch without copying indexed content into telemetry.
Verify the gap is closed
A unit test that checks NSLocalizedString returns Japanese does not prove Spotlight contains Japanese. Verification must cross the process boundary and exercise the stored projection.
Build fixtures for at least two languages with visibly different titles, descriptions, and keyword sets. Then run this release matrix:
| Scenario | Expected result |
|---|---|
| Fresh install with system language A | Indexed copy uses supported language A |
| Fresh install with unsupported language | Indexed copy uses the declared fallback |
| Switch from language A to B | Existing results are replaced with language B copy |
| Translation revision changes | Affected records rebuild without duplicate results |
| App terminates during a large rebuild | Next run resumes without marking incomplete work current |
| Content is deleted or made private | Its Spotlight item is removed |
| Result opens from a cold launch | Stable ID resolves and the destination uses the current locale |
| User signs out | Account-scoped indexed content is removed |
Search for both the old and new localized keywords after a language switch. The new term should find the expected record, while the old term should not leave an obsolete duplicate unless multilingual discovery is a documented product choice. Open the result from a terminated app and verify the title on the destination screen matches current content, not merely the old index snapshot.
Also test a partial failure by injecting an error after one batch. The projection revision must stay stale, a later retry must replace the remaining records, and successful records must not multiply. Confirm that a fallback-language rebuild can later be superseded when the requested translation becomes available.
Common implementation mistakes
Calling a localization function during initial indexing does not keep the item current forever. It stays exactly as localized as the submitted snapshot until your app updates it.
Putting a translated title or locale-specific route into uniqueIdentifier creates duplicate identities when wording or language changes. Stable domain identity belongs in the identifier; presentation belongs in the attribute set.
Rebuilding only when the user changes language misses other invalidation events. Translation corrections, source-copy edits, restored preferences, account changes, and withdrawn content can all invalidate the projection. Compare explicit source and translation revisions whenever the indexer runs.
Marking indexing complete before the platform operation succeeds creates false state. Persist completion after the callback, and preserve enough batch state to retry without guessing.
Checking only a warm app launch misses initialization-order bugs because Spotlight starts the app outside its ordinary navigation flow. Cold-launch routing belongs in every release test.
Put the rebuild behind one contract
Core Spotlight localization becomes manageable once the index is treated as derived data. Stable content records remain the source of truth. One locale resolver chooses the presentation language. One index adapter builds, submits, deletes, and records projection revisions. Search results route by content identity and render through current app state.
Your next action is to add indexedLocale, sourceRevision, and translationRevision to the projection state for one searchable content type. Switch the app language, rebuild that domain, search for old and new terms, and open the result from a cold launch. If that test passes without duplicates or stale copy, extend the same contract to the rest of the index.
Home-screen quick actions are another system surface that caches localized labels; see how to localize iOS app shortcuts.
References
- Apple: Adding your app's content to Spotlight indexes supports the app-owned searchable-item creation and submission workflow.
- Apple: CSSearchableItemAttributeSet supports the indexed metadata fields used for titles, descriptions, and keywords.
- Apple: CSSearchableIndex supports the on-device index and item-management boundary.
- Stack Overflow: Core Spotlight content description localization supplies a practitioner report about localizing submitted content descriptions.
