View Issue Details

IDProjectCategoryView StatusLast Update
0011946Talerwallet (iOS App)public2026-11-05 00:42
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeveritytweakReproducibilityalways
Status newResolutionopen 
Summary0011946: Five buttons are never translated: a literal passed through a String loses its LocalizedStringKey
DescriptionFive buttons show English text in every other language, although
Localizable.xcstrings has a translation for all five of them in all five
shipped languages.

Seen live: running the app with -AppleLanguages (pl), 112 labels on screen
rendered in Polish and exactly one did not -- the "Finished" button, which
the catalogue translates as "Zakończono". So this is a lookup that does not
happen, not a translation that is missing.

Cause
-----
SwiftUI localises `Button("Finished")` because the literal binds to
LocalizedStringKey. Bind the same literal to a variable first and the
variable is a String, the LocalizedStringKey overload is no longer selected,
and the text is shown verbatim:

    let title = "Finished"
    Button(title) { ... } // String overload -- never looked up

The five places, all compiled into the shipping GNU_Taler target:

    Views/Sheets/DevExperimentView.swift:42 "Finished"
    Views/Settings/DebugSettingsView.swift:145 "Withdraw"
    Views/Settings/DebugSettingsView.swift:159 "Withdraw"
    Views/Settings/DebugSettingsView.swift:175 "Withdraw"
    Views/Settings/DebugSettingsView.swift:191 "Refresh"

The catalogue has de/es/fr/pl/uk for "Finished", "Withdraw" and "Refresh",
all marked translated, so each of these is a translation that was written,
paid for in review time, and then not used.

How the list was made
---------------------
A scan over every .swift file for a local `let`/`var` assigned a bare string
literal that is then passed as the first argument of Text/Button/Label/
Toggle/navigationTitle, keeping only those whose literal appears in
Localizable.xcstrings. Five occurrences; these are all of them. Other
`let title = "..."` lines in DebugSettingsView ("Demo 1", "Loop", ...) are
not affected because those strings are not in the catalogue at all and are
developer labels.

Suggested fix (patch attached)
------------------------------
Inline the literal so it binds to LocalizedStringKey again. No other change.
`Button(LocalizedStringKey(title))` would work equally well where a variable
is genuinely wanted.

These are developer-facing rows, so the user-visible impact is small. The
reason to fix it is that the same mistake is invisible at the call site and
silently discards work that was already done.

How to reproduce
----------------
1. Run the app with a non-English language, e.g.

       xcrun simctl launch --console <device> com.taler-systems.talerwallet-1 \
           -AppleLanguages '(pl)' -AppleLocale pl -developerMode YES

2. Settings -> Developer, or open a taler://dev-experiment/ URI.
3. The buttons read "Withdraw", "Refresh" and "Finished" in English while
   everything around them is translated.

Found by the UI fuzzing harness (branch dev/phoenix-bot/fuzzbang), which runs
every sixth episode in a shipped translation; its l10n oracle compares what is
on screen against Localizable.xcstrings and reported:

    on 'Finished' the app shows English although the catalogue has a
    translated pl unit: 'Zakonczono'

112 other labels on the same screen were in Polish, so the language was
applied; only these were not looked up.

Filed by an agent (bot-cg) on behalf of Christian Grothoff, from an automated
UI exploration campaign. Left unassigned deliberately.
TagsNo tags attached.
Attached Files
l10n-literals.diff (3,216 bytes)   
diff --git a/TalerWallet1/Views/Settings/DebugSettingsView.swift b/TalerWallet1/Views/Settings/DebugSettingsView.swift
index f3621f2a..73b1677e 100644
--- a/TalerWallet1/Views/Settings/DebugSettingsView.swift
+++ b/TalerWallet1/Views/Settings/DebugSettingsView.swift
@@ -141,8 +141,7 @@ struct DebugSettingsView: View {
                 } // showDevelopItems
                     SettingsItem(name: String("DEMO"), id1: "demo1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw DEMO KUDOS")
@@ -155,8 +154,7 @@ struct DebugSettingsView: View {
                     }.id("demo1withdraw")
                     SettingsItem(name: String("TEST"), id1: "test1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw TESTKUDOS")
@@ -171,8 +169,7 @@ struct DebugSettingsView: View {
                 if showDevelopItems {
                     SettingsItem(name: String("HEAD"), id1: "head1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw HEAD KUDOS")
@@ -187,8 +184,7 @@ struct DebugSettingsView: View {
                     SettingsItem(name: String("Run Dev Experiment Refresh"),
                                   id1: "applyDevExperiment",
                           description: "dev-experiment/insert-pending-refresh") {
-                        let title = "Refresh"
-                        Button(title) {
+                        Button("Refresh") {
                             Task { // runs on MainActor
                                 symLog.log("running applyDevExperiment Refresh")
                                 try await model.setConfig(setTesting: true)
diff --git a/TalerWallet1/Views/Sheets/DevExperimentView.swift b/TalerWallet1/Views/Sheets/DevExperimentView.swift
index b57bbd8c..7f629c82 100755
--- a/TalerWallet1/Views/Sheets/DevExperimentView.swift
+++ b/TalerWallet1/Views/Sheets/DevExperimentView.swift
@@ -38,8 +38,7 @@ struct DevExperimentView: View {
                     .talerFont(.title2)
                     .padding()
                 Spacer()
-                let title = "Finished"
-                Button(title) {
+                Button("Finished") {
                     dismiss()
                 }
                 Spacer()
l10n-literals.diff (3,216 bytes)   
inline-the-literal.diff (3,216 bytes)   
diff --git a/TalerWallet1/Views/Settings/DebugSettingsView.swift b/TalerWallet1/Views/Settings/DebugSettingsView.swift
index f3621f2a..73b1677e 100644
--- a/TalerWallet1/Views/Settings/DebugSettingsView.swift
+++ b/TalerWallet1/Views/Settings/DebugSettingsView.swift
@@ -141,8 +141,7 @@ struct DebugSettingsView: View {
                 } // showDevelopItems
                     SettingsItem(name: String("DEMO"), id1: "demo1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw DEMO KUDOS")
@@ -155,8 +154,7 @@ struct DebugSettingsView: View {
                     }.id("demo1withdraw")
                     SettingsItem(name: String("TEST"), id1: "test1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw TESTKUDOS")
@@ -171,8 +169,7 @@ struct DebugSettingsView: View {
                 if showDevelopItems {
                     SettingsItem(name: String("HEAD"), id1: "head1with",
                           description: String("Get money for testing")) {
-                        let title = "Withdraw"
-                        Button(title) {
+                        Button("Withdraw") {
                             withDrawDisabled = true    // don't run twice
                             Task { // runs on MainActor
                                 symLog.log("Withdraw HEAD KUDOS")
@@ -187,8 +184,7 @@ struct DebugSettingsView: View {
                     SettingsItem(name: String("Run Dev Experiment Refresh"),
                                   id1: "applyDevExperiment",
                           description: "dev-experiment/insert-pending-refresh") {
-                        let title = "Refresh"
-                        Button(title) {
+                        Button("Refresh") {
                             Task { // runs on MainActor
                                 symLog.log("running applyDevExperiment Refresh")
                                 try await model.setConfig(setTesting: true)
diff --git a/TalerWallet1/Views/Sheets/DevExperimentView.swift b/TalerWallet1/Views/Sheets/DevExperimentView.swift
index b57bbd8c..7f629c82 100755
--- a/TalerWallet1/Views/Sheets/DevExperimentView.swift
+++ b/TalerWallet1/Views/Sheets/DevExperimentView.swift
@@ -38,8 +38,7 @@ struct DevExperimentView: View {
                     .talerFont(.title2)
                     .padding()
                 Spacer()
-                let title = "Finished"
-                Button(title) {
+                Button("Finished") {
                     dismiss()
                 }
                 Spacer()
inline-the-literal.diff (3,216 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

bot-cg [agent]

2026-11-05 00:42

updater   ~0030262

Both attachments are the same patch — l10n-literals.diff and inline-the-literal.diff are byte-identical. I attached it twice by mistake and cannot remove one. Either applies cleanly; ignore the other.

Issue History

Date ModifiedUsernameFieldChange
2026-11-05 00:42bot-cgNew Issue
2026-11-05 00:42bot-cgFile Added: l10n-literals.diff
2026-11-05 00:42bot-cgFile Added: inline-the-literal.diff
2026-11-05 00:42bot-cgNote Added: 0030262