Display StringProtocol Text content verbatim instead of localizing it - #136
Open
vincentborko wants to merge 1 commit into
Open
Display StringProtocol Text content verbatim instead of localizing it#136vincentborko wants to merge 1 commit into
vincentborko wants to merge 1 commit into
Conversation
SwiftUI documents Text.init<S>(_:) as displaying a stored string without localization, but it wrapped its argument in a LocalizedStringKey, so an already-resolved string was resolved against the string catalog a second time. Every other S : StringProtocol overload forwards to Text(title) and inherited the same divergence. Fixes skiptools#135
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #135.
Labels: bug
Text.init<S>(_ content: S) where S : StringProtocolwrapped its argument in aLocalizedStringKey, so an already-resolved string was looked up in the string catalog a second time. SwiftUI documents that overload as displaying "a stored string without localization" — it is the verbatim path, and it is the one aStringvariable binds to, since@_disfavoredOverloadsends literals to theLocalizedStringKeyoverload instead.The failure this produces: an app that resolves its own strings — for an in-app language setting independent of the device language, say — hands
Texta finished string. If that string is also a key in the catalog (which it is whenever the catalog's source language is the app's own, since then the keys are the source strings), it is translated again into whatever\.localehappens to be, and the screen renders in two languages. Nothing signals it, because the same call on iOS is already verbatim.Changes
Text.init<S>(_:)buildsTextSpec(verbatim:)rather thanTextSpec(key:).That initializer is the funnel for the rest of the module. Of the 76 implemented
S : StringProtocoloverloads across 22 files —grep -rn "S : StringProtocol" Sources/SkipSwiftUIreturns 130 lines, less 13 commented-out declarations and 41 marked@available(*, unavailable)— 73 forward intoText(title), coveringButton,Toggle,Picker,DatePicker,Stepper,ProgressView,Link,NavigationLink,Menu,Section,GroupBox,DisclosureGroup,Label,LabeledContent,TextField,SecureField,.alert,.confirmationDialog,.searchable,.navigationTitle,.navigationBarTitle,.accessibilityLabeland.accessibilityValue. The two that do not are theTabViewinitializers atContainers/TabView.swift:525and:545, which discard their title argument entirely.Those forwarding overloads follow the SwiftUI contract now too, and they matter more than
Textitself in practice because they have noverbatim:escape hatch — the workaround there is rewriting the call into the label-closure form.Skip Lite is deliberately unaffected:
SkipUI.Texthas a non-genericinit(_ key: String)that treats aStringas a key, because after transpilation to Kotlin a literal and a variable are indistinguishable at the call site (CHANGELOG, 2023-12-22). Skip Fuse compiles with real Swift overload resolution, so the distinction is available here and the SwiftUI behaviour can be matched.Testing
Tests/SkipSwiftUITests/TextTests.swiftcovers theStringandSubstringforms producing a verbatim spec, plus the string literal still producing a key spec so the localized path stays pinned.Verified by mutation rather than by a green check alone: with the one-line change reverted,
testStringProtocolInitIsVerbatimandtestSubstringInitIsVerbatimfail (spec.verbatimnil,spec.keyLocalizedStringKey("Welcome")) whiletestStringLiteralInitRemainsLocalizedkeeps passing; with the change applied all three pass.Full
swift test: 20 tests, 7 skipped, 0 failures, including the transpiled Robolectric suites and the existingFuseComposeUITestsCompose rendering tests.:SkipSwiftUI:compileDebugKotlinand the other module Kotlin compilations succeed.One limitation worth stating: the new assertions sit inside
#if !SKIP, matching the existing tests in this target that need@testablefor internal API, so they execute host-native rather than under Robolectric.SkipSwiftUIis a native (Fuse) module, so that is the same Swift source that compiles for Android — what is being pinned here is Swift overload resolution, not transpiler behaviour.Thank you for contributing to the Skip project! Please use this space to describe your change and add any labels (bug, enhancement, documentation, etc.) to help categorize your contribution.
Please review the contribution guide at https://skip.dev/docs/contributing/ for advice and guidance on making high-quality PRs.
Skip Pull Request Checklist:
swift testAI assistance was used to locate the divergence, trace which overloads funnel through this initializer, write the tests, and draft this description. Verification steps taken: the contract was read from Apple's own documentation for this overload, whose summary is "Creates a text view that displays a stored string without localization" and which states that a string literal instead triggers the
LocalizedStringKeyoverload; the fan-out claim was established by reading everyStringProtocoloverload in the module rather than trusting a summary of them; and the tests were run with the fix reverted first, to confirm they fail for the stated reason before confirming they pass with it.