diff --git a/CableTests/LocalizationIntegrityTests.swift b/CableTests/LocalizationIntegrityTests.swift new file mode 100644 index 0000000..d9c48f3 --- /dev/null +++ b/CableTests/LocalizationIntegrityTests.swift @@ -0,0 +1,75 @@ +import Foundation +import Testing +@testable import Cable + +/// Guards the string tables themselves. A malformed `.strings` file still parses — a value +/// spanning several lines silently swallows the entries behind it — and the app then renders +/// raw keys like "tab.overview". These tests fail instead. +struct LocalizationIntegrityTests { + + private static let locales = ["Base", "de", "es", "fr", "nl"] + + /// Keys rendered without a `defaultValue:`, so a missing entry is visible to users. + private static let keysWithoutFallback = [ + "tab.overview", + "tab.components", + "tab.batteries", + "tab.chargers", + ] + + private func table(for locale: String) throws -> [String: String] { + let bundle = Bundle.main + guard let url = bundle.url( + forResource: "Localizable", + withExtension: "strings", + subdirectory: nil, + localization: locale + ) else { + throw LocalizationTestError.tableMissing(locale) + } + let data = try Data(contentsOf: url) + let parsed = try PropertyListSerialization.propertyList(from: data, format: nil) + guard let table = parsed as? [String: String] else { + throw LocalizationTestError.tableMalformed(locale) + } + return table + } + + @Test func everyLocaleProvidesTheKeysThatHaveNoFallback() async throws { + for locale in Self.locales { + let table = try table(for: locale) + for key in Self.keysWithoutFallback { + #expect(table[key] != nil, "\(locale): missing \(key), the UI would show the raw key") + #expect( + table[key] != key, + "\(locale): \(key) is not translated" + ) + } + } + } + + @Test func noValueSpansMultipleLines() async throws { + for locale in Self.locales { + let table = try table(for: locale) + for (key, value) in table { + #expect( + !value.contains("\n"), + "\(locale): \(key) contains a real newline; removing such an entry line by line corrupts the table" + ) + } + } + } + + @Test func retiredProKeysAreGone() async throws { + for locale in Self.locales { + let table = try table(for: locale) + let leftovers = table.keys.filter { $0.hasPrefix("cable.pro.") } + #expect(leftovers.isEmpty, "\(locale): dead PRO keys \(leftovers.sorted())") + } + } +} + +private enum LocalizationTestError: Error { + case tableMissing(String) + case tableMalformed(String) +} diff --git a/CableUITestsScreenshot/LocalizedTabBarUITests.swift b/CableUITestsScreenshot/LocalizedTabBarUITests.swift new file mode 100644 index 0000000..ab90356 --- /dev/null +++ b/CableUITestsScreenshot/LocalizedTabBarUITests.swift @@ -0,0 +1,62 @@ +import XCTest + +/// Reproduces the regression where the tab bar rendered raw keys ("tab.overview") because a +/// malformed entry in Localizable.strings hid every key behind it. The tab titles are looked up +/// without a `defaultValue:`, so a broken table is immediately visible here. +final class LocalizedTabBarUITests: XCTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + XCUIDevice.shared.orientation = .portrait + } + + @MainActor + func testTabBarIsLocalizedInGerman() throws { + let app = XCUIApplication() + app.launchArguments = [ + "--uitest-reset-data", + "--uitest-sample-data", + "-AppleLanguages", "(de)", + "-AppleLocale", "de_DE", + ] + app.launch() + + openFirstSystem(in: app) + + let expected = ["Übersicht", "Verbraucher", "Batterien", "Ladegeräte"] + for title in expected { + XCTAssertTrue( + app.buttons[title].waitForExistence(timeout: 15), + "Tab \"\(title)\" is missing — the German string table is not being read" + ) + } + + for rawKey in ["tab.overview", "tab.components", "tab.batteries", "tab.chargers"] { + XCTAssertFalse( + app.buttons[rawKey].exists, + "Tab bar shows the raw key \(rawKey) instead of a translation" + ) + } + } + + private func openFirstSystem(in app: XCUIApplication) { + let list: XCUIElement + if app.collectionViews["systems-list"].waitForExistence(timeout: 15) { + list = app.collectionViews["systems-list"] + } else { + list = app.collectionViews.firstMatch + } + XCTAssertTrue(list.waitForExistence(timeout: 15)) + + let firstCell = list.cells.element(boundBy: 0) + XCTAssertTrue(firstCell.waitForExistence(timeout: 10)) + + let cellButton = firstCell.buttons.firstMatch + if cellButton.exists { + cellButton.tap() + } else { + firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() + } + } +}