-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
515 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"object": { | ||
"pins": [ | ||
{ | ||
"package": "FoundationExtensions", | ||
"repositoryURL": "https://github.com/teufelaudio/FoundationExtensions", | ||
"state": { | ||
"branch": null, | ||
"revision": "b393be572e8e6e424eb38a096ca28b2bf8f3744e", | ||
"version": "0.1.3" | ||
} | ||
}, | ||
{ | ||
"package": "swift-argument-parser", | ||
"repositoryURL": "https://github.com/apple/swift-argument-parser", | ||
"state": { | ||
"branch": null, | ||
"revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", | ||
"version": "0.1.0" | ||
} | ||
} | ||
] | ||
}, | ||
"version": 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// swift-tools-version:5.2 | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "MergeL10n", | ||
platforms: [.macOS(.v10_15)], | ||
products: [ | ||
.executable(name: "MergeL10n", targets: ["MergeL10n"]) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "0.0.6")), | ||
.package(url: "https://github.com/teufelaudio/FoundationExtensions", .upToNextMajor(from: "0.1.1")) | ||
], | ||
targets: [ | ||
.target( | ||
name: "MergeL10n", | ||
dependencies: [ | ||
.product(name: "ArgumentParser", package: "swift-argument-parser"), | ||
.product(name: "FoundationExtensionsStatic", package: "FoundationExtensions") | ||
] | ||
) | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# MergeL10n | ||
|
||
A description of this package. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// | ||
// MergeL10n.swift | ||
// MergeL10n | ||
// | ||
// Created by Luiz Barbosa on 17.06.20. | ||
// Copyright © 2020 Lautsprecher Teufel GmbH. All rights reserved. | ||
// | ||
|
||
import ArgumentParser | ||
|
||
struct MergeL10n: ParsableCommand { | ||
static var configuration = CommandConfiguration( | ||
abstract: "Localizable utils", | ||
subcommands: [PseudoToLanguages.self] | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// | ||
// PseudoToLanguages.swift | ||
// MergeL10n | ||
// | ||
// Created by Luiz Barbosa on 17.06.20. | ||
// Copyright © 2020 Lautsprecher Teufel GmbH. All rights reserved. | ||
// | ||
|
||
import ArgumentParser | ||
import Combine | ||
import Foundation | ||
import FoundationExtensions | ||
|
||
struct World { | ||
let fileManager: SimpleFileManager | ||
let environmentVariables: EnvironmentVariables | ||
} | ||
|
||
extension World { | ||
static let `default` = World( | ||
fileManager: .default, | ||
environmentVariables: .default | ||
) | ||
} | ||
|
||
struct SimpleFileManager { | ||
let fileExists: (String) -> Bool | ||
let readTextFile: (String, String.Encoding) -> Result<String, Error> | ||
let createTextFile: (String, String, String.Encoding) -> Bool | ||
} | ||
|
||
extension SimpleFileManager { | ||
static let `default` = SimpleFileManager( | ||
fileExists: FileManager.default.fileExists, | ||
readTextFile: { path, encoding in | ||
Result { try String(contentsOfFile: path, encoding: encoding) } | ||
}, | ||
createTextFile: { path, contents, encoding in | ||
FileManager.default.createFile(atPath: path, contents: contents.data(using: encoding)) | ||
} | ||
) | ||
} | ||
|
||
struct EnvironmentVariables { | ||
let get: (String) -> String? | ||
let set: (String, String) -> Void | ||
let loadDotEnv: (String) -> Void | ||
} | ||
|
||
extension EnvironmentVariables { | ||
static let `default` = EnvironmentVariables( | ||
get: { name in (getenv(name)).flatMap { String(utf8String: $0) } }, | ||
set: { key, value in setenv(key, value, 1) }, | ||
loadDotEnv: { path in | ||
guard let file = try? String(contentsOfFile: path, encoding: .utf8) else { return } | ||
file | ||
.split { $0 == "\n" || $0 == "\r\n" } | ||
.map(String.init) | ||
.forEach { fullLine in | ||
let line = fullLine.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
||
guard line[line.startIndex] != "#" else { return } | ||
guard !line.isEmpty else { return } | ||
let parts = line.split(separator: "=", maxSplits: 1).map(String.init) | ||
let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) | ||
let value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .init(arrayLiteral: "\"")) | ||
setenv(key, value, 1) | ||
} | ||
} | ||
) | ||
} | ||
|
||
struct PseudoToLanguages: ParsableCommand { | ||
@Option(help: "Separated-by-comma list of languages that should be created. Environment Variable SUPPORTED_LANGUAGES is an alternative.") | ||
var languages: String? | ||
|
||
@Option(help: "Separated-by-comma list of paths to base localizable strings folders. In this folder we expect to find zz.lproj directory and files. Environment Variable L10N_BASE_PATHS is an alternative.") | ||
var basePaths: String? | ||
|
||
@Option(default: "en", help: "Which language is used for development. Default: en.") | ||
var developmentLanguage: String | ||
|
||
func run() throws { | ||
let world = World.default | ||
let paramLanguages = self.languages | ||
let paramBasePaths = self.basePaths | ||
let paramDevelopmentLanguage = self.developmentLanguage | ||
|
||
_ = try languagesAndPaths(languages: paramLanguages, paths: paramBasePaths) | ||
.mapResultError(identity) | ||
.contramapEnvironment(\World.environmentVariables) | ||
.flatMapResult { languages, paths -> Reader<World, Result<[Void], Error>> in | ||
print("Running MergeL10n with languages: \(languages) and development language: \(paramDevelopmentLanguage)") | ||
|
||
return paths | ||
.traverse { folder in | ||
self.run(folder: folder, languages: languages, developmentLanguage: paramDevelopmentLanguage) | ||
.mapResultError(identity) | ||
.contramapEnvironment(\.fileManager) | ||
} | ||
.mapValue { $0.traverse(identity) } | ||
} | ||
.inject(world) | ||
.get() | ||
} | ||
|
||
private func run(folder: String, languages: [String], developmentLanguage: String) -> Reader<SimpleFileManager, Result<Void, LocalizedStringFileError>>{ | ||
Reader { fileManager in | ||
LocalizedStringFile(basePath: folder, language: "zz") | ||
.read(encoding: .utf16) | ||
.inject(fileManager) | ||
.flatMap { zzEntries in | ||
languages.traverse { language in | ||
LocalizedStringFile.replace( | ||
language: LocalizedStringFile(basePath: folder, language: language), | ||
withKeysFrom: zzEntries, | ||
fillWithEmpty: language == developmentLanguage, | ||
encoding: .utf8 | ||
).inject(fileManager) | ||
} | ||
}.map(ignore) | ||
} | ||
} | ||
|
||
private func languagesAndPaths(languages: String?, paths: String?) -> Reader<EnvironmentVariables, Result<(languages: [String], paths: [String]), ValidationError>> { | ||
|
||
Reader { environmentVariables in | ||
if self.languages == nil || self.basePaths == nil { | ||
// Load the .env file | ||
environmentVariables.loadDotEnv(".env") | ||
} | ||
|
||
return zip( | ||
languages.map(Result<String, ValidationError>.success) | ||
?? self.supportedLanguagesFromEnvironment().inject(environmentVariables), | ||
paths.map(Result<String, ValidationError>.success) | ||
?? self.basePathsFromEnvironment().inject(environmentVariables) | ||
).map { (languages: String, paths: String) -> (languages: [String], paths: [String]) in | ||
( | ||
languages: languages.split(separator: ",").map(String.init), | ||
paths: paths.split(separator: ",").map(String.init) | ||
) | ||
} | ||
} | ||
} | ||
|
||
private func supportedLanguagesFromEnvironment() -> Reader<EnvironmentVariables, Result<String, ValidationError>> { | ||
Reader { environmentVariables in | ||
environmentVariables.get("SUPPORTED_LANGUAGES") | ||
.toResult(orError: ValidationError( | ||
"Use --languages:\"en,pt\" option or set `SUPPORTED_LANGUAGES` environment variable before running the script. File .env is also an option." | ||
)) | ||
} | ||
} | ||
|
||
private func basePathsFromEnvironment() -> Reader<EnvironmentVariables, Result<String, ValidationError>> { | ||
Reader { environmentVariables in | ||
environmentVariables.get("L10N_BASE_PATHS") | ||
.toResult(orError: ValidationError( | ||
"Use --base-paths:\"SomeFolder/Resources,AnotherFolder/Resources\" option or set `L10N_BASE_PATHS` environment variable before running the script. File .env is also an option." | ||
)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// | ||
// Parser.swift | ||
// MergeL10n | ||
// | ||
// Created by Luiz Barbosa on 17.06.20. | ||
// Copyright © 2020 Lautsprecher Teufel GmbH. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
// MARK: - Localizable.strings File Parsing | ||
|
||
struct Parser<A> { | ||
let run: (inout Substring) -> A? | ||
|
||
func run(_ str: String) -> (match: A?, rest: Substring) { | ||
var str = str[...] | ||
let match = self.run(&str) | ||
return (match, str) | ||
} | ||
|
||
func map<B>(_ f: @escaping (A) -> B) -> Parser<B> { | ||
Parser<B> { str in | ||
self.run(&str).map(f) | ||
} | ||
} | ||
|
||
func flatMap<B>(_ f: @escaping (A) -> Parser<B>) -> Parser<B> { | ||
Parser<B> { str in | ||
let original = str | ||
let matchA = self.run(&str) | ||
let parserB = matchA.map(f) | ||
guard let matchB = parserB?.run(&str) else { | ||
str = original | ||
return nil | ||
} | ||
return matchB | ||
} | ||
} | ||
} | ||
func literal(_ literal: String) -> Parser<Void> { | ||
Parser<Void> { str in | ||
guard str.hasPrefix(literal) else { return nil } | ||
str.removeFirst(literal.count) | ||
return () | ||
} | ||
} | ||
let charParser = Parser<Character> { str in | ||
guard !str.isEmpty else { return nil } | ||
return str.removeFirst() | ||
} | ||
func always<A>(_ a: A) -> Parser<A> { Parser { _ in a } } | ||
func never<A>() -> Parser<A> { Parser { _ in nil } } | ||
func zip<A, B>(_ a: Parser<A>, _ b: Parser<B>) -> Parser<(A, B)> { | ||
Parser<(A, B)> { str in | ||
let original = str | ||
guard let matchA = a.run(&str) else { return nil } | ||
guard let matchB = b.run(&str) else { | ||
str = original | ||
return nil | ||
} | ||
return (matchA, matchB) | ||
} | ||
} | ||
func zip<A, B, C>(_ a: Parser<A>, _ b: Parser<B>, _ c: Parser<C> ) -> Parser<(A, B, C)> { | ||
zip(a, zip(b, c)).map { a, bc in (a, bc.0, bc.1) } | ||
} | ||
func zip<A, B, C, D>(_ a: Parser<A>, _ b: Parser<B>, _ c: Parser<C>, _ d: Parser<D> ) -> Parser<(A, B, C, D)> { | ||
zip(a, zip(b, c, d)).map { a, bcd in (a, bcd.0, bcd.1, bcd.2) } | ||
} | ||
func zip<A, B, C, D, E>(_ a: Parser<A>, _ b: Parser<B>, _ c: Parser<C>, _ d: Parser<D>, _ e: Parser<E>) -> Parser<(A, B, C, D, E)> { | ||
zip(a, zip(b, c, d, e)).map { a, bcde in (a, bcde.0, bcde.1, bcde.2, bcde.3) } | ||
} | ||
// swiftlint:disable function_parameter_count line_length | ||
func zip<A, B, C, D, E, F>(_ a: Parser<A>, _ b: Parser<B>, _ c: Parser<C>, _ d: Parser<D>, _ e: Parser<E>, _ f: Parser<F>) -> Parser<(A, B, C, D, E, F)> { | ||
zip(a, zip(b, c, d, e, f)).map { a, bcdef in (a, bcdef.0, bcdef.1, bcdef.2, bcdef.3, bcdef.4) } | ||
} | ||
func zip<A, B, C, D, E, F, G>(_ a: Parser<A>, _ b: Parser<B>, _ c: Parser<C>, _ d: Parser<D>, _ e: Parser<E>, _ f: Parser<F>, _ g: Parser<G>) -> Parser<(A, B, C, D, E, F, G)> { | ||
zip(a, zip(b, c, d, e, f, g)).map { a, bcdefg in (a, bcdefg.0, bcdefg.1, bcdefg.2, bcdefg.3, bcdefg.4, bcdefg.5) } | ||
} | ||
// swiftlint:enable function_parameter_count line_length | ||
func prefix(while p: @escaping (Character) -> Bool) -> Parser<Substring> { | ||
Parser<Substring> { str in | ||
let prefix = str.prefix(while: p) | ||
str.removeFirst(prefix.count) | ||
return prefix | ||
} | ||
} | ||
let zeroOrMoreSpaces = prefix(while: { $0 == " " }).map { _ in () } | ||
let oneOrMoreSpaces = prefix(while: { $0 == " " }).flatMap { $0.isEmpty ? never() : always(()) } | ||
let zeroOrMoreSpacesOrLines = prefix(while: { $0 == " " || $0 == "\n" }).map { _ in () } | ||
func zeroOrMore<A>(_ p: Parser<A>, separatedBy s: Parser<Void> = literal("")) -> Parser<[A]> { | ||
Parser<[A]> { str in | ||
var rest = str | ||
var matches: [A] = [] | ||
while let match = p.run(&str) { | ||
rest = str | ||
matches.append(match) | ||
if s.run(&str) == nil { | ||
return matches | ||
} | ||
} | ||
str = rest | ||
return matches | ||
} | ||
} | ||
func oneOf<A>(_ ps: [Parser<A>]) -> Parser<A> { | ||
Parser<A> { str in | ||
for p in ps { | ||
if let match = p.run(&str) { | ||
return match | ||
} | ||
} | ||
return nil | ||
} | ||
} | ||
func not<A>(_ p: Parser<A>) -> Parser<Character> { | ||
Parser { str in | ||
let backup = str | ||
if p.run(&str) != nil { | ||
str = backup | ||
return nil | ||
} | ||
return charParser.run(&str) | ||
} | ||
} | ||
func string<A>(until: Parser<A>) -> Parser<String> { | ||
zeroOrMore(not(until)).map { String($0) } | ||
} |
Oops, something went wrong.