From d6d8805d2f22bf0dc0423733d2dafe1d4832f0d9 Mon Sep 17 00:00:00 2001 From: Leo Liu Date: Tue, 7 Nov 2023 16:31:33 -0500 Subject: [PATCH] initial feature implementation --- .gitignore | 6 ++ README.md | 18 ++++- SringCatalogEnum/Makefile | 5 +- .../Sources/SringCatalogEnum/main.swift | 69 +++++++++++++++---- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index c6bba59..1791cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# VIM +.*.sw* + +# macOS +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 965ffa1..bba3913 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,19 @@ If you are not familiar with [Apple's String Catalog](https://developer.apple.co - If you are dealing with an old base and needs to provide localization right now, add copies in the String Catalog. Otherwise leave them along. We'll revisit it later. - Whenever you want to add a new string to the project, always manually create an entry in `Localizable.xcstrings`. - Use a variable name friendly string as `Key`. - - TODO: plural forms etc. is out of the scope for now. + - TODO: plural forms etc. is to be added later. - Create `enum` in your code that contains all the String Catalog keys. + - It can be constructed manually, all you need is to use a `LocalizedStringKey` in `SwiftUI.View`, not the String itself. + - Helper function: `var key: LocalizedStringKey { LocalizedStringKey(rawValue) }` + - OR use the [SringCatalogEnum](./StringCatalogEnum) CLI tool in this repo. + - Copy `xcstrings-enum-generate` to your project. + - Added something like `xcstrings-enum-generate --xcstrings-path ../Resources/Localizable.xcstrings --output-filename ../Generated/XcodeString.swift` to generate the `enum`. + - OR use [SwiftGen](https://github.com/SwiftGen/SwiftGen/issues/1065) - hopefully better support will be added in future. + - OR explore how to achieve it with [Sourcery](https://github.com/krzysztofzablocki/Sourcery). - Use `LocalizedStringKey(key)` in your SwiftUI components. + - With `StringCatalogEnum`: `Text(XCS.welcomeBack.key)` +- At this point, you end up with a native `enum`, hopefully generated from your `String Catalog`. + - With `StringCatalogEnum`: remove hard-coded strings from your `SwiftUI` components and create manual keys and localized strings. ## Appendix @@ -68,4 +78,8 @@ I think this is because Apple, at its core, is a company that sells products. It Companies like Google, on the other hand, are built around technology. They don't hesitate to kill their services and update their tech stack, which sometimes results confusion and frustration. But they do offer more software architectural level of thinking, which can be insightful for developers in their ecosystems. -Of course, this is just a high level summary. Apple built async/wait APIs to promote better codes, and Google still uses XML (bah) for string resources. And just like iOS and Android, we are seeing both parties learn from each other. Hopefully technology will keep evolving. \ No newline at end of file +Of course, this is just a high level summary. Apple built async/wait APIs to promote better codes, and Google still uses XML (bah) for string resources. And just like iOS and Android, we are seeing both parties learn from each other. Hopefully technology will keep evolving. + +## Credit + +- CLI tool built from [SwiftCLITemplate](https://github.com/superarts/SwiftCLITemplate). \ No newline at end of file diff --git a/SringCatalogEnum/Makefile b/SringCatalogEnum/Makefile index 7880992..508fd2a 100644 --- a/SringCatalogEnum/Makefile +++ b/SringCatalogEnum/Makefile @@ -3,8 +3,9 @@ NAME=xcstrings-enum-generate all: debug ./$(NAME) \ --xcstrings-path ~/prj/ios/quible/Quible/Resources/Localizable.xcstrings \ - --enum-name XcodeStrings \ - --enum-type-alias X + --enum-name XcodeString \ + --enum-typealias X \ + --output-filename ~/prj/ios/quible/Quible/Const/Generated/XcodeString.swift debug: swift build diff --git a/SringCatalogEnum/Sources/SringCatalogEnum/main.swift b/SringCatalogEnum/Sources/SringCatalogEnum/main.swift index f256c7a..2304acc 100644 --- a/SringCatalogEnum/Sources/SringCatalogEnum/main.swift +++ b/SringCatalogEnum/Sources/SringCatalogEnum/main.swift @@ -15,6 +15,10 @@ struct SringCatalogEnum: ParsableCommand { case unexpectedJSON(message: String? = nil) } + enum Keyword: String, CaseIterable { + case `continue`, `default` + } + // @Argument(help: "The phrase to repeat.") // var phrase: String @@ -24,11 +28,14 @@ struct SringCatalogEnum: ParsableCommand { @Option(name: .long, help: "Full path and filename of the 'xcstrings' file.") var xcstringsPath: String + @Option(name: .long, help: "Full path and filename of the generated Swift file.") + var outputFilename: String + @Option(name: .long, help: "Generated enum name.") - var enumName: String = "XcodeString" + var enumName: String = "XcodeStringKey" @Option(name: .long, help: "A typealias of the generated enum name.") - var enumTypeAlias: String = "XCS" + var enumTypealias: String = "XCS" func run() throws { print("LOADING: \(xcstringsPath)") @@ -49,18 +56,22 @@ struct SringCatalogEnum: ParsableCommand { // print(strings) var output = """ - // This file is generated by XcodeStringEnum. Please do *NOT* update it manually. + // This file is generated by XcodeStringEnum. Please do *NOT* update it manually. + // As a common practice, swiftLint is disabled for generated files. + // swiftlint:disable all - import SwiftUI + import SwiftUI - /// Makes it a bit easier to type. - typealias \(enumTypeAlias) = \(enumName) + /// Makes it a bit easier to type. + typealias \(enumTypealias) = \(enumName) - enum \(enumName): String, CaseIterable { + /// Generated by SringCatalogEnum, this enum contains all existing Strin Category keys. + enum \(enumName): String, CaseIterable { - """ + """ var cases = [String]() + var knownCases = [String]() for (key, _) in strings { guard let name = convertToVariableName(key) else { print("SKIPPING: \(key)") @@ -69,10 +80,19 @@ struct SringCatalogEnum: ParsableCommand { guard key == name else { continue } + guard !knownCases.contains(name) else { + cases.append(" // TODO: fix duplicated entry - case \(name)\n") + continue + } + knownCases.append(name) // print("\(name):\t\(key)") // TODO: extract `localizations.en.stringUnit.value` and add in comments as inline documents - cases.append(" case \(name)\n") + if Keyword.allCases.map({ $0.rawValue }).contains(name) { + cases.append(" case `\(name)`\n") + } else { + cases.append(" case \(name)\n") + } } cases.sort() cases.forEach { string in @@ -81,10 +101,10 @@ struct SringCatalogEnum: ParsableCommand { output += """ - // MARK: - The following cases should be manually replaced in your codebase. + // MARK: - The following cases should be manually replaced in your codebase. - """ + """ cases.removeAll() for (key, _) in strings { guard let name = convertToVariableName(key) else { @@ -94,18 +114,41 @@ struct SringCatalogEnum: ParsableCommand { guard key != name else { continue } + guard !knownCases.contains(name) else { + cases.append(" // TODO: fix duplicated entry - case \(name)\n") + continue + } + knownCases.append(name) // print("\(name):\t\(key)") // TODO: probably missing " handling? - cases.append(" case \(name) = \"\(key.replacingOccurrences(of: "\n", with: "")))\"\n") + if Keyword.allCases.map({ $0.rawValue }).contains(name) { + cases.append(" case `\(name)` = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n") + } else { + cases.append(" case \(name) = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n") + } } + // cases = Array(Set(cases)) cases.sort() cases.forEach { string in output += string } - output += "}" + output += """ + + /// Usage: `SwiftUI.Text(\(enumTypealias).yourStringCatalogKey.key)` + var key: LocalizedStringKey { LocalizedStringKey(rawValue) } + + // var text: String { String(localized: key) } + + // var text: String.LocalizationValue { String.LocalizationValue(rawValue) } + } + // swiftlint:enable all + """ print(output) + let outputURL = URL(fileURLWithPath: outputFilename) + try output.write(to: outputURL, atomically: true, encoding: .utf8) + print("Written to: \(outputFilename)") } // TODO: add to some StringUtility