A multilingual messaging or support app can look finished in chat and still greet the callee with an English role label, a raw number, or a wrong-language display name on the system incoming-call screen. Drivers of that bug are rarely missing chat translations. CallKit and Android Telecom own that presentation. Your ordinary string catalog never paints it unless you push localized values into those APIs before the OS shows the call.
Treat system call UI as a release surface: inventory the labels, resolve them in the callee's effective app language, update them when identity arrives late, and verify lock-screen and killed-state paths the same way you verify chat translation.
Why system call screens drift from the app language
In-app calling usually has two owners. Your app owns the dial pad, call button, in-thread call history, and permission copy. The operating system owns the full-screen incoming call UI, the Dynamic Island or status-bar call indicator, and many lock-screen controls. Apple's CallKit documentation states that CallKit displays the same calling interfaces as the Phone app for VoIP services. Android's Telecom stack similarly presents caller-facing metadata your app supplies through a Connection.
That split creates a localization gap:
- Chat strings live in a String Catalog or
strings.xml. System call labels are arguments to CallKit and Telecom APIs. - Push or VoIP payloads often arrive with a number-like
fromvalue before your backend returns a display name. - The callee's in-app language can disagree with the device language the OS uses for Contacts-based fallbacks.
- A late identity update may refresh on one device class and not another.
- Hardcoded English role strings such as "Support" or "Business line" never reach translators because they never lived in the catalog.
Practitioner reports match that model. A Stack Overflow author using Twilio Programmable Voice describes being unable to show an appropriate caller name on the iOS incoming-call screen when the invite exposes a number-like identifier. Another author reports updating localizedCallerName after the call starts and seeing the refresh fail on iPhone 14 Dynamic Island while other models worked (Stack Overflow question). A Twilio Voice Flutter issue asks how to show a Contacts or From display name instead of the default caller on iOS. Those reports are project-specific, not proof of a universal OS defect. They still show the integration questions your workflow must answer.
General iOS localization guides, including Phrase's iOS localization overview and the live i18nAgent iOS guide, cover catalogs and UI strings. They do not connect CallKit or Telecom caller labels to app-language ownership. The Android localization guide likewise stops short of Telecom display-name rules.
Build a system-call string inventory
Keep a separate system-call string inventory for role labels, unknown-caller fallbacks, and business display names instead of reusing chat UI copy that never reaches CallKit or Telecom.
For each call presentation string, record:
| Field | Why it matters |
|---|---|
| Stable key | Shared meaning across iOS and Android when the role matches |
| CallKit surface | localizedCallerName, handle value, video flag, update path |
| Telecom surface | setCallerDisplayName, presentation int, address, notification extras |
| Source of truth | Directory profile, push payload, local Contacts, or role template |
| Locale owner | Callee app language, device language, or Contacts-provided name |
| Timing | Available before reportNewIncomingCall / connection create, or only after a network round trip |
| Fallback | Unknown caller, number-only, or localized role such as "Support" |
| Verification | Lock screen, killed state, Dynamic Island, Android Telecom UI |
Map app locales to calling test locales deliberately. Shipping German chat does not prove the German CallKit path if every QA call still injects an English display name from a staging fixture.
Put role templates in the same translation pipeline as the rest of the app, with translator notes that the string appears on the OS call screen and should stay short. Avoid format strings that glue an unlocalized first name to an English role suffix unless every locale has been reviewed for that pattern.
Resolve the callee language before you report the call
Resolve CallKit localizedCallerName and Android setCallerDisplayName in the callee's effective app language before reporting the incoming call, not after the system UI has already painted an English fallback.
On iOS, Apple's localizedCallerName property on CXCallUpdate is the override you set when the system Contacts-derived name is wrong or incomplete. Apple's Making and receiving VoIP calls guide covers reporting incoming and outgoing calls through CallKit actions and updates. Build the first CXCallUpdate with a localized display name already resolved from:
- the callee's effective app locale (in-app picker or per-app language),
- a directory lookup keyed by stable user id,
- or a localized role template when the caller is a queue or campaign line.
Do not wait for the first frame of system UI to decide that English "Unknown" was acceptable.
On Android, call setCallerDisplayName with the localized string and an explicit presentation value before the connection becomes active in the UI. The self-managed calling guide describes caller-facing metadata such as display name and address for app-managed calls. Resolve that metadata from the same locale adapter your chat UI uses.
Pseudo-flow for the incoming path:
1. Receive VoIP push / connection request
2. Read callee effectiveLocale from app language store
3. Resolve displayName:
directory hit -> localized preferred name for effectiveLocale
else role key -> String(localized: "call.role.support")
else E.164 / handle -> format for display, still no English role glue
4. iOS: CXCallUpdate.localizedCallerName = displayName; reportNewIncomingCall
Android: connection.setCallerDisplayName(displayName, presentation)
5. If identity arrives later, write a second update (next section)
Keep person names as names. Localize role labels, queue names, and fallbacks. Do not machine-translate a person's display name unless your product policy explicitly supports transliteration and you store both forms.
Handle delayed identity without assuming every screen refreshes
Push payloads often carry a call UUID and a temporary handle before your backend returns the caller profile. Treat delayed identity resolution as a second CXCallUpdate or Connection display-name write, and verify Dynamic Island and lock-screen refresh because not every device applies the update identically.
On iOS, construct a new CXCallUpdate, set localizedCallerName (and the remote handle if it changed), and report the update through your provider. The Dynamic Island report above is a reminder to test the refresh path on current devices rather than assuming the first paint is the only paint that matters.
On Android, call setCallerDisplayName again when the profile arrives, and confirm the Telecom UI and any app-owned notification extras stay aligned. If you also mirror the name into a custom notification, update that notification in the same transaction so lock-screen users do not see two languages.
Timeouts need a product rule. If the directory lookup exceeds your reporting budget, show a localized unknown-caller or role fallback immediately, then upgrade the name when the lookup completes. Logging should record whether the first paint used fallback or resolved identity, without storing raw phone numbers in analytics if policy forbids it.
Prefer one locale adapter across CallKit, Telecom, and chat
Create one function that turns the app's effective locale plus a caller identity into a display package:
displayName: string ready for system UIhandle: CXHandle or Telecom addresssource: directory, contacts, role, or fallbacklocale: BCP 47 tag used for role templatespendingIdentity: whether a second update is expected
Both platforms should call that adapter. iOS then maps into CallKit. Android maps into Telecom. Chat history rendering can reuse the same display package so the in-thread call row does not disagree with the system screen the user just dismissed.
When you adopt LiveCommunicationKit for newer calling integrations, keep the same adapter. The framework changes how you initiate and coordinate conversations; it does not remove the need to supply localized caller-facing text.
Verify lock-screen, killed-state, and language-change paths
QA that only places a call while the app is foregrounded will miss the failure mode users hit. Build a matrix:
| Scenario | What to assert |
|---|---|
| App killed, VoIP push arrives | System UI shows localized name or localized fallback, not English role glue |
| Lock screen | Name readable; no mixed English role + localized name unless intentional |
| App language ≠ device language | System call label follows app-language policy you documented |
| Delayed directory lookup | First paint fallback, then updated name where the OS allows it |
| Video vs audio | Flags correct; labels still localized |
| Outgoing call | Start-call update uses the same adapter |
| After in-app language change | Next call uses new locale templates without reinstall |
Automate what you can with launch arguments and fixtures that inject known caller ids. Still run one manual pass on a locked device per major OS version you support. Record screenshots for the localized fallback and the resolved-name states.
Include localization owners in the release checklist: new call role strings require translator notes, screenshot review, and a Telecom/CallKit test account. Do not ship a new "Sales" queue line in English-only because it looked fine in the chat transcript.
Release gates that catch English drift
Add CI or pre-release checks that fail when:
- Call reporting code paths reference string literals for role or fallback labels outside the catalog.
- The system-call inventory lists a locale the chat catalog ships but calling fixtures never exercise.
- The locale adapter returns English fallbacks for a locale that has completed translation coverage.
- LiveCommunicationKit or CallKit migration code bypasses the shared display-name adapter.
Pair those gates with a short runbook: which staging users to call, which languages to force, and how to capture lock-screen evidence. You do not need perfect Contacts coverage for every number. You do need every string your app supplies to the system call UI to travel through the same localization pipeline as the rest of the product.
What good looks like
When the workflow is healthy, a callee who chose Spanish in the app sees Spanish role labels and fallbacks on the system incoming-call screen even if the phone UI language is English. A delayed profile lookup upgrades "Soporte" or a localized unknown-caller string to the correct person name without flashing an English intermediate the product did not authorize. Android and iOS share one inventory and one adapter, so a new business line does not ship English-only on one platform.
CallKit and Telecom will keep owning the chrome of the call screen. Your job is to stop treating their text arguments as afterthoughts. Inventory them, localize them, resolve them before first paint, update them deliberately, and verify the paths where the OS, not your SwiftUI or Compose tree, is what the user reads.
How i18nAgent helps
i18nAgent already helps teams keep iOS and Android resource catalogs in a reviewable translation workflow. Extend that pipeline to CallKit and Telecom keys instead of leaving caller labels as English literals inside push handlers. Keep system-call keys in the same project, mark them with a clear namespace, and require the lock-screen matrix before you ship a new locale.
Validation checklist
- Incoming CallKit and Telecom paths resolve display names from the shared locale adapter before first paint.
- Role labels and unknown-caller fallbacks live in the catalog with translator notes for OS call screens.
- Delayed identity updates write a second CallKit update or
setCallerDisplayNameand are tested on Dynamic Island and lock screen. - App language that disagrees with device language still produces the documented call-screen language.
- New business or queue lines cannot ship English-only on one platform.
The OS will keep owning call-screen chrome. Your caller labels do not have to stay English.
Call screens are one of several system surfaces your app text lands on; see how to localize app push notifications.
