Skip to content

Environment locale does not propagate into .sheet / .fullScreenCover content #501

Description

@vincentborko

.environment(\.locale, …) set on an ancestor does not reach the content of a .sheet or .fullScreenCover on Android. The presented content resolves Text catalog keys — and reports @Environment(\.locale) — in the device locale instead.

It is specifically and only \.locale. Every other environment value crosses the presentation boundary correctly.

Reproduction

Device language en-US, locale overridden once at the root:

struct ContentView: View {
    var body: some View {
        RootView()
            .environment(\.locale, Locale(identifier: "fr"))
            .environment(\.probeKey, "set-at-root")   // custom EnvironmentKey
            .environment(\.layoutDirection, .rightToLeft)
            .environment(\.timeZone, TimeZone(identifier: "Asia/Tokyo")!)
    }
}

struct RootView: View {
    @State var isSheetPresented = false

    var body: some View {
        NavigationStack {
            List {
                LocaleProbe(context: "inline")
                NavigationLink("Push") { LocaleProbe(context: "navigationDestination") }
                Button("Present a sheet") { isSheetPresented = true }
            }
        }
        .sheet(isPresented: $isSheetPresented) {
            LocaleProbe(context: "sheet")
        }
    }
}

struct LocaleProbe: View {
    let context: String
    @Environment(\.locale) var locale
    @Environment(\.probeKey) var probeKey
    @Environment(\.layoutDirection) var layoutDirection
    @Environment(\.timeZone) var timeZone

    var body: some View {
        VStack(alignment: .leading) {
            Text(verbatim: "locale: \(locale.identifier)")
            Text("Welcome")   // "Bienvenue" in fr, "Welcome" in the en base language
        }
        .task {
            logger.info("context=\(context) locale=\(locale.identifier) customKey=\(probeKey) layoutDirection=\(layoutDirection) timeZone=\(timeZone.identifier)")
        }
    }
}

Measured on an Android emulator (API 36, device language en-US), Skip Lite:

context \.locale custom key \.layoutDirection \.timeZone
inline fr set-at-root rightToLeft Asia/Tokyo
navigationDestination fr set-at-root rightToLeft Asia/Tokyo
.sheet en_US set-at-root rightToLeft Asia/Tokyo
.fullScreenCover en_US set-at-root rightToLeft Asia/Tokyo
.sheet + .environment(\.locale, …) re-applied on its own content fr set-at-root rightToLeft Asia/Tokyo

The Text("Welcome") in the sheet renders "Welcome"; inline it renders "Bienvenue".

Cause

EnvironmentValues.locale is the only environment value that is not stored in one of SkipUI's own composition locals. It reads and writes Compose's LocalConfiguration (Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift:481):

public var locale: Locale {
    get { Locale(LocalConfiguration.current.locales[0]) }
    set { /* copies LocalConfiguration.current, sets the locale, provides it */ }
}

.sheet and .fullScreenCover render through Material3's ModalBottomSheet, whose content composes in a separate Android window. Compose's ProvideAndroidCompositionLocals re-provides LocalConfiguration for that window from the platform, so the ancestor's overridden Configuration is replaced by the device one. SkipUI's own composition locals are unaffected, because they flow in through the parent composition context — which is why the custom key, layoutDirection, and timeZone all survive while \.locale does not.

Neither Presentation.swift nor PresentationRoot.swift resets the environment; they only add values. The loss happens entirely inside Compose, one layer below.

.alert is not affected — measured: an alert's message resolves against the presenter's locale both before and after the fix. Material3's ModalBottomSheet hosts its content in its own AbstractComposeView, whereas the Dialog an alert uses does not re-provide LocalConfiguration. So this is specific to the sheet/cover path. I did not measure menus or popovers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions