LocalizationTests.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import Foundation
  2. import Testing
  3. private let bundle = Bundle.main
  4. @Suite("Localization Tests", .serialized) struct LocalizationTests {
  5. @Test("No stray % inside format strings") func testNoStrayPercent() {
  6. // Array to collect strings with issues
  7. var offenders: [(lang: String, key: String, value: String, file: String)] = []
  8. // Regular expression patterns
  9. let placeholderPattern = "%[0-9]*\\$?[.,]?[0-9]*[a-zA-Z@]" // Matches placeholders like %@, %d, %1$@
  10. let escapedPercentPattern = "%%" // Matches escaped percent signs
  11. let percentPattern = "%" // Matches any percent sign
  12. // Compile regexes (force-unwrapped since patterns are static and valid)
  13. let placeholderRegex = try! NSRegularExpression(pattern: placeholderPattern)
  14. let escapedPercentRegex = try! NSRegularExpression(pattern: escapedPercentPattern)
  15. let percentRegex = try! NSRegularExpression(pattern: percentPattern)
  16. // Assume 'bundle' is accessible, e.g., Bundle.main
  17. for locale in bundle.localizations where locale != "Base" {
  18. guard let lproj = bundle.path(forResource: locale, ofType: "lproj"),
  19. let files = FileManager.default.enumerator(atPath: lproj) else { continue }
  20. // Iterate over .strings files in the localization directory
  21. for case let f as String in files where f.hasSuffix(".strings") {
  22. let path = (lproj as NSString).appendingPathComponent(f)
  23. guard let table = NSDictionary(contentsOfFile: path) as? [String: String] else { continue }
  24. // Check each key-value pair in the .strings file
  25. for (key, value) in table {
  26. let nsValue = value as NSString
  27. let range = NSRange(location: 0, length: nsValue.length)
  28. // Determine if the value contains any placeholders
  29. let hasPlaceholders = placeholderRegex.firstMatch(in: value, range: range) != nil
  30. // Only check for stray % if the value has placeholders
  31. if hasPlaceholders {
  32. // Find all ranges covered by placeholders and escaped %%
  33. let placeholderMatches = placeholderRegex.matches(in: value, range: range)
  34. let escapedMatches = escapedPercentRegex.matches(in: value, range: range)
  35. let coveredRanges = (placeholderMatches + escapedMatches).map(\.range)
  36. // Find all % signs in the value
  37. let percentMatches = percentRegex.matches(in: value, range: range)
  38. // Check each % to see if it's stray (not covered by a placeholder or %%)
  39. for percentMatch in percentMatches {
  40. let percentLocation = percentMatch.range.location
  41. let isCovered = coveredRanges.contains { NSLocationInRange(percentLocation, $0) }
  42. if !isCovered {
  43. offenders.append((lang: locale, key: key, value: value, file: f))
  44. break // Stop checking this string after finding an issue
  45. }
  46. }
  47. }
  48. // If no placeholders, skip the check (single % is allowed)
  49. }
  50. }
  51. }
  52. // Assert that no offenders were found using Testing's #expect
  53. #expect(
  54. offenders.isEmpty,
  55. """
  56. Found \(offenders.count) string(s) that still have a single % although \
  57. the value contains printf placeholders:
  58. \(offenders.map { "\($0.lang) – \($0.file)\n⟨key⟩ \($0.key)\n⟨value⟩ \($0.value)" }
  59. .joined(separator: "\n\n"))
  60. """
  61. )
  62. }
  63. }