Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
luizmb committed Jun 18, 2020
1 parent 6463f49 commit 0909acf
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 3 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ playground.xcworkspace
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
*.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm

.build/

Expand All @@ -57,7 +57,7 @@ playground.xcworkspace
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
*.xcworkspace

# Carthage
#
Expand Down
Binary file added MergeL10n
Binary file not shown.
25 changes: 25 additions & 0 deletions Package.resolved
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
}
23 changes: 23 additions & 0 deletions Package.swift
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")
]
)
]
)
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# MergeL10n

A description of this package.
16 changes: 16 additions & 0 deletions Sources/MergeL10n/Commands/MergeL10n.swift
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]
)
}
164 changes: 164 additions & 0 deletions Sources/MergeL10n/Commands/PseudoToLanguages.swift
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."
))
}
}
}
129 changes: 129 additions & 0 deletions Sources/MergeL10n/FunctionalParserLib/Parser.swift
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) }
}
Loading

0 comments on commit 0909acf

Please sign in to comment.