International phone number input breaks when an app guesses the calling country from its language, device region, or SIM. A French interface does not prove that the number is French, and a traveler may enter a home number while using a foreign network. Bad guesses reject valid numbers, save punctuation as identity data, or send an SMS to the wrong destination. Make the calling country explicit and editable. Keep partial text separate from the parsed number, use current numbering metadata at commit, store one canonical international value, and format it separately for display. The same contract can serve sign-up, authentication, checkout, and contact forms without tying phone identity to locale.
Why locale cannot identify a phone number
Locale describes language and regional conventions. A numbering plan describes how a telephone network interprets digits. They overlap sometimes, but they are not the same system.
Several signals may be available when the field opens:
- The app's effective language.
- The device region.
- The SIM or network country.
- An account's billing or profile country.
- The current IP location.
- A country already selected elsewhere in the form.
None of these proves where the number belongs. A person can use an English app in Japan, keep a Singapore number, and pay with a US card. A signal may supply the initial suggestion, but the country selector must remain visible and editable. Once typing starts, a change in another locale signal must not alter the number.
The ITU Recommendation E.164 page identifies the international public telecommunication numbering plan that governs the global number namespace. Language tags still do not map reliably to calling codes. The field must gather enough explicit information to parse the user's text under the intended numbering region.
Define a field contract before choosing a widget
A country picker and a text box need a data contract. Define what each value means and when it can change.
PhoneInputState
selectedRegion: "LB"
rawText: "81 932 452"
editStatus: EDITING
parsedE164: null
extension: null
metadataVersion: "bundled-library-version"
errorCode: null
selectedRegion is a numbering region, not a UI locale. rawText belongs to the current edit session. parsedE164 changes only after a complete successful parse. The extension is separate because it is not part of the globally routable subscriber number. The metadata version helps diagnose behavior changes after numbering data is updated.
Set a narrow precedence rule for the initial region. For example, prefer a previously confirmed profile country, followed by a country explicitly selected in the current form. The device region can be the last convenience default. Present the result as a suggestion, and freeze it once the user begins editing. Later IP or SIM data must not replace it silently.
Country selection also needs accessible names. A flag is decorative and ambiguous on its own. Show the localized country name and calling code, support text search, expose a clear accessibility label, and preserve the selected item when the interface language changes.
Preserve partial input while formatting
The field has to support editing before it can validate a complete number. Empty text, a leading plus sign, an opening parenthesis, or a short national prefix may be incomplete but valid as an intermediate state. Final validation on every keystroke produces noisy errors and often moves the cursor when the formatter rewrites the field.
Use an as-you-type formatter only for presentation. Google's maintained libphonenumber project includes parsing, validation, number matching, and as-you-type formatting for international numbers. Its metadata changes through regular releases, which is why a frozen regular expression is not a durable substitute.
Keep the raw edit session and cursor stable:
function onPhoneEdit(newText, cursor, region, metadata): EditResult {
cleaned = removeVisualSeparatorsOnlyForClassification(newText)
if containsForbiddenLetters(cleaned):
return keepTextWithError(newText, cursor, INVALID_CHARACTER)
if looksPartial(cleaned):
return formattedPartial(newText, cursor, region, metadata)
return formattedEditable(newText, cursor, region, metadata)
}
Leave national trunk prefixes and country codes alone while the person is typing. Those operations require a selected region and a complete parse. When validation fails, preserve the text so the user can correct one character instead of entering the whole number again.
A practitioner described an Android formatting failure for an international number: the same formatting call handled a US number but returned a Lebanese number unchanged. That is an author report about one implementation, not a universal Android guarantee. It demonstrates why the visible plus sign and country code do not replace explicit region handling and tested metadata.
Parse and validate only at a commit boundary
Commit on form submission or a deliberate field completion event. Parsing should receive the selected region, raw text, and current numbering metadata together.
function commitPhone(state, metadata): Result<CanonicalPhone> {
if state.rawText is empty:
return error(REQUIRED)
candidate = parsePhone(
text = state.rawText,
defaultRegion = state.selectedRegion,
metadata = metadata
)
if candidate failed:
return error(INVALID_FOR_SELECTED_REGION)
if not isPossibleNumber(candidate):
return error(IMPOSSIBLE_LENGTH_OR_PREFIX)
if not isValidNumber(candidate):
return error(NOT_ASSIGNED_BY_CURRENT_METADATA)
return success({
e164: formatE164(candidate),
extension: parseExtensionSeparately(state.rawText),
region: resolvedRegion(candidate)
})
}
Possible and valid answer different questions. A possible number has a plausible structure and length. A valid number also matches current allocation metadata. The product's risk determines which test to require. Account recovery may require strict validity plus possession verification, while a contact notebook may preserve a plausible number that current metadata cannot confirm. Document the choice rather than treating one library method as a universal product rule.
Validation cannot prove ownership. If the number controls login, recovery, money movement, or sensitive notifications, verify possession with a one-time challenge and rate limits. Parsing only answers whether the number is structurally callable under known metadata. The person entering it may not control the destination.
The WHATWG standard defines a telephone input control, but it does not impose one global number syntax. On web, type="tel" can improve keyboard selection and browser behavior. It does not remove the need for region-aware parsing on the application boundary.
Store identity separately from display
Store the canonical international number in E.164 form when the product needs a globally routable identity. Keep the extension in a separate field. Do not store spaces, parentheses, local trunk prefixes, or a translated country name as part of the identity key.
The stored record can stay small:
VerifiedPhone
e164: "+96181932452"
extension: null
verificationStatus: VERIFIED
verifiedAt: "timestamp"
parserMetadataVersion: "library-version"
Use e164 for uniqueness, server calls, and provider requests. Generate national or international display text from the canonical value and the viewer's context. Preserve the exact user-entered text only when the product has a justified support or audit need, and protect it as personal data.
Client and server need compatible metadata and the same policy. A form still fails if the mobile client accepts a number that the API rejects. When server metadata is newer, return a typed validation error rather than rewriting the number silently. Track library versions and update them deliberately because calling code allocations and valid ranges change.
Handle country changes without corrupting text
The same digits can parse differently after a country change. Preserve a national-format value until the user confirms how the new region should interpret it.
Use these decision rules:
- If the field is empty, switch the region and update the placeholder.
- If the text starts with
+, parse it as an international number and offer to align the selector with the resolved region. - If the text is national format, preserve the digits and ask the user to confirm reinterpretation under the new region.
- If the number was already committed, start a new edit session instead of mutating the stored value.
- If the app language changes, relocalize labels and country names but keep the selected numbering region and raw text unchanged.
Shared country calling codes need special care. One calling code can serve several regions, so a code alone may not resolve the exact country. The parser can use following digits and metadata where possible, but the UI should not pretend that every partial input has one certain flag or territory.
Return useful errors without exposing internals
Use stable error codes and localize their presentation:
REQUIRED: enter a phone number.INVALID_CHARACTER: remove unsupported characters.INCOMPLETE: finish entering the number.INVALID_FOR_SELECTED_REGION: check the country and number.NOT_SUPPORTED: this product cannot send to the resolved destination.VERIFICATION_FAILED: the challenge could not be confirmed.
Keep raw parser exceptions and provider responses out of the interface. Protected telemetry can record the selected region, error code, client metadata version, and server metadata version. Avoid logging the complete number. A redacted suffix is still personal data, so include it only when the product's logging policy permits it.
Separate delivery policy from syntax. A structurally valid premium, fixed line, or toll free number may still be unsuitable for SMS verification. The library can classify types when metadata supports it, but the sending provider and product policy determine whether a destination is allowed.
Verify the full phone input lifecycle
Use maintained test fixtures from several numbering plans rather than one source country. Include:
- National and international forms for every launch market.
- Countries with and without trunk prefixes.
- Shared calling codes and ambiguous partial input.
- Short, overlong, and impossible prefixes.
- Extensions stored separately from the canonical number.
- Paste containing spaces, parentheses, hyphens, or a leading plus sign.
- Country changes before and after commit.
- App language changes during an active edit.
- Client and server behavior with the same metadata version.
- A planned metadata upgrade with before and after expectations.
UI tests should verify keyboard choice, cursor stability, country search, screen reader labels, error focus, and preservation of raw text after failure. End-to-end tests should submit the canonical value to the real API boundary in a nonproduction environment and confirm that possession verification uses the intended destination.
Version golden fixtures with the phone library. When metadata changes a result, review the affected country and product policy instead of updating snapshots blindly. A changed validity result may be a correct allocation update or a regression in the adapter.
Replace one country assumption now
Choose the highest conversion phone field in the product. Trace how it picks a country, formats partial input, parses on submit, stores the result, and verifies possession. Add fixtures for one domestic number, two foreign numbers, an incomplete edit, a country change, and a client-server metadata mismatch. Then replace locale-derived country logic with an explicit numbering-region field and canonical storage. Do not roll the shared component out to every form until this path passes the complete lifecycle.
References
- ITU Recommendation E.164 identifies the in-force international public telecommunication numbering plan.
- Google libphonenumber documents international parsing, formatting, validation, matching, as-you-type behavior, and metadata releases.
- WHATWG telephone input state defines browser telephone input behavior without prescribing one universal telephone syntax.
- Stack Overflow: Android international-number formatting failure supplies a practitioner report about region-dependent formatting behavior.
