feat(surveys): add survey translations support#513
Conversation
posthog-android Compliance ReportDate: 2026-05-21 16:28:41 UTC
|
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ❌ | 304ms |
| Request Payload.Flags Request Uses V2 Query Param | ❌ | 43ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ❌ | 39ms |
| Request Payload.Flags Request Omits Authorization Header | ❌ | 20ms |
| Request Payload.Token In Flags Body Matches Init | ❌ | 21ms |
| Request Payload.Groups Round Trip | ❌ | 19ms |
| Request Payload.Groups Default To Empty Object | ❌ | 16ms |
| Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It | ❌ | 16ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ❌ | 17ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 17ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ❌ | 16ms |
| Request Lifecycle.No Flags Request On Init Alone | ❌ | 10ms |
| Request Lifecycle.No Flags Request On Normal Capture | ❌ | 2049ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ❌ | 15ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ❌ | 14ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ❌ | 13ms |
Failures
request_payload.request_with_person_properties_device_id
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.flags_request_uses_v2_query_param
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.flags_request_hits_flags_path_not_decide
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.flags_request_omits_authorization_header
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.token_in_flags_body_matches_init
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.groups_round_trip
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.groups_default_to_empty_object
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.person_properties_distinct_id_auto_populated_when_caller_omits_it
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.disable_geoip_false_propagates_as_geoip_disable_false
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.disable_geoip_omitted_defaults_to_false
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_payload.flag_keys_to_evaluate_contains_only_requested_key
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_lifecycle.no_flags_request_on_init_alone
Expected 0 /flags requests, got 1
request_lifecycle.no_flags_request_on_normal_capture
Expected 0 /flags requests, got 1
request_lifecycle.two_flag_calls_produce_two_remote_requests
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
request_lifecycle.mock_response_value_is_returned_to_caller
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
side_effect_events.get_feature_flag_captures_feature_flag_called_event
404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'
Mirrors the posthog-js / posthog-react-native translations feature: each Survey and SurveyQuestion can carry per-language overrides for user-visible strings. At display time, the SDK resolves a language (override → person property → device locale), applies any matching translation onto the display model, and stamps the matched key as `$survey_language` on every survey event when a translation actually took effect. - New `SurveyTranslation` / `SurveyQuestionTranslation` data classes. - `PostHogSurveysConfig.overrideDisplayLanguage` config option. - `findBestTranslationMatch` matching with case-insensitive exact match plus base-language fallback (`pt-BR` → `pt`). - `resolveSurveyTranslations` returns the original-cased dictionary key only when at least one user-visible field changed. - Display conversion (`toDisplaySurvey`) now accepts optional translation overrides; UI delegates receive pre-translated strings transparently. - `$survey_language` threaded through `survey shown`, `survey sent`, `survey dismissed`; `$survey_questions` reflects translated text. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
5a4ea9f to
eda5a3e
Compare
buildSurveyResponseProperties was reading the original question text from the raw Survey. Mirror posthog-js by stamping the translated text the user actually saw so $survey_questions on `survey sent` / `survey dismissed` matches the on-screen survey. Also rewrite the integration test fixtures to use full JSON strings (matching the JVM test pattern) — the previous Map-roundtrip path masked this bug because the assertion at line 108 was on $survey_questions, not on the display model directly. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
Drop KDoc that just paraphrases the code (property docs that restate the field's type, factory docs that restate the function signature). Keep comments that explain a non-obvious why — the description note on SurveyTranslation, per-question-type field applicability on SurveyQuestionTranslation, the matchedKey invariant on ResolvedSurveyTranslations, the storage-key duplication note, and the $survey_questions translated-text rationale. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
Per @turnipdabeets: - Read person properties from PostHogRemoteConfig.getPersonPropertiesForFlags() (now exposed via @PostHogInternal public) instead of cachePreferences. Drop the no-longer-needed readPersistedPersonProperties helper. - Drop the defensive try/catch around Locale.getDefault().toLanguageTag() — neither call throws. - Import SurveyQuestionTranslation so call sites use the short name. Also simplifies the matchedKey selection in resolveSurveyTranslations by tracking a single firstQuestionKey instead of an intermediate List<Pair<...>>. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
…o-op When a translation dictionary entry matches the target language but the translated values are identical to the originals, nothing on screen changes. Verify at the integration level that no $survey_language is stamped on shown/sent events in that case. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
| private var activeSurveyLanguage: String? = null | ||
| private var activeSurveyQuestionTranslations: List<SurveyQuestionTranslation?>? = null |
There was a problem hiding this comment.
are these two fields ever read, it looks like just written but not read so maybe they are dead code?
There was a problem hiding this comment.
great catch — those were just leftover from mirroring the iOS state-storage pattern. on android the callback closures capture resolvedLanguage and resolvedQuestionTranslations directly so the instance fields were never read. dropped in e2e1a56
Mirrors the same nit fix applied on iOS: extract a local helper so the
translations.keys.firstOrNull { it.lowercase() == ... } pattern is written
once instead of duplicated for exact + base-language fallback.
Generated-By: PostHog Code
Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
- Move the ResolvedSurveyTranslations data class to its own file so the file-name-matches-class ktlint rule passes naturally; drop the @file:Suppress("ktlint:standard:filename") workaround. - Add a changeset describing the survey translations feature. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
ioannisj
left a comment
There was a problem hiding this comment.
lgtm, just a very small comment
| /** | ||
| * Creates a display question from a survey question | ||
| * | ||
| * @param question The survey question to convert | ||
| * @return A display question or null if the question type is not supported | ||
| */ |
There was a problem hiding this comment.
we should maybe keep+update these doc comments
There was a problem hiding this comment.
restored + updated in e2e1a56 to cover the new translation params on fromSurveyQuestion, fromSurveyAppearance, and toDisplaySurvey
Per review: - Drop activeSurveyLanguage / activeSurveyQuestionTranslations instance fields — they were only ever written, never read. The integration's callback closures capture local resolvedLanguage and resolvedQuestionTranslations vals directly so the on-instance copies were dead state. - Restore doc comments on PostHogDisplaySurveyQuestion.fromSurveyQuestion, PostHogDisplaySurveyAppearance.fromSurveyAppearance, and PostHogDisplaySurvey.toDisplaySurvey now that they cover the new translation parameters. Matches the existing display-model doc style. Generated-By: PostHog Code Task-Id: 22f7fe99-61e4-4d54-90c9-24788c083799
Summary
Brings the posthog-js / posthog-react-native survey translations feature to the native SDK. Surveys can carry per-language overrides for user-visible strings via a
translationsmap keyed by language code. At display time the SDK resolves a language (init override → person propertylanguage→ device locale), applies any matching translation onto the display model, and stamps the matched key as$survey_languageon every survey event when a translation actually took effect.translationsonSurveyand on eachSurveyQuestionsubtype.PostHogSurveysConfig.overrideDisplayLanguage(init option).pt-BR→pt); returns the original-cased dict key for event reporting.matchedKeywhen at least one user-visible field actually changed.survey shown,survey sent,survey dismissedall carry$survey_languagewhen matched;$survey_questionsreflects translated text.Companion PR: PostHog/posthog-ios#601
Test plan
All tests pass in CI. New coverage:
$survey_languagepresent on all three events when matched, absent otherwise; translated text in$survey_questions.Created with PostHog Code