Skip to content

feat(surveys): re-translate displayed survey on language change#686

Open
bs1180 wants to merge 20 commits into
mainfrom
posthog-code/survey-live-language-update
Open

feat(surveys): re-translate displayed survey on language change#686
bs1180 wants to merge 20 commits into
mainfrom
posthog-code/survey-live-language-update

Conversation

@bs1180

@bs1180 bs1180 commented Jun 30, 2026

Copy link
Copy Markdown

💡 Motivation and Context

Once a translated survey was displayed, later changes to the user's language (e.g. via setPersonProperties / identify with a language property) had no effect — the survey kept the language it was first rendered with. The translation was resolved only at display time.

This makes a survey already on screen follow the user's language: when the person properties used for flags change and a matching translation exists, the survey re-translates in place, keeping the current question and any progress.

Closes #66998

💚 How did you test it?

Added unit tests (run on the iOS simulator, where survey code compiles):

  • Changing the language person property while a survey is active pushes a re-translated survey to the delegate and updates the tracked survey language.
  • Re-resolving to the same language is a no-op (no churn of $survey_language or re-renders).
  • No active survey ⇒ no update pushed.
  • SurveyDisplayController.updateSurvey preserves the current question index and completion state, and ignores updates for a different / absent survey.

I was unable to run make test / the iOS build in this environment (no Xcode available), so CI should be relied on for the full build + simulator test run.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented from a Slack thread request, logged as #66998. Built with the PostHog Slack app (Claude Code). Key decisions:

  • Trigger: chose an internal NotificationCenter signal posted from PostHogRemoteConfig.setPersonPropertiesForFlags (the single chokepoint all person-property updates flow through) rather than sniffing the captured event stream — the $set/$identify ordering relative to the flag-property update is inconsistent, so the notification is the order-correct, robust hook.
  • UI update: added an optional updateSurvey(_:) to the delegate and made SurveySheet observe the display controller (instead of the snapshot passed by .sheet(item:)), so an in-place content swap with the same survey id renders live while preserving SwiftUI @State (current question, typed answers).
  • Guardrails: only commit + render when the matched language actually changes; the whole path is a no-op when no survey is displayed.

One known limitation worth a reviewer's eye: for choice questions, selection is keyed by the option's string value, so a mid-survey language switch can drop an in-progress selection on those question types (labels change). Text/rating answers are preserved.


Created with PostHog from a Slack thread

Once a survey was on screen, later updates to the user's `language` person
property had no effect — the translation was resolved only at display time. Now
the SDK re-resolves the survey language whenever the person properties used for
flags change and updates the on-screen survey in place, preserving the current
question and progress.

- Post an internal notification when person properties for flags change.
- Survey integration observes it, re-resolves translations for the active
  survey, and pushes an in-place update only when the matched language actually
  changed (so `$survey_language` and renders aren't churned needlessly).
- Add optional `updateSurvey(_:)` to `PostHogSurveysDelegate`; the default
  delegate updates the live survey via the display controller. The sheet now
  observes the controller so re-translations render without restarting.

Closes #66998

Generated-By: PostHog Code
Task-Id: 50affc3a-7798-47a6-94fe-c86b2b971dac
@bs1180 bs1180 self-assigned this Jun 30, 2026
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. PostHog/Surveys/PostHogSurveysDefaultDelegate.swift, line 120-123 (link)

    P1 Translation update dropped when survey has a display delay

    When updateSurvey is called while a survey is still within its surveyPopupDelaySeconds window, it correctly updates self.pendingSurvey to the re-translated copy. However, the DispatchWorkItem closure captures the original survey local variable at the time it is created, so when the delay fires it always calls displayController.showSurvey(survey) with the pre-update content. The work item should use self.pendingSurvey — the most-recent version — at the point it executes.

Reviews (1): Last reviewed commit: "feat(surveys): re-translate displayed su..." | Re-trigger Greptile

Comment thread PostHog/Surveys/SurveyDisplayController.swift
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-07-21 12:27:31 UTC
Duration: 218655ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 2752ms
Format Validation.Event Has Uuid 2766ms
Format Validation.Event Has Lib Properties 2799ms
Format Validation.Distinct Id Is String 2834ms
Format Validation.Token Is Present 2732ms
Format Validation.Custom Properties Preserved 2800ms
Format Validation.Event Has Timestamp 344ms
Retry Behavior.Retries On 503 11783ms
Retry Behavior.Does Not Retry On 400 2545ms
Retry Behavior.Does Not Retry On 401 2389ms
Retry Behavior.Respects Retry After Header 5391ms
Retry Behavior.Implements Backoff 21733ms
Retry Behavior.Retries On 500 9221ms
Retry Behavior.Retries On 502 8868ms
Retry Behavior.Retries On 504 9232ms
Retry Behavior.Max Retries Respected 21202ms
Deduplication.Generates Unique Uuids 3292ms
Deduplication.Preserves Uuid On Retry 9296ms
Deduplication.Preserves Uuid And Timestamp On Retry 16728ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7833ms
Deduplication.No Duplicate Events In Batch 2875ms
Deduplication.Different Events Have Different Uuids 2740ms
Compression.Sends Gzip When Enabled 409ms
Batch Format.Uses Proper Batch Structure 2718ms
Batch Format.Flush With No Events Sends Nothing 263ms
Batch Format.Multiple Events Batched Together 2834ms
Error Handling.Does Not Retry On 403 4806ms
Error Handling.Does Not Retry On 413 4702ms
Error Handling.Retries On 408 5395ms

Feature_Flags Tests

16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 2767ms
Request Payload.Flags Request Uses V2 Query Param 2727ms
Request Payload.Flags Request Hits Flags Path Not Decide 2803ms
Request Payload.Flags Request Omits Authorization Header 2838ms
Request Payload.Token In Flags Body Matches Init 2807ms
Request Payload.Groups Round Trip 2812ms
Request Payload.Groups Default To Empty Object 2739ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 2802ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 2824ms
Request Payload.Disable Geoip Omitted Defaults To False 2750ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 2671ms
Request Lifecycle.No Flags Request On Init Alone 69ms
Request Lifecycle.No Flags Request On Normal Capture 2799ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 5409ms
Request Lifecycle.Mock Response Value Is Returned To Caller 2747ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 2900ms

bs1180 added 2 commits June 30, 2026 14:22
- Keep PostHogSurveyIntegration.swift under the 1200-line SwiftLint limit by
  moving self-contained helper types (survey state, condition matching, rating
  buckets) into a new PostHogSurveyMatching.swift, registered in the Xcode
  project.
- Use the non-availability-gated `_surveysConfig` backing in tests; the public
  `surveysConfig` accessor is iOS 15+ only and broke the simulator build.
- Add the new optional `updateSurvey(_:)` delegate method to the public API
  snapshot.

Generated-By: PostHog Code
Task-Id: 50affc3a-7798-47a6-94fe-c86b2b971dac
Address Greptile review on #686:

- When a survey is within its display-delay window and its language updates,
  `updateSurvey` refreshes `pendingSurvey`, but the scheduled work item showed
  the captured (pre-update) copy. Read the latest `pendingSurvey` at execution
  time so the re-translated content is shown.
- Distinguish the "no survey displayed" vs "different survey" cases in the
  display controller's update log to make debugging clearer.

Generated-By: PostHog Code
Task-Id: 50affc3a-7798-47a6-94fe-c86b2b971dac
@turnipdabeets
turnipdabeets requested a review from a team July 2, 2026 20:54
@turnipdabeets

Copy link
Copy Markdown
Contributor

@lucasheriques I know you worked a bit on this, mind giving this a review?

@turnipdabeets
turnipdabeets marked this pull request as ready for review July 2, 2026 20:56
@turnipdabeets
turnipdabeets requested a review from a team as a code owner July 2, 2026 20:56
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "fix(surveys): apply latest translation w..." | Re-trigger Greptile

@turnipdabeets

Copy link
Copy Markdown
Contributor

Thanks @bs1180 instead of a CHANGELOG we actually need a changes, please see RELEASING.md

@ioannisj

ioannisj commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@bs1180 friendly ping on this one

Follow the repo's Changesets release flow: add a minor changeset describing the live survey re-translation feature and revert the direct CHANGELOG.md edit (the release workflow regenerates CHANGELOG.md from changesets).

Generated-By: PostHog Code
Task-Id: 50affc3a-7798-47a6-94fe-c86b2b971dac
@bs1180

bs1180 commented Jul 6, 2026

Copy link
Copy Markdown
Author

Hey @turnipdabeets and @ioannisj, sorry for the delay. This PR is the product of letting PostHog AI loose on a customer complaint :D I'm not an IOS dev so not sure if this implementation makes sense and I've done literally zero QA. Is that OK with you? Won't be offended this just gets rejected.

@ioannisj

ioannisj commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@bs1180 thank you, I can do the QA part and see also how this will affect Flutter surveys as well. we'll probably need to land a similar feature for posthog-android as well I imagine? To get some more context though, do you have the original link to the customer complaint? I personally don't think that on-the-fly language changes are the norm so I'm not completely sure we should be supporting this

@ioannisj

ioannisj commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Would like to know @turnipdabeets thoughts as well on this, maybe it's just me :)

@bs1180

bs1180 commented Jul 7, 2026

Copy link
Copy Markdown
Author

Yup totally understood - PostHog/posthog#66998 is the original feature request, and this is the customer Slack thread

@turnipdabeets

Copy link
Copy Markdown
Contributor

Would like to know @turnipdabeets thoughts as well on this, maybe it's just me :)

I think we need on-device QA, if you want to take that @ioannisj. Maybe we need to check out the sheet dismissal animation too. Not sure if we need @PostHog/team-surveys on par before making this change? And it makes sense to do this for the other mobile if it's needed, android flutter and RN.

@ioannisj ioannisj left a comment

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.

LG. Tested this on iOS and Flutter and left some minor comments. We'll probably want to follow this up on Android and Flutter side

Comment thread PostHog/PostHogRemoteConfig.swift Outdated
Comment thread PostHog/Surveys/PostHogSurveyIntegration.swift
Comment thread PostHog/Surveys/Utils/PostHogSurveyMatching.swift
@ioannisj

Copy link
Copy Markdown
Contributor

Hey @bs1180, want me to take this over if you've moved on to something else?

@bs1180

bs1180 commented Jul 16, 2026

Copy link
Copy Markdown
Author

Hey @ioannisj, sorry didn't see your latest comments. Yes please - as warned I'm not a mobile dev so my next steps would be to just keep prompting in Posthog Code :D

Thank you!

Comment thread PostHog/Surveys/SurveySheet.swift
Comment thread PostHog/Surveys/SurveySheet.swift
Comment thread PostHogTests/PostHogSurveyTranslationsTest.swift
Comment thread PostHog/Surveys/PostHogSurveyIntegration.swift Outdated
Comment thread PostHog/Surveys/PostHogSurveyIntegration.swift Outdated
Comment thread PostHog/Surveys/PostHogSurveysConfig.swift
Comment thread PostHog/PostHogRemoteConfig.swift Outdated
// Commit only if the update targets a different language than what's currently shown
guard self.activeSurveyLanguage != translations.matchedKey else { return nil }
self.activeSurveyLanguage = translations.matchedKey
self.activeSurveyQuestionTranslations = translations.questions

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.

[question] Once this commits mid-survey, survey sent stamps all $survey_questions (and $survey_language) with the final translations — answers given before the switch are reported with question text the user never saw, and survey shown vs sent disagree on the language. Acceptable for analytics, or should we freeze the language after the first response?

@ioannisj ioannisj Jul 21, 2026

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.

Yeah, tbh I think this is a product call? It may make sense to track each answer with the $survey_language that matches the UI at that point. There's no other SDK doing mid-display translation changes, so I can't really reference a previous implementation. @PostHog/team-surveys what's your call here? Should $survey_language correctly reflect the language shown to the user for each question or should we fix it so that all survey events match (even though the user may have seen something else)

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