Skip to content

Display StringProtocol Text content verbatim instead of localizing it - #136

Open
vincentborko wants to merge 1 commit into
skiptools:mainfrom
vincentborko:pr/text-verbatim-stringprotocol
Open

Display StringProtocol Text content verbatim instead of localizing it#136
vincentborko wants to merge 1 commit into
skiptools:mainfrom
vincentborko:pr/text-verbatim-stringprotocol

Conversation

@vincentborko

Copy link
Copy Markdown
Contributor

Fixes #135.

Labels: bug

Text.init<S>(_ content: S) where S : StringProtocol wrapped its argument in a LocalizedStringKey, 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 a String variable binds to, since @_disfavoredOverload sends literals to the LocalizedStringKey overload 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 Text a 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 \.locale happens 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>(_:) builds TextSpec(verbatim:) rather than TextSpec(key:).

That initializer is the funnel for the rest of the module. Of the 76 implemented S : StringProtocol overloads across 22 files — grep -rn "S : StringProtocol" Sources/SkipSwiftUI returns 130 lines, less 13 commented-out declarations and 41 marked @available(*, unavailable) — 73 forward into Text(title), covering Button, Toggle, Picker, DatePicker, Stepper, ProgressView, Link, NavigationLink, Menu, Section, GroupBox, DisclosureGroup, Label, LabeledContent, TextField, SecureField, .alert, .confirmationDialog, .searchable, .navigationTitle, .navigationBarTitle, .accessibilityLabel and .accessibilityValue. The two that do not are the TabView initializers at Containers/TabView.swift:525 and :545, which discard their title argument entirely.

Those forwarding overloads follow the SwiftUI contract now too, and they matter more than Text itself in practice because they have no verbatim: escape hatch — the workaround there is rewriting the call into the label-closure form.

Skip Lite is deliberately unaffected: SkipUI.Text has a non-generic init(_ key: String) that treats a String as 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.swift covers the String and Substring forms 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, testStringProtocolInitIsVerbatim and testSubstringInitIsVerbatim fail (spec.verbatim nil, spec.key LocalizedStringKey("Welcome")) while testStringLiteralInitRemainsLocalized keeps passing; with the change applied all three pass.

Full swift test: 20 tests, 7 skipped, 0 failures, including the transpiled Robolectric suites and the existing FuseComposeUITests Compose rendering tests. :SkipSwiftUI:compileDebugKotlin and 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 @testable for internal API, so they execute host-native rather than under Robolectric. SkipSwiftUI is 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:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

AI 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 LocalizedStringKey overload; the fan-out claim was established by reading every StringProtocol overload 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.


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
@cla-bot cla-bot Bot added the cla-signed label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text(_:) StringProtocol overload localizes its argument instead of displaying it verbatim

1 participant