Skip to main content

Localize VPN Packet Tunnel and VpnService Surfaces Without Leaving English Connection Labels

2026-11-11

Localize VPN Packet Tunnel and VpnService Surfaces Without Leaving English Connection Labels

A multilingual VPN, secure-tunnel, or zero-trust client can look finished in onboarding, account settings, and the server picker, then fall apart the moment someone opens system VPN settings or the persistent connection notification. The Settings VPN list shows an English configuration name. The Packet Tunnel Provider extension surfaces English connection-state copy from its own bundle. Android posts an English VpnService foreground notification, channel label, or tunnel-start error even though every Activity string already shipped in German or Japanese.

Treat tunnel chrome with the same release discipline as the rest of the product UI. Inventory Settings-facing VPN display names, Packet Tunnel Provider extension strings, and Android VpnService status resources apart from the host catalog. Localize them on purpose, then prove them after real Language and Region changes.

Why VPN chrome drifts from the app language

VPN products split ownership across three places. The host app owns the account and server UI. The Network Extension or VpnService process owns the tunnel lifecycle. The operating system owns consent and parts of the Settings presentation. Apple's NEVPNManager API creates and manages Personal VPN configurations. Its localizedDescription property is the display name shown in the system's VPN settings UI, and when it is nil at creation time the system fills the calling app's display name. NETunnelProviderManager configures custom-protocol tunnels whose client side is a Packet Tunnel Provider extension. NEPacketTunnelProvider is the principal class for that com.apple.networkextension.packet-tunnel extension target, with its own Info.plist and Embed App Extensions build phase. Android's AOSP VpnService documents the system-bound VPN service, user consent through prepare, and the Builder that establishes the tunnel interface apps accompany with their own foreground notifications and status UI.

The localization gap shows up in six places:

  1. Host screens live in the main String Catalog or strings.xml. Settings VPN names are written through localizedDescription when the configuration is saved.
  2. Packet Tunnel Provider code and any user-visible extension strings resolve from the extension bundle, not from the host target alone.
  3. Android tunnel errors and foreground notification titles often start as English literals inside VpnService subclasses.
  4. System VPN consent (VpnService.prepare and related prompts) is system-owned. Translating the host app does not rewrite that chrome, and it also does not localize your notification channel.
  5. Quick Settings or Control Center VPN tiles are a different surface. Localizing a tile label does not localize Settings VPN names or VpnService notification text.
  6. After a device or per-app language change, a configuration saved with an English localizedDescription or a service that cached English notification text keeps looking unfinished until you rewrite those fields.

Practitioner materials line up with that split. A public Open-SSTP-Client pull request titled Localization i18n states the UI was hardcoded in English and moves user-facing copy into Android string resources so the app can follow the device language, including changes inside SstpVpnService such as replacing a hardcoded "Establish VPN connection" path with getString(...). WireGuard Android ships parallel English and German resources for tunnel create/start failures, vpn_start_error, vpn_not_authorized_error, and related tunnel labels (values-de/strings.xml). A Stack Overflow thread on whether app extensions need their own Localizable.strings reminds teams that extension bundles must actually contain the strings file they call through NSLocalizedString. Phrase's iOS localization overview and the live i18nAgent iOS and Android guides cover catalogs and UI strings. They do not connect Settings VPN display names, Packet Tunnel extension ownership, and VpnService status chrome in one workflow. Control Center or Quick Settings VPN tiles, marketing push campaigns, scheduled local reminders, and privacy purpose strings covered elsewhere are different problems: a translated tile or Info.plist purpose line is not the same as a localized tunnel configuration name and foreground notification.

Set Settings VPN display names from localized strings

Set NEVPNManager and NETunnelProviderManager localizedDescription from a localized string before saveToPreferences so Settings VPN list names are not stuck on the English app display-name fallback.

For each VPN configuration your app saves, record:

Field Why it matters
Stable key Shared meaning for "work tunnel", "personal VPN", or profile display name across iOS Settings and Android status UI
Apple surface NEVPNManager.localizedDescription or NETunnelProviderManager.localizedDescription before saveToPreferences
Android surface Connection name shown in your UI, foreground notification title/text, notification channel name
Bundle owner Host app for managers that save configurations; Packet Tunnel Provider extension for extension-local status copy; VpnService process for service notifications
Display path iOS Settings VPN list, in-app connection banner, Android ongoing notification, error dialogs after establish failures
Fallback Base localization that remains English-complete without masking missing tunnel translations in QA
Verification Device Language and Region, per-app language, fresh install, reconnect after language change, Settings list spot-check

Apple documents localizedDescription as the string used as the display name of the VPN configuration in the system's VPN settings UI. If you leave it nil, users see the app display name, often still English or a brand token that was never translated as a VPN profile label. Resolve the string with String(localized:) (or your catalog API) in the active app locale immediately before you assign it and save. If someone changes language in your app, update and re-save configurations that still show the old language in Settings. Renaming the App Store display name does not rewrite every VPN configuration already saved on device.

Localize the Packet Tunnel Provider extension bundle

Treat the Packet Tunnel Provider extension bundle as its own String Catalog or Localizable.strings target: host-app translations do not automatically ship inside com.apple.networkextension.packet-tunnel.

NEPacketTunnelProvider runs as an app extension. Xcode's extension target gets its own Info.plist (NSExtensionPointIdentifier = com.apple.networkextension.packet-tunnel) and must be embedded on the host. Any user-visible string the extension resolves (debug status surfaces you expose, shared app-group templates the extension reads, or localized content you present through extension-adjacent UI) needs membership in the extension target's resources. Export-for-localization behavior can fold extension strings into a host XLIFF entry; that does not remove the requirement that the built extension bundle contain the .lproj or String Catalog tables it loads at runtime. Follow the same discipline teams already use for share extensions: check Target Membership, confirm the strings file is copied into both bundles when shared, and run a device build of the extension under a non-English system language.

Keep protocol credentials and routing configuration out of translator-facing files. Username fields and server hostnames are not marketing copy. Translator context belongs on display names, reconnect messages, and any extension-owned labels people actually read.

Own VpnService notification and error strings in the service process

Resolve VpnService foreground notification titles, channel names, and tunnel error copy with getString (or your localization layer) inside the service process, then re-verify after device language changes because prepare consent UI is system-owned while status chrome is app-owned.

Android's VpnService path separates system consent from app chrome. prepare launches system UI the user must accept before your service can establish a tunnel. That dialog is not your strings.xml. What you do own is everything after authorization: the foreground notification required while the tunnel is up, the notification channel name and description, connection-state titles, and errors such as failed establish, unauthorized service, or DNS/config parse failures. Open-SSTP-Client's localization work shows the failure mode in reverse: English literals inside SstpVpnService until they move into resources. WireGuard's German table shows the success mode: vpn_start_error, vpn_not_authorized_error, tunnel create/start messages, and related labels ship as translated resources beside the English defaults.

Build the notification with resources resolved at post time, not with English constants captured at process start. If your service caches a title string in a field during onCreate, a later language change can leave the ongoing notification in the old language until the service restarts. Prefer resolving getString(R.string…) when you call startForeground or update the notification. Give translators length notes for compact status text on small phones and for channel names shown in system notification settings.

Verify after device and per-app language changes

Re-check VPN surfaces the same way you re-check widgets or Autofill extensions. After a Language and Region change on iOS, open Settings → VPN and confirm every configuration your app saved shows the expected localizedDescription. Connect once and confirm any app-owned status UI from the host or extension matches. On Android, switch the device language (and per-app language when you support it), restart the tunnel, and confirm the foreground notification title, text, and channel label, plus the next error path you can safely trigger in a staging build.

Release checks that catch English VPN chrome:

  • Every saved iOS VPN configuration sets localizedDescription from a localized string before saveToPreferences.
  • The Packet Tunnel Provider extension target contains the String Catalog or .lproj tables it loads; Target Membership is not host-only.
  • Android values-* folders include foreground notification, channel, and tunnel-error resources the VpnService references.
  • QA inspected Settings VPN names and the ongoing Android notification in the target language after a language change, not only the in-app server list in English.
  • System consent UI was recorded as out of scope for translators so nobody "fixes" it by editing the wrong file.

Automate what you can. Fail CI when VPN display-name keys present in English catalogs are missing in a required locale. Fail when VpnService-referenced notification strings are absent for a locale the host ships. Still keep a manual connect pass. Tunnel establish paths and On Demand rules can hide status chrome from automation that only launches the main Activity.

What not to confuse with this workflow

Do not treat this as a second article about Control Center or Quick Settings VPN tiles. Shade toggles remain a separate release surface with their own ControlWidget and TileService inventories. Do not reopen server-sent push campaigns or scheduled local notification language alignment here either. Those govern marketing payloads and reminder scheduling, not tunnel configuration names. Privacy purpose strings for camera or microphone stay in their own Info.plist workflow; system VPN consent is likewise system-owned and not a substitute for localizing localizedDescription and VpnService status chrome.

Keep the rule narrow. If your app creates a VPN configuration or runs a tunnel service people see in Settings or in an ongoing notification, that chrome needs an owner, a catalog entry for every display name and status string you control, translator context for compact system UI, and a check after language changes. Packet Tunnel Providers and VpnService are usually where users notice first when that owner is missing.