Skip to content

fix(profile): apply a saved language to the running app - #96

Merged
anilcancakir merged 1 commit into
mainfrom
feat/apply-saved-language-live
Aug 19, 2026
Merged

fix(profile): apply a saved language to the running app#96
anilcancakir merged 1 commit into
mainfrom
feat/apply-saved-language-live

Conversation

@anilcancakir

Copy link
Copy Markdown
Contributor

What

Saving a locale persisted it and confirmed it with a toast, and then the app went on speaking the old language until its next boot. Nothing re-pointed the translator after Auth.restore(), so /settings/language, the one screen whose entire purpose is changing the language, appeared to do nothing.

doUpdateProfile now applies it through Lang.setLocale, which loads the catalogue and rebuilds the tree.

Why here and not in the views

Five call sites pass language to this method:

  • magic_starter_language_view.dart:79
  • magic_starter_profile_settings_view.dart:196 and :1372
  • magic_starter_profile_sub_page_view.dart:433 and :537

Duplicating the application five times would guarantee one of them drifts.

Two details that are load-bearing

It is scheduled after the current frame. Lang.setLocale calls Magic.reload() -> MagicAppWidget.restart(), which swaps a UniqueKey above the whole tree and therefore unmounts the caller that is still inside its own await on this method. The language page clears an isolated save spinner in a finally, on a ValueNotifier its own state owns and disposes, so rebuilding before that runs would use it after disposal. Letting the caller finish costs one frame and keeps every call site correct without any of them knowing.

It asks for that frame explicitly. addPostFrameCallback does not request a frame, and an idle Flutter app stops producing them, so without scheduleFrame() the switch could sit unapplied until the next interaction happened to schedule one. That is not a guess:

registered, fired=0
after pump 1: 0
after scheduleFrame + pump: 1

A no-op when the language is absent or already current, so re-saving a profile without touching the selector does not flash a rebuild.

Testing

Two testWidgets cases in magic_starter_profile_controller_test.dart: a changed language switches Lang.current, an unchanged one does not. The first was confirmed to fail with the call removed.

flutter test: 1255 passing. flutter analyze --no-fatal-infos: clean. dart format .: applied.

Also verified end to end in a consuming app (uptizm, which resolves this package by path). Tapping Save on /settings/language with Türkçe selected, with no reload, turned the page title "Language" into "Dil", "Save" into "Kaydet" and the bottom tab bar into "Ana Sayfa / İzleyiciler / Olaylar / Durum". Before this change the same tap left every one of them in English.

Saving a locale persisted it and confirmed it with a toast, and then the app went
on speaking the old language until its next boot. Nothing re-pointed the
translator after `Auth.restore()`, so the one screen whose entire purpose is
changing the language appeared to do nothing.

`doUpdateProfile` now applies it through `Lang.setLocale`, which loads the
catalogue and rebuilds the tree. It lives here rather than in the views because
five call sites pass `language` to this method: the dedicated language page, the
profile settings view (twice) and the profile sub-page view (twice).

Two details that are load-bearing rather than incidental:

- It is scheduled AFTER the current frame. `Lang.setLocale` calls `Magic.reload()`,
  which swaps a key above the whole tree and unmounts the caller that is still
  inside its own `await` on this method. The language page, for one, clears an
  isolated save spinner in a `finally` on a `ValueNotifier` its own state owns and
  disposes, so rebuilding before that runs would use it after disposal.
- It asks for that frame explicitly. `addPostFrameCallback` does not request one,
  and an idle Flutter app stops producing frames, so without `scheduleFrame()` the
  switch could sit unapplied until the next interaction happened to schedule one.
  Measured: a post-frame callback registered outside a build does not fire on
  `tester.pump()` alone, and does fire after `scheduleFrame()`.

A no-op when the language is absent or already current, so re-saving a profile
without touching the selector does not flash a rebuild.
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@anilcancakir
anilcancakir merged commit b11921e into main Aug 19, 2026
2 checks passed
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.

1 participant