├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── LocoBuddy ├── Commands ├── Search.swift └── Translate.swift ├── Extensions ├── Collection-SingleElement.swift ├── Print-Colors.swift ├── String-Initials.swift ├── XMLElement-OnlyChild.swift └── XMLNode-OnlyChildText.swift ├── Internal ├── ANSIColors.swift └── Parser.swift ├── Models ├── CustomError.swift └── Entry.swift └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at julian@schiavo.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # LocoBuddy Contribution Guide 2 | 3 | *Adapted from the [Plot Contribution Guide](https://github.com/JohnSundell/Plot/blob/master/CONTRIBUTING.md)* 4 | 5 | Welcome to the **LocoBuddy Contribution Guide**, which aims to give you all the information you need to contribute to LocoBuddy. Thank you for your interest in contributing to this project! 6 | 7 | ## Bugs, feature requests and support 8 | 9 | ### I found a bug, how do I report it? 10 | 11 | If you find a bug, such as an issue in parsing glossaries, please file a detailed Github Issue containing all of the information necessary to reproduce the bug. This helps us to quickly understand the bug and triage it. 12 | 13 | ### I have an idea for a feature request 14 | 15 | Awesome! You can either create a Github Issue describing your idea and how it would improve the project, or create a Pull Request with your feature. Creating a Pull Request contributes to the project and makes it more likely your idea will be added! 16 | 17 | ### I have a question 18 | 19 | Please make sure you read the documentation (inline in the source files) and [README](README.md) carefully. If your question is still not answered, file a Github Issue detailing your question and what steps you took already. 20 | 21 | ## Project structure 22 | 23 | LocoBuddy is structured in the default way for Swift Packages. Source code is in the `Source/LocoBuddy` directory, with each type in a separate file. 24 | 25 | ## Conclusion 26 | 27 | Hopefully this document helped you better understand how LocoBuddy is structured and the best way to get help or contribute to the project. Thanks again for the interest! 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Julian Schiavo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 7 | "state": { 8 | "branch": null, 9 | "revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc", 10 | "version": "0.3.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LocoBuddy", 8 | platforms: [ 9 | .macOS(.v10_14) 10 | ], 11 | products: [ 12 | .executable(name: "LocoBuddy", targets: ["LocoBuddy"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.2") 16 | ], 17 | targets: [ 18 | .target(name: "LocoBuddy", dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🗣 LocoBuddy 2 | 3 | **LocoBuddy** is a Swift command-line tool that makes it easier to use and search Apple's `AppleGlot` [translation glossaries](https://developer.apple.com/download/more/), which are exports of translations used in Apple's system apps, such as Maps and Home, for all system languages. 4 | 5 | However, these translation glossaries are provided in `lg` file formats, and separated per each system app. LocoBuddy handles parsing these glossaries to search for specific terms and translate a found term into any number of given languages. 6 | 7 | This package is heavily inspired by and uses code adapted from [Douglas' in depth blog post on parsing these glossaries](https://douglashill.co/localisation-using-apples-glossaries/). 8 | 9 |
10 | 11 | ## Requirements 12 | 13 | **LocoBuddy** requires **macOS 11+** and is used directly on the command-line (Terminal). The command-line interface is created through Apple's [Swift Argument Parser](https://github.com/apple/swift-argument-parser) library, which is added through Swift Package Manager. 14 | 15 | *SF Symbols are used in output, so it is recommended to use an Apple SF font, such as SF Mono, on Terminal, but if not the tool will work and display unicode unknown symbols instead.* 16 | 17 |
18 | 19 | ## Installation 20 | 21 | You can download the latest build of **LocoBuddy**, or build it from the source in this repository. 22 | 23 | ### Prebuilt Install 24 | 25 | Download the `LocoBuddy` binary in the [latest release](https://github.com/julianschiavo/LocoBuddy/releases/latest), then run it from Terminal. 26 | 27 | ### Manual Build 28 | 29 | Clone this repository, then run `swift build` in the source folder using Terminal, and find the binary in the `.build/debug` or `.build/release` folder. 30 | 31 |
32 | 33 | ## Usage 34 | 35 | This tool requires Apple's translation glossaries to be downloaded and mounted; a valid Apple Developer account is required to download the glossaries. [Download](https://developer.apple.com/download/more/) the glossary languages you want from Apple's website by searching for glossaries, then mount the `dmg` disk images by double-clicking on each one. 36 | 37 | After mounting the disk images, search for terms in the glossaries you downloaded. For example, to search for "Book" in the Simplified Chinese glossary: 38 | 39 | ```bash 40 | LocoBuddy search --language "Simplified Chinese" --match contains "Book" 41 | ``` 42 | ``` 43 | 􀅴 Found 12 strings matching or containing `Cancel Ride`! 44 | 45 | 􀅶 Add to Bookmarks 46 | 􀰑 加入書籤 47 | 􀟕 URL_ACTION_BOOKMARK 48 | 􀎫 ContactsUI.lg 49 | ... 50 | ``` 51 | 52 | You can then translate a specific key (e.g. `URL_ACTION_BOOKMARK`) into one or more languages, using the initials or full names of the glossaries, at the same time: 53 | 54 | (SC is Simplified Chinese, TC is Traditional Chinese) 55 | 56 | ```bash 57 | LocoBuddy translate --key "URL_ACTION_BOOKMARK" "SC" "TC" 58 | ``` 59 | 60 | ``` 61 | 􀰑 加入書籤 62 | 􀎫 ContactsUI.lg 63 | 􀆪 Traditional Chinese 64 | 65 | 􀰑 添加到书签 66 | 􀎫 ContactsUI.lg 67 | 􀆪 Simplified Chinese 68 | ... 69 | ``` 70 | 71 |
72 | 73 | ## Contributing 74 | 75 | Contributions and pull requests are welcomed by anyone! If you find an issue with **LocoBuddy**, file a Github Issue, or, if you know how to fix it, submit a pull request. 76 | 77 | Please review our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contribution Guidelines](CONTRIBUTING.md) before making a contribution. 78 | 79 |
80 | 81 | ## Credit 82 | 83 | **LocoBuddy** was created by [Julian Schiavo](https://twitter.com/julianschiavo), and published under the [MIT License](LICENSE). The original code is adapted from Douglas Hill's post [*Localisation using Apple’s glossaries*](https://douglashill.co/localisation-using-apples-glossaries/) under the MIT License. 84 | 85 |
86 | 87 | ## License 88 | 89 | Available under the MIT License. See the [License](LICENSE) for more info. 90 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Commands/Search.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | 4 | /// A command that searches for translations for a term in a language. 5 | struct Search: ParsableCommand { 6 | public static let configuration = CommandConfiguration(abstract: "Search for translations for a term.") 7 | 8 | /// A text matching type 9 | enum Match: String, ExpressibleByArgument { 10 | /// Match exact matches 11 | case exact 12 | /// Match terms that contain the text 13 | case contains 14 | /// Match terms that contain each word in the text 15 | case containsAll 16 | } 17 | 18 | /// The language to find translations in. 19 | @Option(name: .shortAndLong, help: "The language to find translations in.") 20 | var language: String 21 | 22 | /// The text matching type, defaults to `contains`. 23 | @Option(name: .shortAndLong, help: "The text matching type, defaults to `contains`.") 24 | var match: Match = .contains 25 | 26 | /// The text to find. 27 | @Argument(help: "The text to find.") 28 | var text: String 29 | 30 | /// Runs the command 31 | /// - Throws: When there is an error in glossary parsing 32 | func run() throws { 33 | print("􀅴 Reading Glossaries...") 34 | let parser = Parser() 35 | 36 | let entries = try parser.parse(language: language) 37 | print("􀅴 Found \(entries.count) localization entries!") 38 | 39 | let keyedEntries = Dictionary(grouping: entries, by: \.base) 40 | print("􀅴 Found \(keyedEntries.count) unique strings!") 41 | 42 | let options = keyedEntries 43 | .filter { entry in 44 | switch match { 45 | case .exact: return entry.key.lowercased() == text 46 | case .contains: return entry.key.lowercased().contains(text.lowercased()) 47 | case .containsAll: 48 | let textWords = text.split(separator: " ") 49 | return entry.key.split(separator: " ").allSatisfy { textWords.contains($0) } 50 | } 51 | } 52 | .flatMap(\.value) 53 | print("􀅴 Found \(options.count) strings matching or containing `\(text)`!") 54 | 55 | if options.isEmpty { 56 | print("􀇾 Failed to find any translations containing \(text).", color: .red) 57 | return 58 | } 59 | 60 | print("") 61 | for option in options { 62 | print("􀅶 \(option.base)", color: .cyan) 63 | print("􀰑 \(option.translation)", color: .green) 64 | if let comment = option.comment { 65 | print("􀌪 \(comment)", color: .magenta) 66 | } 67 | print("􀟕 \(option.key)", color: .white) 68 | print("􀎫 \(option.fileURL.lastPathComponent)", color: .white) 69 | print("") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Commands/Translate.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | 4 | /// A command to translate a term into another language, using a key obtained from the `search` command. 5 | struct Translate: ParsableCommand { 6 | public static let configuration = CommandConfiguration(abstract: "Translate a term into another language, using a key obtained from the `search` command.") 7 | 8 | /// The languages to translate the term to. 9 | @Argument(help: "The languages to translate the term to.") 10 | var languages: [String] 11 | 12 | /// The key for the entry. 13 | @Option(name: .shortAndLong, help: "The key for the entry.") 14 | var key: String 15 | 16 | /// Runs the command 17 | /// - Throws: When there is an error in glossary parsing 18 | func run() throws { 19 | print("􀅴 Reading Glossaries...") 20 | let parser = Parser() 21 | 22 | let entries = try parser.parse(languages: languages) 23 | print("􀅴 Found \(entries.count) localization entries!") 24 | 25 | let keyedEntries = Dictionary(grouping: entries, by: \.key) 26 | print("􀅴 Found \(keyedEntries.count) unique strings!") 27 | 28 | let options = keyedEntries 29 | .filter { 30 | $0.key.lowercased() == key.lowercased() 31 | } 32 | .flatMap(\.value) 33 | 34 | if options.isEmpty { 35 | print("􀇾 Failed to find any translations for key \(key).", color: .red) 36 | return 37 | } 38 | 39 | print("") 40 | print("􀅶 \(options.first?.base ?? key)", color: .cyan) 41 | for option in options { 42 | print("􀰑 \(option.translation)", color: .green) 43 | print("􀎫 \(option.fileURL.lastPathComponent)", color: .white) 44 | print("􀆪 \(option.language)", color: .white) 45 | print("") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Extensions/Collection-SingleElement.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | /// The only element in the collection, or `nil` if there are multiple or zero elements. 3 | var single: Element? { count == 1 ? first : nil } 4 | } 5 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Extensions/Print-Colors.swift: -------------------------------------------------------------------------------- 1 | /// Writes the textual representations of the given items into the standard 2 | /// output. 3 | func print(_ text: String, color: ANSIColor) { 4 | print(color + text) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Extensions/String-Initials.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | // Adapted from https://stackoverflow.com/a/35286740 3 | var initials: String { 4 | components(separatedBy: " ") 5 | .reduce("") { 6 | $0.firstOrEmpty + $1.firstOrEmpty 7 | } 8 | } 9 | 10 | private var firstOrEmpty: String { 11 | if let first = first { 12 | return String(first) 13 | } 14 | return "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Extensions/XMLElement-OnlyChild.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension XMLElement { 4 | func onlyChild(named name: String) -> XMLElement? { 5 | elements(forName: name).single 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Extensions/XMLNode-OnlyChildText.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension XMLNode { 4 | var textOfOnlyChild: String? { 5 | guard let onlyChild = children?.single, 6 | onlyChild.kind == .text 7 | else { 8 | return nil 9 | } 10 | return onlyChild.stringValue 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Internal/ANSIColors.swift: -------------------------------------------------------------------------------- 1 | // Adapted from https://stackoverflow.com/a/30454802 2 | enum ANSIColor: String { 3 | case black = "\u{001B}[0;30m" 4 | case red = "\u{001B}[0;31m" 5 | case green = "\u{001B}[0;32m" 6 | case yellow = "\u{001B}[0;33m" 7 | case blue = "\u{001B}[0;34m" 8 | case magenta = "\u{001B}[0;35m" 9 | case cyan = "\u{001B}[0;36m" 10 | case white = "\u{001B}[0;37m" 11 | } 12 | 13 | func +(left: ANSIColor, right: String) -> String { 14 | return left.rawValue + right 15 | } 16 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Internal/Parser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Parser { 4 | 5 | // MARK: - Public 6 | 7 | func parse(language: String) throws -> [Entry] { 8 | guard let volumes = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil, options: []) else { 9 | throw CustomError("Failed to mount disk image volumes.") 10 | } 11 | 12 | let languageVolumes = volumes.filter { fileURL -> Bool in 13 | fileURL.lastPathComponent.contains(language) 14 | } 15 | 16 | guard !languageVolumes.isEmpty else { 17 | throw CustomError("Failed to find volumes for \(language). Download the glossaries from https://developer.apple.com/download/more/ and mount the disk images, then try again.") 18 | } 19 | 20 | return try languageVolumes.flatMap(parseVolume) 21 | } 22 | 23 | func parse(languages: [String]) throws -> [Entry] { 24 | guard let volumes = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil, options: []) else { 25 | throw CustomError("Failed to mount disk image volumes.") 26 | } 27 | 28 | let languageVolumes = volumes 29 | .filter { !$0.lastPathComponent.contains("System") } 30 | .filter { fileURL in 31 | for language in languages { 32 | if fileURL.lastPathComponent.lowercased() == language.lowercased() || 33 | fileURL.lastPathComponent.initials.lowercased() == language.lowercased() { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | guard !languageVolumes.isEmpty else { 41 | throw CustomError("Failed to find volumes for any language in \(languages). Download the glossaries from https://developer.apple.com/download/more/ and mount the disk images, then try again.") 42 | } 43 | 44 | return try languageVolumes.flatMap(parseVolume) 45 | } 46 | 47 | // MARK: - Private 48 | 49 | private func parseVolume(at url: URL) throws -> [Entry] { 50 | let glossaryURLs = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) 51 | return try glossaryURLs.flatMap { try parseDocument(at: $0, language: url.lastPathComponent) } 52 | } 53 | 54 | private func parseDocument(at url: URL, language: String) throws -> [Entry] { 55 | let document = try XMLDocument(contentsOf: url, options: [.nodePreserveWhitespace]) 56 | guard let root = document.rootElement() else { 57 | throw CustomError("Invalid Glossary Format.") 58 | } 59 | 60 | return try root.elements(forName: "File").flatMap { file in 61 | try file.elements(forName: "TextItem").compactMap { element in 62 | try parseElement(element, at: url, language: language) 63 | } 64 | } 65 | } 66 | 67 | private func parseElement(_ element: XMLElement, at url: URL, language: String) throws -> Entry? { 68 | guard let translationSet = element.onlyChild(named: "TranslationSet"), 69 | let base = translationSet.onlyChild(named: "base")?.textOfOnlyChild, 70 | let translation = translationSet.onlyChild(named: "tran")?.textOfOnlyChild 71 | else { 72 | return nil 73 | } 74 | let comment = element.onlyChild(named: "Description")?.textOfOnlyChild 75 | let key = element.onlyChild(named: "Position")?.textOfOnlyChild ?? "" 76 | 77 | return Entry(fileURL: url, comment: comment, key: key, base: base, translation: translation, language: language) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Models/CustomError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CustomError: LocalizedError { 4 | let errorDescription: String? 5 | 6 | init(_ errorDescription: String) { 7 | self.errorDescription = errorDescription 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/Models/Entry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Entry { 4 | /// The file the entry was read from. 5 | let fileURL: URL 6 | /// The usage description for the entry. 7 | let comment: String? 8 | /// The key identifying this string. This can be because some Apple strings files use just whitespace as a key and `NSXMLDocument` cannot read whitespace-only text elements. 9 | let key: String 10 | /// The original text. 11 | let base: String 12 | /// The localised text. 13 | let translation: String 14 | /// The language of the translated term. 15 | let language: String 16 | } 17 | -------------------------------------------------------------------------------- /Sources/LocoBuddy/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | 3 | struct LocoBuddy: ParsableCommand { 4 | static let configuration = CommandConfiguration( 5 | abstract: "A command-line tool to parse and traverse AppleGlot Localization Glossaries", 6 | subcommands: [Search.self, Translate.self]) 7 | 8 | init() { } 9 | } 10 | 11 | LocoBuddy.main() 12 | --------------------------------------------------------------------------------