|
|
@@ -0,0 +1,77 @@
|
|
|
+
|
|
|
+import Foundation
|
|
|
+import Testing
|
|
|
+
|
|
|
+private let bundle = Bundle.main
|
|
|
+
|
|
|
+@Suite("Localization Tests", .serialized) struct LocalizationTests {
|
|
|
+ @Test("No stray % inside format strings") func testNoStrayPercent() {
|
|
|
+ // Array to collect strings with issues
|
|
|
+ var offenders: [(lang: String, key: String, value: String, file: String)] = []
|
|
|
+
|
|
|
+ // Regular expression patterns
|
|
|
+ let placeholderPattern = "%[0-9]*\\$?[.,]?[0-9]*[a-zA-Z@]" // Matches placeholders like %@, %d, %1$@
|
|
|
+ let escapedPercentPattern = "%%" // Matches escaped percent signs
|
|
|
+ let percentPattern = "%" // Matches any percent sign
|
|
|
+
|
|
|
+ // Compile regexes (force-unwrapped since patterns are static and valid)
|
|
|
+ let placeholderRegex = try! NSRegularExpression(pattern: placeholderPattern)
|
|
|
+ let escapedPercentRegex = try! NSRegularExpression(pattern: escapedPercentPattern)
|
|
|
+ let percentRegex = try! NSRegularExpression(pattern: percentPattern)
|
|
|
+
|
|
|
+ // Assume 'bundle' is accessible, e.g., Bundle.main
|
|
|
+ for locale in bundle.localizations where locale != "Base" {
|
|
|
+ guard let lproj = bundle.path(forResource: locale, ofType: "lproj"),
|
|
|
+ let files = FileManager.default.enumerator(atPath: lproj) else { continue }
|
|
|
+
|
|
|
+ // Iterate over .strings files in the localization directory
|
|
|
+ for case let f as String in files where f.hasSuffix(".strings") {
|
|
|
+ let path = (lproj as NSString).appendingPathComponent(f)
|
|
|
+ guard let table = NSDictionary(contentsOfFile: path) as? [String: String] else { continue }
|
|
|
+
|
|
|
+ // Check each key-value pair in the .strings file
|
|
|
+ for (key, value) in table {
|
|
|
+ let nsValue = value as NSString
|
|
|
+ let range = NSRange(location: 0, length: nsValue.length)
|
|
|
+
|
|
|
+ // Determine if the value contains any placeholders
|
|
|
+ let hasPlaceholders = placeholderRegex.firstMatch(in: value, range: range) != nil
|
|
|
+
|
|
|
+ // Only check for stray % if the value has placeholders
|
|
|
+ if hasPlaceholders {
|
|
|
+ // Find all ranges covered by placeholders and escaped %%
|
|
|
+ let placeholderMatches = placeholderRegex.matches(in: value, range: range)
|
|
|
+ let escapedMatches = escapedPercentRegex.matches(in: value, range: range)
|
|
|
+ let coveredRanges = (placeholderMatches + escapedMatches).map(\.range)
|
|
|
+
|
|
|
+ // Find all % signs in the value
|
|
|
+ let percentMatches = percentRegex.matches(in: value, range: range)
|
|
|
+
|
|
|
+ // Check each % to see if it's stray (not covered by a placeholder or %%)
|
|
|
+ for percentMatch in percentMatches {
|
|
|
+ let percentLocation = percentMatch.range.location
|
|
|
+ let isCovered = coveredRanges.contains { NSLocationInRange(percentLocation, $0) }
|
|
|
+ if !isCovered {
|
|
|
+ offenders.append((lang: locale, key: key, value: value, file: f))
|
|
|
+ break // Stop checking this string after finding an issue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // If no placeholders, skip the check (single % is allowed)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assert that no offenders were found using Testing's #expect
|
|
|
+ #expect(
|
|
|
+ offenders.isEmpty,
|
|
|
+ """
|
|
|
+ Found \(offenders.count) string(s) that still have a single % although \
|
|
|
+ the value contains printf placeholders:
|
|
|
+
|
|
|
+ \(offenders.map { "\($0.lang) – \($0.file)\n⟨key⟩ \($0.key)\n⟨value⟩ \($0.value)" }
|
|
|
+ .joined(separator: "\n\n"))
|
|
|
+ """
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|