Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.

Commit c1926fa

Browse files
Merge pull request #19 from code4teaching/localization-fixes
Simulator und Inspektor übersetzen, Gerätesprache als Standard
2 parents e0e8812 + 13f64f6 commit c1926fa

26 files changed

Lines changed: 521 additions & 208 deletions

File tree

‎VisuStruct-iOS/VisuStructCore/Sources/VisuStructCore/VisuStructSimulationEngine.swift‎

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ public struct VisuStructSimulationState: Equatable, Sendable {
102102
public var variables: [String: VisuStructSimulationValue]
103103
public var outputLines: [String]
104104
public var inputRequest: VisuStructSimulationInputRequest?
105-
public var inputError: String?
105+
/// Fehler zur letzten Eingabe (z. B. keine Zahl); Text in der App über `VisuStructSimulationNotice` lokalisieren.
106+
public var inputErrorNotice: VisuStructSimulationNotice?
106107
public var lastTrace: String?
107-
public var message: String?
108+
/// Hinweis nach dem letzten Schritt (Ende, Fehler); `nil` während die Simulation normal läuft.
109+
public var notice: VisuStructSimulationNotice?
108110
/// Nach einem Fehler bleibt die Simulation auf dem fehlerhaften Schritt stehen, bis sie zurückgesetzt wird.
109111
public var isHalted: Bool
110112

@@ -113,22 +115,29 @@ public struct VisuStructSimulationState: Equatable, Sendable {
113115
variables: [String: VisuStructSimulationValue] = [:],
114116
outputLines: [String] = [],
115117
inputRequest: VisuStructSimulationInputRequest? = nil,
116-
inputError: String? = nil,
118+
inputErrorNotice: VisuStructSimulationNotice? = nil,
117119
lastTrace: String? = nil,
118-
message: String? = nil,
120+
notice: VisuStructSimulationNotice? = nil,
119121
isHalted: Bool = false
120122
) {
121123
self.stepIndex = stepIndex
122124
self.variables = variables
123125
self.outputLines = outputLines
124126
self.inputRequest = inputRequest
125-
self.inputError = inputError
127+
self.inputErrorNotice = inputErrorNotice
126128
self.lastTrace = lastTrace
127-
self.message = message
129+
self.notice = notice
128130
self.isHalted = isHalted
129131
}
130132
}
131133

134+
extension VisuStructSimulationState {
135+
/// Englischer Text von `notice` (Tests, Protokolle).
136+
public var message: String? { notice?.englishText }
137+
/// Englischer Text von `inputErrorNotice`.
138+
public var inputError: String? { inputErrorNotice?.englishText }
139+
}
140+
132141
public struct VisuStructSimulationEngine: Equatable, Sendable {
133142
public private(set) var steps: [VisuStructSimulationStep]
134143
public private(set) var state: VisuStructSimulationState
@@ -157,7 +166,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
157166
public mutating func step() {
158167
guard canStep else {
159168
if state.inputRequest == nil && !state.isHalted {
160-
state.message = "Simulation finished."
169+
state.notice = .finished
161170
}
162171
return
163172
}
@@ -196,13 +205,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
196205
return
197206
}
198207
if state.inputRequest != nil {
199-
state.message = nil
208+
state.notice = nil
200209
return
201210
}
202211
state.stepIndex += 1
203-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
212+
state.notice = state.stepIndex >= steps.count ? .finished : nil
204213
} catch {
205-
state.message = error.localizedDescription
214+
state.notice = VisuStructSimulationNotice(error)
206215
state.isHalted = true
207216
}
208217
}
@@ -214,13 +223,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
214223
try assignValue(value, to: request.variableName)
215224
state.lastTrace = "\(request.variableName) = input -> \(value.description)"
216225
} catch {
217-
state.inputError = error.localizedDescription
226+
state.inputErrorNotice = VisuStructSimulationNotice(error)
218227
return
219228
}
220229
state.inputRequest = nil
221-
state.inputError = nil
230+
state.inputErrorNotice = nil
222231
state.stepIndex += 1
223-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
232+
state.notice = state.stepIndex >= steps.count ? .finished : nil
224233
}
225234

226235
private static func parseInputValue(_ rawValue: String, as valueType: VisuStructSimulationValueType) throws -> VisuStructSimulationValue {
@@ -230,12 +239,12 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
230239
switch valueType {
231240
case .int:
232241
guard let value = Int(trimmed) else {
233-
throw SimulationError.invalidInput("Please enter a whole number.")
242+
throw SimulationError.invalidInput(.wholeNumberExpected)
234243
}
235244
return .int(value)
236245
case .double:
237246
guard let value = Double(trimmed) else {
238-
throw SimulationError.invalidInput("Please enter a number.")
247+
throw SimulationError.invalidInput(.numberExpected)
239248
}
240249
return .double(value)
241250
case .string:
@@ -346,7 +355,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
346355
steps.remove(at: state.stepIndex)
347356
steps.insert(contentsOf: branchSteps, at: state.stepIndex)
348357
state.lastTrace = "\(condition) -> \(result ? "true" : "false")"
349-
state.message = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
358+
state.notice = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
350359
}
351360

352361
private mutating func executeSwitchSelection(_ step: VisuStructSimulationStep) throws {
@@ -366,7 +375,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
366375
steps.remove(at: state.stepIndex)
367376
steps.insert(contentsOf: caseSteps, at: state.stepIndex)
368377
state.lastTrace = "\(selector) -> \(selectedCase.name)"
369-
state.message = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
378+
state.notice = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
370379
}
371380

372381
private mutating func executeForLoopStart(_ step: VisuStructSimulationStep) throws {
@@ -419,7 +428,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
419428
steps.insert(contentsOf: bodySteps + [incrementStep, conditionStep], at: state.stepIndex)
420429
} else {
421430
steps.remove(at: state.stepIndex)
422-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
431+
state.notice = state.stepIndex >= steps.count ? .finished : nil
423432
}
424433
}
425434

@@ -449,10 +458,10 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
449458
)
450459
steps.remove(at: state.stepIndex)
451460
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
452-
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
461+
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
453462
} else {
454463
steps.remove(at: state.stepIndex)
455-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
464+
state.notice = state.stepIndex >= steps.count ? .finished : nil
456465
}
457466
}
458467

@@ -469,7 +478,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
469478
)
470479
steps.remove(at: state.stepIndex)
471480
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
472-
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
481+
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
473482
}
474483

475484
private mutating func executeDoUntilCondition(_ step: VisuStructSimulationStep) throws {
@@ -491,7 +500,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
491500
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
492501
} else {
493502
steps.remove(at: state.stepIndex)
494-
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
503+
state.notice = state.stepIndex >= steps.count ? .finished : nil
495504
}
496505
}
497506

@@ -509,14 +518,14 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
509518
steps.remove(at: state.stepIndex)
510519
steps.insert(contentsOf: bodySteps + [loopStep], at: state.stepIndex)
511520
state.lastTrace = "true -> true"
512-
state.message = !bodySteps.isEmpty ? nil : "Infinite loop has no body."
521+
state.notice = !bodySteps.isEmpty ? nil : .infiniteLoopWithoutBody
513522
}
514523

515524
private mutating func execute(statement rawText: String) throws {
516525
let text = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
517526
if text.lowercased().hasPrefix("input:") {
518527
state.inputRequest = try parseInputRequest(text)
519-
state.inputError = nil
528+
state.inputErrorNotice = nil
520529
return
521530
}
522531

@@ -1343,20 +1352,53 @@ private enum SimulationError: LocalizedError, Equatable {
13431352
case invalidExpression(String)
13441353
case unknownVariable(String)
13451354
case divisionByZero
1346-
case invalidInput(String)
1355+
case invalidInput(VisuStructSimulationNotice)
1356+
1357+
var notice: VisuStructSimulationNotice {
1358+
switch self {
1359+
case .unsupportedStatement(let text): return .unsupportedStatement(text)
1360+
case .invalidExpression(let text): return .invalidExpression(text)
1361+
case .unknownVariable(let name): return .unknownVariable(name)
1362+
case .divisionByZero: return .divisionByZero
1363+
case .invalidInput(let notice): return notice
1364+
}
1365+
}
1366+
1367+
var errorDescription: String? { notice.englishText }
1368+
}
1369+
1370+
/// Meldungen der Simulation, unabhängig von der Sprache; die App übersetzt sie.
1371+
public enum VisuStructSimulationNotice: Equatable, Sendable {
1372+
case finished
1373+
case infiniteLoopWithoutBody
1374+
case unsupportedStatement(String)
1375+
case invalidExpression(String)
1376+
case unknownVariable(String)
1377+
case divisionByZero
1378+
case wholeNumberExpected
1379+
case numberExpected
1380+
/// Fehler aus einer anderen Quelle, nur als Text bekannt.
1381+
case other(String)
1382+
1383+
init(_ error: Error) {
1384+
if let simulationError = error as? SimulationError {
1385+
self = simulationError.notice
1386+
} else {
1387+
self = .other(error.localizedDescription)
1388+
}
1389+
}
13471390

1348-
var errorDescription: String? {
1391+
public var englishText: String {
13491392
switch self {
1350-
case .unsupportedStatement(let text):
1351-
return "Unsupported statement: \(text)"
1352-
case .invalidExpression(let text):
1353-
return "Invalid expression: \(text)"
1354-
case .unknownVariable(let name):
1355-
return "Unknown variable: \(name)"
1356-
case .divisionByZero:
1357-
return "Division by zero."
1358-
case .invalidInput(let message):
1359-
return message
1393+
case .finished: return "Simulation finished."
1394+
case .infiniteLoopWithoutBody: return "Infinite loop has no body."
1395+
case .unsupportedStatement(let text): return "Unsupported statement: \(text)"
1396+
case .invalidExpression(let text): return "Invalid expression: \(text)"
1397+
case .unknownVariable(let name): return "Unknown variable: \(name)"
1398+
case .divisionByZero: return "Division by zero."
1399+
case .wholeNumberExpected: return "Please enter a whole number."
1400+
case .numberExpected: return "Please enter a number."
1401+
case .other(let text): return text
13601402
}
13611403
}
13621404
}

‎VisuStruct-iOS/VisuStructCore/Tests/VisuStructCoreTests/SimulationEngineTests.swift‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class SimulationEngineTests: XCTestCase {
3232
XCTAssertEqual(engine.state.inputRequest?.variableName, "n")
3333

3434
engine.provideInput("abc")
35-
XCTAssertNotNil(engine.state.inputError)
35+
XCTAssertEqual(engine.state.inputErrorNotice, .wholeNumberExpected)
3636
XCTAssertNotNil(engine.state.inputRequest)
3737

3838
engine.provideInput("21")
@@ -107,7 +107,18 @@ final class SimulationEngineTests: XCTestCase {
107107
])
108108
var engine = VisuStructSimulationEngine(document: document)
109109
engine.step()
110+
XCTAssertEqual(engine.state.notice, .divisionByZero)
110111
XCTAssertEqual(engine.state.message, "Division by zero.")
111112
XCTAssertNil(engine.state.variables["x"])
112113
}
114+
115+
func testFinishedNoticeAfterLastStep() {
116+
let document = VisuStructDocument(elements: [
117+
VisuStructElement(type: .anweisung, textLines: ["int x = 1"]),
118+
])
119+
var engine = VisuStructSimulationEngine(document: document)
120+
engine.step()
121+
XCTAssertEqual(engine.state.notice, .finished)
122+
XCTAssertEqual(engine.state.message, "Simulation finished.")
123+
}
113124
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/AppLanguage.swift‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,37 @@ enum AppLanguage: String, CaseIterable, Identifiable {
5151
return b
5252
}
5353

54+
/// Übersetzung zu `key`; fehlt sie in dieser Sprache, der englische Text (nie der Schlüssel selbst).
5455
func tr(_ key: String) -> String {
56+
let text: String
5557
if self == .javaLike {
56-
return NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
58+
text = NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
59+
} else {
60+
text = NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
5761
}
58-
return NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
62+
if text == key, self != .english {
63+
return AppLanguage.english.tr(key)
64+
}
65+
return text
66+
}
67+
68+
/// Sprache des Geräts (bzw. die für VisuStruct in den Systemeinstellungen gewählte), sofern die App sie anbietet; sonst Englisch.
69+
static var deviceDefault: AppLanguage {
70+
for code in Bundle.main.preferredLocalizations {
71+
if let lang = AppLanguage(rawValue: code), lang != .javaLike { return lang }
72+
}
73+
return .english
74+
}
75+
76+
/// Erster Start ohne gespeicherte Sprache: Gerätesprache übernehmen, damit auch die Systemeinstellungen sie anzeigen.
77+
static func storeDeviceDefaultIfNeeded() {
78+
guard UserDefaults.standard.string(forKey: storageKey) == nil else { return }
79+
UserDefaults.standard.set(deviceDefault.rawValue, forKey: storageKey)
5980
}
6081

6182
/// Für `ContentView.init`, bevor `Environment` steht.
6283
static func currentFromDefaults() -> AppLanguage {
63-
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.english.rawValue
84+
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.deviceDefault.rawValue
6485
return AppLanguage(rawValue: code) ?? .english
6586
}
6687
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/Simulation/SimulationPanels.swift‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ struct SimulationStatePanel: View {
7777
prompt: engine.state.inputRequest?.prompt,
7878
variableName: engine.state.inputRequest?.variableName,
7979
valueType: engine.state.inputRequest?.valueType,
80-
inputError: engine.state.inputError,
80+
inputError: engine.state.inputErrorNotice?.localized(in: lang),
8181
inputText: $inputText,
8282
onSubmit: onSubmitInput
8383
)
@@ -160,7 +160,7 @@ struct SimulationStatePanel: View {
160160
}
161161
.layoutPriority(flexibleCardsFillHeight ? 1 : 0)
162162

163-
if let message = engine.state.message {
163+
if let message = engine.state.notice?.localized(in: lang) {
164164
Text(message)
165165
.font(.caption)
166166
.foregroundStyle(.secondary)
@@ -301,3 +301,31 @@ struct SimulationPlaceholderCard<Content: View>: View {
301301
}
302302
}
303303

304+
305+
// MARK: - Meldungen der Simulation
306+
307+
extension VisuStructSimulationNotice {
308+
/// Meldung der Simulation in der App-Sprache.
309+
func localized(in lang: AppLanguage) -> String {
310+
switch self {
311+
case .finished:
312+
return lang.tr("sim_notice_finished")
313+
case .infiniteLoopWithoutBody:
314+
return lang.tr("sim_notice_infinite_loop_empty")
315+
case .unsupportedStatement(let text):
316+
return String(format: lang.tr("sim_notice_unsupported_statement_fmt"), text)
317+
case .invalidExpression(let text):
318+
return String(format: lang.tr("sim_notice_invalid_expression_fmt"), text)
319+
case .unknownVariable(let name):
320+
return String(format: lang.tr("sim_notice_unknown_variable_fmt"), name)
321+
case .divisionByZero:
322+
return lang.tr("sim_notice_division_by_zero")
323+
case .wholeNumberExpected:
324+
return lang.tr("sim_notice_whole_number_expected")
325+
case .numberExpected:
326+
return lang.tr("sim_notice_number_expected")
327+
case .other(let text):
328+
return text
329+
}
330+
}
331+
}

0 commit comments

Comments
 (0)