Skip to content

feat(surveys): add survey translations support#513

Merged
lucasheriques merged 8 commits into
mainfrom
posthog-code/survey-translations
May 21, 2026
Merged

feat(surveys): add survey translations support#513
lucasheriques merged 8 commits into
mainfrom
posthog-code/survey-translations

Conversation

@lucasheriques
Copy link
Copy Markdown
Contributor

@lucasheriques lucasheriques commented May 14, 2026

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 translations map keyed by language code. At display time the SDK resolves a language (init override → person property language → 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.

  • Wire model: translations on Survey and on each SurveyQuestion subtype.
  • Config: PostHogSurveysConfig.overrideDisplayLanguage (init option).
  • Matching: case-insensitive exact match with base-language fallback (pt-BRpt); returns the original-cased dict key for event reporting.
  • Resolution: only stamps matchedKey when at least one user-visible field actually changed.
  • Event threading: survey shown, survey sent, survey dismissed all carry $survey_language when matched; $survey_questions reflects translated text.

Companion PR: PostHog/posthog-ios#601

Test plan

All tests pass in CI. New coverage:

  • Wire-format decoding round-trip (Gson + serializer).
  • Matching algorithm: exact, case-insensitive, base-language fallback, no-match, no-fallback-without-hyphen.
  • Priority chain: override → person prop → device locale.
  • Display model translation with field-level fallback to original.
  • Integration: $survey_language present on all three events when matched, absent otherwise; translated text in $survey_questions.

Created with PostHog Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

posthog-android Compliance Report

Date: 2026-05-21 16:28:41 UTC
Duration: 2885ms

⚠️ Some Tests Failed

0/16 tests passed, 16 failed


Feature_Flags Tests

⚠️ 0/16 tests passed, 16 failed

View Details
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
@lucasheriques lucasheriques force-pushed the posthog-code/survey-translations branch from 5a4ea9f to eda5a3e Compare May 14, 2026 07:19
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
Comment thread posthog/src/main/java/com/posthog/internal/surveys/SurveyLanguageDetection.kt Outdated
@turnipdabeets turnipdabeets requested a review from a team May 18, 2026 22:16
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
Comment on lines +593 to +594
private var activeSurveyLanguage: String? = null
private var activeSurveyQuestionTranslations: List<SurveyQuestionTranslation?>? = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these two fields ever read, it looks like just written but not read so maybe they are dead code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@turnipdabeets turnipdabeets requested a review from ioannisj May 20, 2026 14:02
Copy link
Copy Markdown
Contributor

@ioannisj ioannisj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just a very small comment

Comment on lines -22 to -27
/**
* 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
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should maybe keep+update these doc comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@lucasheriques lucasheriques merged commit 2282895 into main May 21, 2026
14 checks passed
@lucasheriques lucasheriques deleted the posthog-code/survey-translations branch May 21, 2026 16:48
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.

3 participants