//import Foundation //import XCTest //@testable import bitchat // //private let localizationTestsDirectoryURL = URL(fileURLWithPath: #filePath).deletingLastPathComponent() //private let testsRootURL = localizationTestsDirectoryURL.deletingLastPathComponent() //private let repoRootURL = testsRootURL.deletingLastPathComponent() // //final class LocalizationCatalogTests: XCTestCase { // // Ensures every app locale includes exactly the same keys as Base. // func testAppCatalogLocaleParity() throws { // let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings") // assertLocaleParity(context: context, catalogName: "App") // } // // // Verifies format placeholders stay consistent across app locales. // func testAppCatalogPlaceholderConsistency() throws { // let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings") // assertPlaceholderConsistency(context: context, catalogName: "App") // } // // // Guards a core set of app strings from going empty per locale. // func testAppPrimaryKeysNonEmpty() throws { // let context = try loadContext(relativePath: "bitchat/Localizable.xcstrings") // let primaryKeys = try loadPrimaryKeys().app // assertPrimaryKeysPresent(context: context, keys: primaryKeys, catalogName: "App") // } // // // Ensures every share extension locale matches Base key coverage. // func testShareExtensionCatalogLocaleParity() throws { // let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings") // assertLocaleParity(context: context, catalogName: "ShareExtension") // } // // // Verifies share extension placeholders align across locales. // func testShareExtensionCatalogPlaceholderConsistency() throws { // let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings") // assertPlaceholderConsistency(context: context, catalogName: "ShareExtension") // } // // // Confirms critical share extension strings remain non-empty per locale. // func testShareExtensionPrimaryKeysNonEmpty() throws { // let context = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings") // let primaryKeys = try loadPrimaryKeys().shareExtension // assertPrimaryKeysPresent(context: context, keys: primaryKeys, catalogName: "ShareExtension") // } // // // Validates that configured locales contain expected string values. // func testLocalizationExpectedValues() throws { // let appContext = try loadContext(relativePath: "bitchat/Localizable.xcstrings") // let shareContext = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings") // let config = try loadPrimaryKeys() // // guard let testLocales = config.testLocales else { // // If no testLocales specified, skip this test // return // } // // guard let expectedValues = config.expectedValues else { // XCTFail("No expectedValues configured in PrimaryLocalizationKeys.json") // return // } // // // Loop through each locale to test // for locale in testLocales { // guard let localeExpectedValues = expectedValues[locale] else { // XCTFail("No expected values configured for locale '\(locale)' in PrimaryLocalizationKeys.json") // continue // } // // // Test each expected key/value pair for this locale // for (key, expectedValue) in localeExpectedValues { // if config.app.contains(key) { // assertLocaleStringValue(context: appContext, locale: locale, key: key, expectedValue: expectedValue, catalogName: "App") // } else if config.shareExtension.contains(key) { // assertLocaleStringValue(context: shareContext, locale: locale, key: key, expectedValue: expectedValue, catalogName: "ShareExtension") // } // } // } // } // // // Ensures configured test locales are present and complete. // func testConfiguredLocalesCompleteness() throws { // let appContext = try loadContext(relativePath: "bitchat/Localizable.xcstrings") // let shareContext = try loadContext(relativePath: "bitchatShareExtension/Localization/Localizable.xcstrings") // let config = try loadPrimaryKeys() // // guard let testLocales = config.testLocales else { // // If no testLocales specified, skip this test // return // } // // let baseLocale = appContext.baseLocale // let baseAppKeys = appContext.keysByLocale[baseLocale] ?? Set() // let baseShareKeys = shareContext.keysByLocale[baseLocale] ?? Set() // // for locale in testLocales { // // Skip base locale comparison with itself // if locale == baseLocale { continue } // // // Verify locale is present in both catalogs // XCTAssertTrue(appContext.locales.contains(locale), "Locale '\(locale)' missing from app catalog") // XCTAssertTrue(shareContext.locales.contains(locale), "Locale '\(locale)' missing from share extension catalog") // // // Verify locale has same number of keys as base locale // let appLocaleKeys = appContext.keysByLocale[locale] ?? Set() // XCTAssertEqual(appLocaleKeys.count, baseAppKeys.count, "Locale '\(locale)' app catalog missing keys compared to \(baseLocale)") // // let shareLocaleKeys = shareContext.keysByLocale[locale] ?? Set() // XCTAssertEqual(shareLocaleKeys.count, baseShareKeys.count, "Locale '\(locale)' share extension catalog missing keys compared to \(baseLocale)") // } // } // // // MARK: - Assertions // // private func assertLocaleParity(context: CatalogContext, catalogName: String, file: StaticString = #filePath, line: UInt = #line) { // let baseLocale = context.baseLocale // guard let baseKeys = context.keysByLocale[baseLocale] else { // return XCTFail("Missing base locale \(baseLocale) in \(catalogName) catalog", file: file, line: line) // } // // for (locale, keys) in context.keysByLocale.sorted(by: { $0.key < $1.key }) { // XCTAssertEqual(keys, baseKeys, "Locale \(locale) has key mismatch in \(catalogName) catalog", file: file, line: line) // } // } // // private func assertPlaceholderConsistency(context: CatalogContext, catalogName: String, file: StaticString = #filePath, line: UInt = #line) { // let baseLocale = context.baseLocale // guard let baseSignatures = context.placeholderSignature[baseLocale] else { // return XCTFail("Missing base placeholder signature for \(catalogName)", file: file, line: line) // } // // for (locale, localeSignatures) in context.placeholderSignature.sorted(by: { $0.key < $1.key }) { // guard locale != baseLocale else { continue } // for key in baseSignatures.keys.sorted() { // guard let baseMap = baseSignatures[key] else { // continue // } // guard let localeMap = localeSignatures[key] else { // return XCTFail("Key \(key) missing for locale \(locale) in \(catalogName) catalog", file: file, line: line) // } // for path in baseMap.keys.sorted() { // let expected = normalizedPlaceholders(baseMap[path, default: []]) // let actual = normalizedPlaceholders(localeMap[path, default: []]) // XCTAssertEqual(actual, expected, "Placeholder mismatch for key \(key) at \(path) in locale \(locale) (\(catalogName))", file: file, line: line) // } // for (localePath, localeTokens) in localeMap { // guard baseMap[localePath] == nil else { continue } // guard let fallback = fallbackPath(for: localePath, baseMap: baseMap) else { // XCTFail("Unexpected variation \(localePath) for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line) // continue // } // let expected = normalizedPlaceholders(baseMap[fallback, default: []]) // let actual = normalizedPlaceholders(localeTokens) // XCTAssertEqual(actual, expected, "Placeholder mismatch for key \(key) at \(localePath) (fallback \(fallback)) in locale \(locale) (\(catalogName))", file: file, line: line) // } // } // } // } // // private func assertPrimaryKeysPresent(context: CatalogContext, keys: [String], catalogName: String, file: StaticString = #filePath, line: UInt = #line) { // for key in keys { // guard let entry = context.catalog.strings[key] else { // XCTFail("Missing primary key \(key) in \(catalogName) catalog", file: file, line: line) // continue // } // for locale in context.locales.sorted() { // guard let localization = entry.localizations[locale], let unit = localization.stringUnit else { // XCTFail("Missing localization for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line) // continue // } // let segments = gatherSegments(from: unit) // XCTAssertFalse(segments.isEmpty, "No content for key \(key) in locale \(locale) (\(catalogName))", file: file, line: line) // for segment in segments { // let trimmed = segment.value.trimmingCharacters(in: .whitespacesAndNewlines) // XCTAssertFalse(trimmed.isEmpty, "Empty translation for key \(key) at \(segment.path) in locale \(locale) (\(catalogName))", file: file, line: line) // } // } // } // } // // private func assertLocaleStringValue(context: CatalogContext, locale: String, key: String, expectedValue: String, catalogName: String, file: StaticString = #filePath, line: UInt = #line) { // guard let entry = context.catalog.strings[key] else { // XCTFail("Missing key \(key) in \(catalogName) catalog", file: file, line: line) // return // } // // guard let localization = entry.localizations[locale], let unit = localization.stringUnit else { // XCTFail("Missing \(locale) localization for key \(key) in \(catalogName) catalog", file: file, line: line) // return // } // // // For simple strings (non-pluralized) // if let actualValue = unit.value { // XCTAssertEqual(actualValue, expectedValue, "\(locale) translation mismatch for key \(key) in \(catalogName) catalog. Expected: '\(expectedValue)', Actual: '\(actualValue)'", file: file, line: line) // } else { // XCTFail("Key \(key) has no value in \(locale) localization for \(catalogName) catalog", file: file, line: line) // } // } // // // MARK: - Loading // // private func loadContext(relativePath: String) throws -> CatalogContext { // let catalog = try loadCatalog(relativePath: relativePath) // let locales = catalog.locales // let baseLocale = catalog.sourceLanguage // var keysByLocale: [String: Set] = [:] // var placeholderSignature: [String: [String: [String: [String]]]] = [:] // // for locale in locales { // var localeKeys: Set = [] // var localePlaceholders: [String: [String: [String]]] = [:] // for (key, entry) in catalog.strings { // guard let localization = entry.localizations[locale], let unit = localization.stringUnit else { // continue // } // localeKeys.insert(key) // let segments = gatherSegments(from: unit) // var pathMap: [String: [String]] = [:] // for segment in segments { // pathMap[segment.path] = placeholders(in: segment.value) // } // localePlaceholders[key] = pathMap // } // keysByLocale[locale] = localeKeys // placeholderSignature[locale] = localePlaceholders // } // // return CatalogContext(catalog: catalog, locales: locales, baseLocale: baseLocale, keysByLocale: keysByLocale, placeholderSignature: placeholderSignature) // } // // private func loadCatalog(relativePath: String) throws -> StringCatalog { // let url = repoRootURL.appendingPathComponent(relativePath) // let data = try Data(contentsOf: url) // return try JSONDecoder().decode(StringCatalog.self, from: data) // } // // private func loadPrimaryKeys() throws -> PrimaryKeyConfig { // let url = localizationTestsDirectoryURL.appendingPathComponent("PrimaryLocalizationKeys.json") // let data = try Data(contentsOf: url) // return try JSONDecoder().decode(PrimaryKeyConfig.self, from: data) // } // // // MARK: - Regression Tests // // /// Helper method to access the app bundle for localization testing. // /// This ensures tests use the actual app's Localizable.xcstrings instead of // /// the test bundle, preventing false positives where localization keys are // /// returned instead of translated strings. // private func appBundle() -> Bundle { // Bundle(for: ChatViewModel.self) // } // // /// Tests to prevent regression of the pluralization format string issue // /// These tests ensure that String(localized:) properly handles // /// pluralized format strings with %#@variable@ syntax // func testPluralizedFormatStringsDoNotCrash() { // // These are the exact calls that were causing runtime errors // // // Test 1: content.accessibility.people_count with Int argument // XCTAssertNoThrow({ // let result = String( // localized: "content.accessibility.people_count", // bundle: appBundle(), // comment: "Accessibility label announcing number of people in header" // ) // XCTAssertFalse(result.isEmpty, "Should return formatted string") // // Verify we're getting actual localized content, not just the key // XCTAssertNotEqual(result, "content.accessibility.people_count", "Should return localized string, not key") // }(), "People count localization should not throw") // // // Test 2: location_notes.header with String and Int arguments // XCTAssertNoThrow({ // let result = String( // localized: "location_notes.header", // bundle: appBundle(), // comment: "Header displaying the geohash and localized note count" // ) // XCTAssertFalse(result.isEmpty, "Should return formatted string") // // Verify we're getting actual localized content, not just the key // XCTAssertNotEqual(result, "location_notes.header", "Should return localized string, not key") // }(), "Location notes header localization should not throw") // } // // func testFormatStringArgumentMatching() { // // Verify that the format strings properly handle their expected arguments // // // People count expects: %#@people@ format with Int // let peopleResult = String( // localized: "content.accessibility.people_count", // bundle: appBundle(), // comment: "Test" // ) // // // Should not show format specifiers in the base string // XCTAssertFalse(peopleResult.isEmpty, "Should return non-empty string") // // // Location notes expects: #%@ • %#@note_count@ format with String, Int // let notesResult = String( // localized: "location_notes.header", // bundle: appBundle(), // comment: "Test" // ) // // XCTAssertFalse(notesResult.isEmpty, "Should return non-empty string") // } // // func testPluralizedStringsWithArguments() { // // Test the pluralized strings with actual format arguments // // // Test people count with different values // let testCases = [0, 1, 2, 5] // // for count in testCases { // let result = String( // format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"), // locale: .current, // count // ) // // // Just verify it doesn't crash and returns a reasonable string // XCTAssertFalse(result.isEmpty, // "People count should return non-empty string for count \(count)") // XCTAssertTrue(result.contains("\(count)"), // "Result should contain the count number for count \(count)") // } // } // // func testLocationNotesHeaderWithArguments() { // let geohash = "abc123" // let testCases = [0, 1, 2, 10] // // for count in testCases { // let result = String( // format: String(localized: "location_notes.header", bundle: appBundle(), comment: "Test"), // locale: .current, // geohash, count // ) // // // Verify basic structure is maintained // XCTAssertTrue(result.contains(geohash), // "Result should contain geohash for count \(count)") // XCTAssertTrue(result.contains("\(count)"), // "Result should contain count for count \(count)") // } // } // // func testLocationChannelsRowTitleWithArguments() { // let label = "test-channel" // let testCases = [0, 1, 2, 5] // // for count in testCases { // let result = String( // format: String(localized: "location_channels.row_title", bundle: appBundle(), comment: "Test"), // locale: .current, // label, count // ) // // // Verify basic structure is maintained // XCTAssertTrue(result.contains(label), // "Result should contain label for count \(count)") // XCTAssertTrue(result.contains("\(count)"), // "Result should contain count for count \(count)") // } // } // // func testNoArgumentsLocalization() { // // Test that strings without arguments still work // let result = String( // localized: "common.ok", // bundle: appBundle(), // comment: "OK button text" // ) // // XCTAssertFalse(result.isEmpty, "Simple localization should work") // } // // func testLocalizationEdgeCases() { // // Test edge cases that might cause issues // // // Large numbers // let largeNumberResult = String( // format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"), // locale: .current, // 1000000 // ) // XCTAssertTrue(largeNumberResult.contains("1000000"), "Should handle large numbers") // // // Zero // let zeroResult = String( // format: String(localized: "content.accessibility.people_count", bundle: appBundle(), comment: "Test"), // locale: .current, // 0 // ) // XCTAssertTrue(zeroResult.contains("0"), "Should handle zero") // // // Empty geohash (edge case) // let emptyGeohashResult = String( // format: String(localized: "location_notes.header", bundle: appBundle(), comment: "Test"), // locale: .current, // "", 1 // ) // XCTAssertFalse(emptyGeohashResult.isEmpty, "Should handle empty geohash") // } //} // //// MARK: - Helpers // //private struct CatalogContext { // let catalog: StringCatalog // let locales: [String] // let baseLocale: String // let keysByLocale: [String: Set] // let placeholderSignature: [String: [String: [String: [String]]]] //} // //private struct StringCatalog: Decodable { // let sourceLanguage: String // let strings: [String: CatalogEntry] // // var locales: [String] { // var localeSet: Set = [] // for entry in strings.values { // localeSet.formUnion(entry.localizations.keys) // } // return localeSet.sorted() // } //} // //private struct CatalogEntry: Decodable { // let localizations: [String: CatalogLocalization] //} // //private struct CatalogLocalization: Decodable { // let stringUnit: CatalogStringUnit? //} // //private struct CatalogStringUnit: Decodable { // let state: String // let value: String? // let variations: CatalogVariations? // let comment: String? //} // //private struct CatalogVariations: Decodable { // let plural: [String: [String: CatalogVariationValue]]? //} // //private struct CatalogVariationValue: Decodable { // let stringUnit: CatalogStringUnit? //} // //private struct Segment { // let components: [String] // let value: String // // var path: String { // components.isEmpty ? "base" : components.joined(separator: ".") // } //} // //private func gatherSegments(from unit: CatalogStringUnit, prefix: [String] = []) -> [Segment] { // var segments: [Segment] = [] // if let value = unit.value { // segments.append(Segment(components: prefix, value: value)) // } else if prefix.isEmpty { // segments.append(Segment(components: [], value: "")) // } // if let plural = unit.variations?.plural { // for (variable, categories) in plural.sorted(by: { $0.key < $1.key }) { // for (category, variation) in categories.sorted(by: { $0.key < $1.key }) { // if let nested = variation.stringUnit { // var nextPrefix = prefix // nextPrefix.append("plural") // nextPrefix.append(variable) // nextPrefix.append(category) // segments.append(contentsOf: gatherSegments(from: nested, prefix: nextPrefix)) // } // } // } // } // return segments //} // //private func normalizedPlaceholders(_ tokens: [String]) -> [String] { // tokens.sorted() //} // //private func fallbackPath(for localePath: String, baseMap: [String: [String]]) -> String? { // let parts = localePath.split(separator: ".") // guard parts.count == 3, parts.first == "plural" else { // return nil // } // let variable = parts[1] // let otherKey = "plural.\(variable).other" // if baseMap[otherKey] != nil { // return otherKey // } // let oneKey = "plural.\(variable).one" // if baseMap[oneKey] != nil { // return oneKey // } // return nil //} // //private let placeholderRegex: NSRegularExpression = { // let pattern = "%(?:\\d+\\$)?#@[A-Za-z0-9_]+@|%(?:\\d+\\$)?[#0\\- +'\"]*(?:\\d+|\\*)?(?:\\.\\d+)?(?:hh|h|ll|l|z|t|L)?[a-zA-Z@]" // return try! NSRegularExpression(pattern: pattern, options: []) //}() // //private func placeholders(in string: String) -> [String] { // let range = NSRange(location: 0, length: (string as NSString).length) // let matches = placeholderRegex.matches(in: string, options: [], range: range) // var tokens: [String] = [] // for match in matches { // if let range = Range(match.range, in: string) { // let token = String(string[range]) // if token == "%%" { continue } // tokens.append(token) // } // } // return tokens //} // //private struct PrimaryKeyConfig: Decodable { // let app: [String] // let shareExtension: [String] // let expectedValues: [String: [String: String]]? // let testLocales: [String]? //}