mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
8.6 KiB
Swift
252 lines
8.6 KiB
Swift
3 years ago
|
#!/usr/bin/xcrun --sdk macosx swift
|
||
|
|
||
|
//
|
||
|
// ListLocalizableStrings.swift
|
||
|
// Archa
|
||
|
//
|
||
|
// Created by Morgan Pretty on 18/5/20.
|
||
|
// Copyright © 2020 Archa. All rights reserved.
|
||
|
//
|
||
|
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
|
||
|
// is canges to the localized usage regex
|
||
|
|
||
|
import Foundation
|
||
|
|
||
|
let fileManager = FileManager.default
|
||
|
let currentPath = (
|
||
|
ProcessInfo.processInfo.environment["PROJECT_DIR"] ?? fileManager.currentDirectoryPath
|
||
|
)
|
||
|
|
||
|
/// List of files in currentPath - recursive
|
||
|
var pathFiles: [String] = {
|
||
|
guard let enumerator = fileManager.enumerator(atPath: currentPath), let files = enumerator.allObjects as? [String] else {
|
||
|
fatalError("Could not locate files in path directory: \(currentPath)")
|
||
|
}
|
||
|
|
||
|
return files
|
||
|
}()
|
||
|
|
||
|
|
||
|
/// List of localizable files - not including Localizable files in the Pods
|
||
|
var localizableFiles: [String] = {
|
||
|
return pathFiles
|
||
|
.filter {
|
||
|
$0.hasSuffix("Localizable.strings") &&
|
||
|
!$0.contains(".app/") && // Exclude Built Localizable.strings files
|
||
|
!$0.contains("Pods") // Exclude Pods
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
|
||
|
/// List of executable files
|
||
|
var executableFiles: [String] = {
|
||
|
return pathFiles.filter {
|
||
|
!$0.localizedCaseInsensitiveContains("test") && // Exclude test files
|
||
|
!$0.contains(".app/") && // Exclude Built Localizable.strings files
|
||
|
!$0.contains("Pods") && // Exclude Pods
|
||
|
(
|
||
|
NSString(string: $0).pathExtension == "swift" ||
|
||
|
NSString(string: $0).pathExtension == "m"
|
||
|
)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
/// Reads contents in path
|
||
|
///
|
||
|
/// - Parameter path: path of file
|
||
|
/// - Returns: content in file
|
||
|
func contents(atPath path: String) -> String {
|
||
|
print("Path: \(path)")
|
||
|
guard let data = fileManager.contents(atPath: path), let content = String(data: data, encoding: .utf8) else {
|
||
|
fatalError("Could not read from path: \(path)")
|
||
|
}
|
||
|
|
||
|
return content
|
||
|
}
|
||
|
|
||
|
/// Returns a list of strings that match regex pattern from content
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - pattern: regex pattern
|
||
|
/// - content: content to match
|
||
|
/// - Returns: list of results
|
||
|
func regexFor(_ pattern: String, content: String) -> [String] {
|
||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||
|
fatalError("Regex not formatted correctly: \(pattern)")
|
||
|
}
|
||
|
|
||
|
let matches = regex.matches(in: content, options: [], range: NSRange(location: 0, length: content.utf16.count))
|
||
|
|
||
|
return matches.map {
|
||
|
guard let range = Range($0.range(at: 0), in: content) else {
|
||
|
fatalError("Incorrect range match")
|
||
|
}
|
||
|
|
||
|
return String(content[range])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func create() -> [LocalizationStringsFile] {
|
||
|
return localizableFiles.map(LocalizationStringsFile.init(path:))
|
||
|
}
|
||
|
|
||
|
///
|
||
|
///
|
||
|
/// - Returns: A list of LocalizationCodeFile - contains path of file and all keys in it
|
||
|
func localizedStringsInCode() -> [LocalizationCodeFile] {
|
||
|
return executableFiles.compactMap {
|
||
|
let content = contents(atPath: $0)
|
||
|
// Note: Need to exclude escaped quotation marks from strings
|
||
|
let matchesOld = regexFor("(?<=NSLocalizedString\\()\\s*\"(?!.*?%d)(.*?)\"", content: content)
|
||
|
let matchesNew = regexFor("\"(?!.*?%d)([^(\\\")]*?)\"(?=\\s*)(?=\\.localized)", content: content)
|
||
|
let allMatches = (matchesOld + matchesNew)
|
||
|
|
||
|
return allMatches.isEmpty ? nil : LocalizationCodeFile(path: $0, keys: Set(allMatches))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Throws error if ALL localizable files does not have matching keys
|
||
|
///
|
||
|
/// - Parameter files: list of localizable files to validate
|
||
|
func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
||
|
print("------------ Validating keys match in all localizable files ------------")
|
||
|
|
||
|
guard let base = files.first, files.count > 1 else { return }
|
||
|
|
||
|
let files = Array(files.dropFirst())
|
||
|
|
||
|
files.forEach {
|
||
|
guard let extraKey = Set(base.keys).symmetricDifference($0.keys).first else { return }
|
||
|
let incorrectFile = $0.keys.contains(extraKey) ? $0 : base
|
||
|
printPretty("error: Found extra key: \(extraKey) in file: \(incorrectFile.path)")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Throws error if localizable files are missing keys
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - codeFiles: Array of LocalizationCodeFile
|
||
|
/// - localizationFiles: Array of LocalizableStringFiles
|
||
|
func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||
|
print("------------ Checking for missing keys -----------")
|
||
|
|
||
|
guard let baseFile = localizationFiles.first else {
|
||
|
fatalError("Could not locate base localization file")
|
||
|
}
|
||
|
|
||
|
let baseKeys = Set(baseFile.keys)
|
||
|
|
||
|
codeFiles.forEach {
|
||
|
let extraKeys = $0.keys.subtracting(baseKeys)
|
||
|
if !extraKeys.isEmpty {
|
||
|
printPretty("error: Found keys in code missing in strings file: \(extraKeys) from \($0.path)")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Throws warning if keys exist in localizable file but are not being used
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - codeFiles: Array of LocalizationCodeFile
|
||
|
/// - localizationFiles: Array of LocalizableStringFiles
|
||
|
func validateDeadKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||
|
print("------------ Checking for any dead keys in localizable file -----------")
|
||
|
|
||
|
guard let baseFile = localizationFiles.first else {
|
||
|
fatalError("Could not locate base localization file")
|
||
|
}
|
||
|
|
||
|
let baseKeys: Set<String> = Set(baseFile.keys)
|
||
|
let allCodeFileKeys: [String] = codeFiles.flatMap { $0.keys }
|
||
|
let deadKeys: [String] = Array(baseKeys.subtracting(allCodeFileKeys))
|
||
|
.sorted()
|
||
|
.map { $0.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) }
|
||
|
|
||
|
if !deadKeys.isEmpty {
|
||
|
printPretty("warning: \(deadKeys) - Suggest cleaning dead keys")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protocol Pathable {
|
||
|
var path: String { get }
|
||
|
}
|
||
|
|
||
|
struct LocalizationStringsFile: Pathable {
|
||
|
let path: String
|
||
|
let kv: [String: String]
|
||
|
|
||
|
var keys: [String] {
|
||
|
return Array(kv.keys)
|
||
|
}
|
||
|
|
||
|
init(path: String) {
|
||
|
self.path = path
|
||
|
self.kv = ContentParser.parse(path)
|
||
|
}
|
||
|
|
||
|
/// Writes back to localizable file with sorted keys and removed whitespaces and new lines
|
||
|
func cleanWrite() {
|
||
|
print("------------ Sort and remove whitespaces: \(path) ------------")
|
||
|
let content = kv.keys.sorted().map { "\($0) = \(kv[$0]!);" }.joined(separator: "\n")
|
||
|
try! content.write(toFile: path, atomically: true, encoding: .utf8)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
struct LocalizationCodeFile: Pathable {
|
||
|
let path: String
|
||
|
let keys: Set<String>
|
||
|
}
|
||
|
|
||
|
struct ContentParser {
|
||
|
|
||
|
/// Parses contents of a file to localizable keys and values - Throws error if localizable file have duplicated keys
|
||
|
///
|
||
|
/// - Parameter path: Localizable file paths
|
||
|
/// - Returns: localizable key and value for content at path
|
||
|
static func parse(_ path: String) -> [String: String] {
|
||
|
print("------------ Checking for duplicate keys: \(path) ------------")
|
||
|
|
||
|
let content = contents(atPath: path)
|
||
|
let trimmed = content
|
||
|
.replacingOccurrences(of: "\n+", with: "", options: .regularExpression, range: nil)
|
||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
let keys = regexFor("\"([^\"]*?)\"(?= =)", content: trimmed)
|
||
|
let values = regexFor("(?<== )\"(.*?)\"(?=;)", content: trimmed)
|
||
|
|
||
|
if keys.count != values.count {
|
||
|
fatalError("Error parsing contents: Make sure all keys and values are in correct format (this could be due to extra spaces between keys and values)")
|
||
|
}
|
||
|
|
||
|
return zip(keys, values).reduce(into: [String: String]()) { results, keyValue in
|
||
|
if results[keyValue.0] != nil {
|
||
|
printPretty("error: Found duplicate key: \(keyValue.0) in file: \(path)")
|
||
|
abort()
|
||
|
}
|
||
|
results[keyValue.0] = keyValue.1
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func printPretty(_ string: String) {
|
||
|
print(string.replacingOccurrences(of: "\\", with: ""))
|
||
|
}
|
||
|
|
||
|
let stringFiles = create()
|
||
|
|
||
|
if !stringFiles.isEmpty {
|
||
|
print("------------ Found \(stringFiles.count) file(s) ------------")
|
||
|
|
||
|
stringFiles.forEach { print($0.path) }
|
||
|
validateMatchKeys(stringFiles)
|
||
|
|
||
|
// Note: Uncomment the below file to clean out all comments from the localizable file (we don't want this because comments make it readable...)
|
||
|
// stringFiles.forEach { $0.cleanWrite() }
|
||
|
|
||
|
let codeFiles = localizedStringsInCode()
|
||
|
validateMissingKeys(codeFiles, localizationFiles: stringFiles)
|
||
|
validateDeadKeys(codeFiles, localizationFiles: stringFiles)
|
||
|
}
|
||
|
|
||
|
print("------------ SUCCESS ------------")
|