Skip to content

docs(capture): define $locale as the device language and add $app_language - #172

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog/record-device-and-app-language
Draft

posthog[bot] wants to merge 1 commit into
mainfrom
posthog/record-device-and-app-language

Conversation

@posthog

@posthog posthog Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Why

Nothing in openspec/specs/ says what $locale means. It is one of the oldest client auto-properties and no spec names it, so each client SDK invented its own meaning and the meanings disagreed:

  • posthog-ios 3.x sent Locale.current.languageCode — a bare language subtag, de for a de-CH device.
  • posthog-android 3.x sent Locale.getDefault(), which is the app's locale: a per-app language, a secondary device language, or whatever the app last passed to Locale.setDefault(). It also dropped the script (zh-TW for zh-Hant-TW) and sent the legacy codes iw, in, ji.
  • posthog-react-native sends the platform's full languageTag; posthog_flutter's pure-Dart desktop client sends the language code only; posthog-kmp's JVM context composes "${language}-${country}" off Locale.getDefault().

So $locale = de matched an iOS user and missed the Android one, and nothing reported the language the app actually displays — the thing most people are reaching for when they break down by $locale.

PostHog/posthog-ios#1009 and PostHog/posthog-android#927 (closing posthog-android#822) settle it together, as one cross-SDK change for the 4.0 majors. Both are breaking changes to a property's meaning, which is the kind of contract this repo exists to hold.

What changed

One new requirement in capture, Device language and app display language:

  • $locale is the first language in the device's own preferences, as a full BCP 47 tag with script and region (de-CH, zh-Hant-TW), with Unicode extension subtags (-u-fw-mon) stripped. It is not read from a process-global default the app can set on itself.
  • $app_language is the language the app's UI is displayed in, as the platform names that localization (de, pt-BR, zh-Hans).
  • Each is omitted rather than guessed when unavailable — und/C/POSIX for $locale, undetectable display language for $app_language, which never falls back to $locale. Detection may be limited by platform version (Android 13+).
  • Both are re-read per event, not captured once at setup, and not on the setup/capture path's critical section.
  • A public setAppLanguage() overrides $app_language: trimmed and normalized (" pt_BR " → pt-BR), cleared by null or blank, held in memory only and not cleared by reset().
  • Neither property is sent on flag evaluation requests.
  • The iOS/Android divergence on a per-app language is recorded with no winner: iOS surfaces it as the first preferred language so $locale follows it, Android keeps $locale device-wide and reports the choice through $app_language only.

Proposed, applied and archived on this branch per AGENTS.md: openspec/changes/archive/2026-11-04-record-device-and-app-language/.

Impact on other SDKs

posthog-react-native already sends a full language tag but has no $app_language or setter. posthog_flutter's desktop client sends a bare language code. posthog-kmp's JVM context reads Locale.getDefault(), the app-influenced source the requirement rules out. Those three have to change. An SDK that reports no language today (posthog-js browser) is not obliged to start.

Uncertain / for human review

  • Spec home. capture is where client ambient enrichment is specified, so the requirement went there. A separate event-context capability for the static/dynamic context properties as a whole may be the better long-run shape — none exists today and creating one is a bigger call than this change.
  • The per-app-language divergence is left open. Recording it without naming a winner is deliberate: the two 4.0 majors shipped it knowingly and the migration guides document it, but it means $locale is not cross-platform comparable for those users. Whether that is the intended end state is a question for a human.
  • Rules generalized from the two implementations rather than stated by both: stripping Unicode extension subtags (Android strips them; iOS was not observed to produce one), and "$app_language never falls back to $locale". Both are the behavior the PRs describe, but neither SDK states them as a contract.
  • acceptance/ is untouched. The harness has no steps for device language, so the eleven scenarios live in spec.md only and have not been run against an adapter.
  • compliance/ is untouched — no row is re-scored here.

Validation

openspec validate --specs --strict --no-interactive: 32 passed, 36 failed, identical to main's totals. validate capture --type spec --strict --json reports no issue other than the pre-existing "requirement text is very long" warnings that every long requirement in the repo already carries.


Created with PostHog Desktop

🤖 Generated with Claude Code

…guage

posthog-ios 4.0 and posthog-android 4.0 redefine $locale as the device's
first preferred language, as a full language tag, and add $app_language
for the language the app displays plus a setAppLanguage() override. The
capture spec said nothing about either property.

Proposed, applied and archived on this branch per AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 07d78af0-3d76-41ed-985e-0a27b54b645c
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.

0 participants