Add Swift Package plugin
This commit is contained in:
parent
475e30e784
commit
3a0eda789f
7
Documentation/Generation/README.md
Normal file
7
Documentation/Generation/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Reference Documentation
|
||||
|
||||
## Enums
|
||||
|
||||
- [Generation](enums/Generation.md)
|
||||
|
||||
This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs)
|
||||
10
Documentation/Generation/enums/Generation.md
Normal file
10
Documentation/Generation/enums/Generation.md
Normal file
@ -0,0 +1,10 @@
|
||||
**ENUM**
|
||||
|
||||
# `Generation`
|
||||
|
||||
A type containing the generation function for the plugin.
|
||||
|
||||
## Methods
|
||||
### `main()`
|
||||
|
||||
Generate the Swift code for the plugin.
|
||||
8
Documentation/GenerationLibrary/README.md
Normal file
8
Documentation/GenerationLibrary/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Reference Documentation
|
||||
|
||||
## Enums
|
||||
|
||||
- [Generation](enums/Generation.md)
|
||||
- [Generation.GenerationError](enums/Generation.GenerationError.md)
|
||||
|
||||
This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs)
|
||||
@ -0,0 +1,19 @@
|
||||
**ENUM**
|
||||
|
||||
# `Generation.GenerationError`
|
||||
|
||||
An error that occurs during code generation.
|
||||
|
||||
## Cases
|
||||
### `missingTranslationInDefaultLanguage(key:)`
|
||||
|
||||
A translation in the default language missing for a specific key.
|
||||
Missing translations in other languages will cause the default language to be used.
|
||||
|
||||
### `unknownYMLPasingError`
|
||||
|
||||
An unknown error occured while parsing the YML.
|
||||
|
||||
### `missingDefaultLanguage`
|
||||
|
||||
The default language information is missing.
|
||||
81
Documentation/GenerationLibrary/enums/Generation.md
Normal file
81
Documentation/GenerationLibrary/enums/Generation.md
Normal file
@ -0,0 +1,81 @@
|
||||
**ENUM**
|
||||
|
||||
# `Generation`
|
||||
|
||||
Generate the Swift code for the plugin and macro.
|
||||
|
||||
## Properties
|
||||
### `indentOne`
|
||||
|
||||
Number of spaces for indentation 1.
|
||||
|
||||
### `indentTwo`
|
||||
|
||||
Number of spaces for indentation 2.
|
||||
|
||||
### `indentThree`
|
||||
|
||||
Number of spaces for indentation 3.
|
||||
|
||||
## Methods
|
||||
### `getCode(yml:)`
|
||||
|
||||
Get the Swift code for the plugin and macro.
|
||||
- Parameter yml: The YML code.
|
||||
- Returns: The code.
|
||||
|
||||
### `generateEnumCases(dictionary:)`
|
||||
|
||||
Generate the cases for the `Localized` enumeration.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateStaticLocVariables(dictionary:)`
|
||||
|
||||
Generate the static variables and functions for the `Loc` type.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateTranslations(dictionary:defaultLanguage:)`
|
||||
|
||||
Generate the variables for the translations.
|
||||
- Parameters:
|
||||
- dictionary: The parsed YML.
|
||||
- defaultLanguage: The default language.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateLanguageFunction(dictionary:defaultLanguage:)`
|
||||
|
||||
Generate the function for getting the translated string for a specified language code.
|
||||
- Parameters:
|
||||
- dictionary: The parsed YML.
|
||||
- defaultLanguage: The default language.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `getLanguages(dictionary:)`
|
||||
|
||||
Get the available languages.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax
|
||||
|
||||
### `parse(key:)`
|
||||
|
||||
Parse the key for a phrase.
|
||||
- Parameter key: The key definition including parameters.
|
||||
- Returns: The key.
|
||||
|
||||
### `parse(translation:arguments:)`
|
||||
|
||||
Parse the translation for a phrase.
|
||||
- Parameters:
|
||||
- translation: The translation without correct escaping.
|
||||
- arguments: The arguments.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `indent(_:by:)`
|
||||
|
||||
Indent each line of a text by a certain amount of whitespaces.
|
||||
- Parameters:
|
||||
- string: The text.
|
||||
- count: The indentation.
|
||||
- Returns: The syntax.
|
||||
@ -8,19 +8,6 @@ Access a specific language using `Localized.key.language`, or use `Localized.key
|
||||
which automatically uses the system language on Linux, macOS and Windows.
|
||||
Use `Loc.key` for a quick access to the automatically localized value.
|
||||
|
||||
## Properties
|
||||
### `indentOne`
|
||||
|
||||
Number of spaces for indentation 1.
|
||||
|
||||
### `indentTwo`
|
||||
|
||||
Number of spaces for indentation 2.
|
||||
|
||||
### `indentThree`
|
||||
|
||||
Number of spaces for indentation 3.
|
||||
|
||||
## Methods
|
||||
### `expansion(of:in:)`
|
||||
|
||||
@ -29,57 +16,3 @@ Expand the `localized` macro.
|
||||
- node: Information about the macro call.
|
||||
- context: The expansion context.
|
||||
- Returns: The enumerations `Localized` and `Loc`.
|
||||
|
||||
### `generateEnumCases(dictionary:)`
|
||||
|
||||
Generate the cases for the `Localized` enumeration.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateStaticLocVariables(dictionary:)`
|
||||
|
||||
Generate the static variables and functions for the `Loc` type.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateTranslations(dictionary:)`
|
||||
|
||||
Generate the variables for the translations.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `generateLanguageFunction(dictionary:defaultLanguage:)`
|
||||
|
||||
Generate the function for getting the translated string for a specified language code.
|
||||
- Parameters:
|
||||
- dictionary: The parsed YML.
|
||||
- defaultLanguage: The syntax for the default language.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `getLanguages(dictionary:)`
|
||||
|
||||
Get the available languages.
|
||||
- Parameter dictionary: The parsed YML.
|
||||
- Returns: The syntax
|
||||
|
||||
### `parse(key:)`
|
||||
|
||||
Parse the key for a phrase.
|
||||
- Parameter key: The key definition including parameters.
|
||||
- Returns: The key.
|
||||
|
||||
### `parse(translation:arguments:)`
|
||||
|
||||
Parse the translation for a phrase.
|
||||
- Parameters:
|
||||
- translation: The translation without correct escaping.
|
||||
- arguments: The arguments.
|
||||
- Returns: The syntax.
|
||||
|
||||
### `indent(_:by:)`
|
||||
|
||||
Indent each line of a text by a certain amount of whitespaces.
|
||||
- Parameters:
|
||||
- string: The text.
|
||||
- count: The indentation.
|
||||
- Returns: The syntax.
|
||||
|
||||
@ -7,3 +7,11 @@ Find documentation for the `localized` macro [here](LocalizedMacros/README.md).
|
||||
## Localized
|
||||
|
||||
Find documentation for the `System` enumeration [here](Localized/README.md).
|
||||
|
||||
## Generation
|
||||
|
||||
Find documentation for the generation executable used by the plugin [here](Generation/README.md).
|
||||
|
||||
## GenerationLibrary
|
||||
|
||||
Find documentation for the generation used by the plugin and macro [here](GenerationLibrary/README.md).
|
||||
|
||||
2
Makefile
2
Makefile
@ -1,6 +1,8 @@
|
||||
docs:
|
||||
@sourcedocs generate --min-acl private -r --spm-module Localized --output-folder Documentation/Localized
|
||||
@sourcedocs generate --min-acl private -r --spm-module LocalizedMacros --output-folder Documentation/LocalizedMacros
|
||||
@sourcedocs generate --min-acl private -r --spm-module Generation --output-folder Documentation/Generation
|
||||
@sourcedocs generate --min-acl private -r --spm-module GenerationLibrary --output-folder Documentation/GenerationLibrary
|
||||
|
||||
swiftlint:
|
||||
@swiftlint --autocorrect
|
||||
|
||||
@ -17,6 +17,10 @@ let package = Package(
|
||||
.library(
|
||||
name: "Localized",
|
||||
targets: ["Localized"]
|
||||
),
|
||||
.plugin(
|
||||
name: "GenerateLocalized",
|
||||
targets: ["GenerateLocalized"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
@ -25,13 +29,32 @@ let package = Package(
|
||||
.package(url: "https://github.com/stackotter/swift-macro-toolkit", from: "0.3.1")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "GenerationLibrary",
|
||||
dependencies: [
|
||||
.product(name: "Yams", package: "Yams")
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "Generation",
|
||||
dependencies: [
|
||||
"GenerationLibrary"
|
||||
]
|
||||
),
|
||||
.macro(
|
||||
name: "LocalizedMacros",
|
||||
dependencies: [
|
||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||
.product(name: "MacroToolkit", package: "swift-macro-toolkit"),
|
||||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
||||
.product(name: "Yams", package: "Yams")
|
||||
"GenerationLibrary"
|
||||
]
|
||||
),
|
||||
.plugin(
|
||||
name: "GenerateLocalized",
|
||||
capability: .buildTool(),
|
||||
dependencies: [
|
||||
"Generation"
|
||||
]
|
||||
),
|
||||
.target(
|
||||
@ -41,11 +64,24 @@ let package = Package(
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "LocalizedTests",
|
||||
name: "MacroTests",
|
||||
dependencies: [
|
||||
"Localized"
|
||||
],
|
||||
path: "Tests"
|
||||
path: "Tests/MacroTests"
|
||||
),
|
||||
.executableTarget(
|
||||
name: "PluginTests",
|
||||
dependencies: [
|
||||
"Localized"
|
||||
],
|
||||
path: "Tests/PluginTests",
|
||||
resources: [
|
||||
.process("Localized.yml")
|
||||
],
|
||||
plugins: [
|
||||
"GenerateLocalized"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
39
Plugins/GenerateLocalized/Plugin.swift
Normal file
39
Plugins/GenerateLocalized/Plugin.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// Plugin.swift
|
||||
// Localized
|
||||
//
|
||||
// Created by david-swift on 02.03.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PackagePlugin
|
||||
|
||||
/// The build tool plugin for generating Swift code from the `Localized.yml` file.
|
||||
@main
|
||||
struct Plugin: BuildToolPlugin {
|
||||
|
||||
/// Create the commands for generating the code.
|
||||
/// - Parameters:
|
||||
/// - context: The plugin context.
|
||||
/// - target: The target.
|
||||
/// - Returns: The commands.
|
||||
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
|
||||
guard let target = target.sourceModule,
|
||||
let inputFile = target.sourceFiles.first(
|
||||
where: { ["Localized.yml", "Localized.yaml"].contains($0.path.lastComponent) }
|
||||
) else {
|
||||
return []
|
||||
}
|
||||
let outputFile = context.pluginWorkDirectory.appending(subpath: "Localized.swift")
|
||||
return [
|
||||
.buildCommand(
|
||||
displayName: "Generating Localized.swift",
|
||||
executable: try context.tool(named: "Generation").path,
|
||||
arguments: [inputFile.path.string, outputFile.string],
|
||||
inputFiles: [inputFile.path],
|
||||
outputFiles: [outputFile]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
45
README.md
45
README.md
@ -13,7 +13,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
_Localized_ provides a macro for localizing cross-platform Swift code.
|
||||
_Localized_ provides a Swift package plugin for localizing cross-platform Swift code.
|
||||
|
||||
Use YML syntax for defining available phrases:
|
||||
|
||||
@ -60,7 +60,44 @@ print(Localized.house.fr)
|
||||
|
||||
### Definition
|
||||
|
||||
Define the available phrases using YML.
|
||||
Define the available phrases in a file called `Localized.yml`.
|
||||
|
||||
```yml
|
||||
default: en
|
||||
|
||||
export:
|
||||
en: Export Document
|
||||
de: Exportiere das Dokument
|
||||
|
||||
send(message, name):
|
||||
en: Send (message) to (name).
|
||||
de: Sende (message) to (name).
|
||||
```
|
||||
|
||||
As you can see, you can add parameters using brackets after the key.
|
||||
|
||||
The line `default: en` sets English as the fallback language.
|
||||
|
||||
Then, add the `Localized` dependency, the plugin and the `Localized.yml` resource
|
||||
to the target in the `Package.swift` file.
|
||||
|
||||
```swift
|
||||
.executableTarget(
|
||||
name: "PluginTests",
|
||||
dependencies: ["Localized"],
|
||||
resources: [.process("Localized.yml")],
|
||||
plugins: ["GenerateLocalized"]
|
||||
)
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
<summary> Use the Swift macro alternatively </summary>
|
||||
|
||||
If you don't want to have a separate `Localized.yml` resource, you can use the
|
||||
YML syntax directly in your Swift code using a Swift macro.
|
||||
Leave out the `resources` and `plugins` lines in the target definition, and
|
||||
instead of creating a `Localized.yml` file, use the following macro in a Swift file.
|
||||
|
||||
```swift
|
||||
#localized(default: "en", yml: """
|
||||
@ -74,7 +111,9 @@ send(message, name):
|
||||
""")
|
||||
```
|
||||
|
||||
As you can see, you can add parameters using brackets after the key.
|
||||
You cannot have a `defaultLanguage` set in the YML, instead, use the macro parameter.
|
||||
|
||||
</details>
|
||||
|
||||
### Usage
|
||||
|
||||
|
||||
26
Sources/Generation/Generation.swift
Normal file
26
Sources/Generation/Generation.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// Generation.swift
|
||||
// Localized
|
||||
//
|
||||
// Created by david-swift on 02.03.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GenerationLibrary
|
||||
|
||||
/// A type containing the generation function for the plugin.
|
||||
@main
|
||||
public enum Generation {
|
||||
|
||||
/// Generate the Swift code for the plugin.
|
||||
public static func main() throws {
|
||||
let yml = try String(contentsOfFile: CommandLine.arguments[1])
|
||||
let content = try GenerationLibrary.Generation.getCode(yml: yml)
|
||||
let outputPathIndex = 2
|
||||
FileManager.default.createFile(
|
||||
atPath: CommandLine.arguments[outputPathIndex],
|
||||
contents: .init(("import Localized" + "\n\n" + content[0] + "\n\n" + content[1]).utf8)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
247
Sources/GenerationLibrary/Generation.swift
Normal file
247
Sources/GenerationLibrary/Generation.swift
Normal file
@ -0,0 +1,247 @@
|
||||
//
|
||||
// Generation.swift
|
||||
// Localized
|
||||
//
|
||||
// Created by david-swift on 02.03.2024.
|
||||
//
|
||||
|
||||
import Yams
|
||||
|
||||
/// Generate the Swift code for the plugin and macro.
|
||||
public enum Generation {
|
||||
|
||||
/// Number of spaces for indentation 1.
|
||||
static let indentOne = 4
|
||||
/// Number of spaces for indentation 2.
|
||||
static let indentTwo = 8
|
||||
/// Number of spaces for indentation 3.
|
||||
static let indentThree = 12
|
||||
|
||||
/// An error that occurs during code generation.
|
||||
public enum GenerationError: Error {
|
||||
|
||||
/// A translation in the default language missing for a specific key.
|
||||
/// Missing translations in other languages will cause the default language to be used.
|
||||
case missingTranslationInDefaultLanguage(key: String)
|
||||
/// An unknown error occured while parsing the YML.
|
||||
case unknownYMLPasingError
|
||||
/// The default language information is missing.
|
||||
case missingDefaultLanguage
|
||||
|
||||
}
|
||||
|
||||
/// Get the Swift code for the plugin and macro.
|
||||
/// - Parameter yml: The YML code.
|
||||
/// - Returns: The code.
|
||||
public static func getCode(yml: String) throws -> [String] {
|
||||
guard var dict = try Yams.load(yaml: yml) as? [String: Any] else {
|
||||
throw GenerationError.unknownYMLPasingError
|
||||
}
|
||||
guard let defaultLanguage = dict["default"] as? String else {
|
||||
throw GenerationError.missingDefaultLanguage
|
||||
}
|
||||
dict["default"] = nil
|
||||
guard let dictionary = dict as? [String: [String: String]] else {
|
||||
throw GenerationError.unknownYMLPasingError
|
||||
}
|
||||
return [
|
||||
"""
|
||||
enum Localized {
|
||||
|
||||
static var yml: String {
|
||||
\"""
|
||||
\(indent(yml, by: indentTwo))
|
||||
\"""
|
||||
}
|
||||
|
||||
\(generateEnumCases(dictionary: dictionary))
|
||||
|
||||
var string: String { string(for: System.getLanguage()) }
|
||||
|
||||
\(try generateTranslations(dictionary: dictionary, defaultLanguage: defaultLanguage))
|
||||
|
||||
\(generateLanguageFunction(dictionary: dictionary, defaultLanguage: defaultLanguage))
|
||||
|
||||
}
|
||||
""",
|
||||
"""
|
||||
enum Loc {
|
||||
|
||||
\(generateStaticLocVariables(dictionary: dictionary))
|
||||
|
||||
}
|
||||
"""
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate the cases for the `Localized` enumeration.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax.
|
||||
static func generateEnumCases(dictionary: [String: [String: String]]) -> String {
|
||||
var result = ""
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
if key.1.isEmpty {
|
||||
result.append("""
|
||||
case \(entry.key)
|
||||
|
||||
""")
|
||||
} else {
|
||||
var line = "case \(key.0)("
|
||||
for argument in key.1 {
|
||||
line += "\(argument): String, "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ")"
|
||||
result.append("""
|
||||
\(line)
|
||||
|
||||
""")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the static variables and functions for the `Loc` type.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax.
|
||||
static func generateStaticLocVariables(dictionary: [String: [String: String]]) -> String {
|
||||
var result = ""
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
if key.1.isEmpty {
|
||||
result.append("""
|
||||
static var \(entry.key): String { Localized.\(entry.key).string }
|
||||
|
||||
""")
|
||||
} else {
|
||||
var line = "static func \(key.0)("
|
||||
for argument in key.1 {
|
||||
line += "\(argument): String, "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ") -> String {\n" + indent("Localized.\(key.0)(", by: indentOne)
|
||||
for argument in key.1 {
|
||||
line += "\(argument): \(argument), "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ").string"
|
||||
line += "\n}"
|
||||
result.append("""
|
||||
\(line)
|
||||
|
||||
""")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the variables for the translations.
|
||||
/// - Parameters:
|
||||
/// - dictionary: The parsed YML.
|
||||
/// - defaultLanguage: The default language.
|
||||
/// - Returns: The syntax.
|
||||
static func generateTranslations(dictionary: [String: [String: String]], defaultLanguage: String) throws -> String {
|
||||
var result = ""
|
||||
for language in getLanguages(dictionary: dictionary) {
|
||||
var variable = indent("var \(language): String {", by: indentOne)
|
||||
variable += indent("\nswitch self {", by: indentTwo)
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
guard let valueForLanguage = entry.value[language] ?? entry.value[defaultLanguage] else {
|
||||
throw GenerationError.missingTranslationInDefaultLanguage(key: key.0)
|
||||
}
|
||||
if key.1.isEmpty {
|
||||
variable += indent("\ncase .\(entry.key):", by: indentTwo)
|
||||
variable += indent("\n\"\(valueForLanguage)\"", by: indentThree)
|
||||
} else {
|
||||
let translation = parse(translation: valueForLanguage, arguments: key.1)
|
||||
variable += indent("\ncase let .\(entry.key):", by: indentTwo)
|
||||
variable += indent("\n\"\(translation)\"", by: indentThree)
|
||||
}
|
||||
}
|
||||
variable += indent("\n }\n}", by: indentOne)
|
||||
result += """
|
||||
\(variable)
|
||||
|
||||
"""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the function for getting the translated string for a specified language code.
|
||||
/// - Parameters:
|
||||
/// - dictionary: The parsed YML.
|
||||
/// - defaultLanguage: The default language.
|
||||
/// - Returns: The syntax.
|
||||
static func generateLanguageFunction(
|
||||
dictionary: [String: [String: String]],
|
||||
defaultLanguage: String
|
||||
) -> String {
|
||||
var result = "func string(for language: String) -> String {\n"
|
||||
for language in getLanguages(dictionary: dictionary) where language != defaultLanguage {
|
||||
result += indent("if language.hasPrefix(\"\(language)\") {", by: indentTwo)
|
||||
result += indent("\nreturn \(language)", by: indentThree)
|
||||
result += indent("\n} else", by: indentTwo)
|
||||
}
|
||||
result += """
|
||||
{
|
||||
return \(defaultLanguage)
|
||||
}
|
||||
}
|
||||
"""
|
||||
return result
|
||||
}
|
||||
|
||||
/// Get the available languages.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax
|
||||
static func getLanguages(dictionary: [String: [String: String]]) -> [String] {
|
||||
var languages: Set<String> = []
|
||||
for key in dictionary {
|
||||
languages = languages.union(key.value.map { $0.key })
|
||||
}
|
||||
return .init(languages)
|
||||
}
|
||||
|
||||
/// Parse the key for a phrase.
|
||||
/// - Parameter key: The key definition including parameters.
|
||||
/// - Returns: The key.
|
||||
static func parse(key: String) -> (String, [String]) {
|
||||
let parts = key.split(separator: "(")
|
||||
if parts.count == 1 {
|
||||
return (key, [])
|
||||
}
|
||||
let arguments = parts[1].dropLast().split(separator: ", ").map { String($0) }
|
||||
return (.init(parts[0]), arguments)
|
||||
}
|
||||
|
||||
/// Parse the translation for a phrase.
|
||||
/// - Parameters:
|
||||
/// - translation: The translation without correct escaping.
|
||||
/// - arguments: The arguments.
|
||||
/// - Returns: The syntax.
|
||||
static func parse(translation: String, arguments: [String]) -> String {
|
||||
var translation = translation
|
||||
for argument in arguments {
|
||||
translation.replace("(\(argument))", with: "\\(\(argument))")
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
||||
/// Indent each line of a text by a certain amount of whitespaces.
|
||||
/// - Parameters:
|
||||
/// - string: The text.
|
||||
/// - count: The indentation.
|
||||
/// - Returns: The syntax.
|
||||
static func indent(_ string: String, by count: Int) -> String {
|
||||
.init(
|
||||
string
|
||||
.components(separatedBy: "\n")
|
||||
.map { "\n" + Array(repeating: " ", count: count).joined() + $0 }
|
||||
.joined()
|
||||
.trimmingPrefix("\n")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,12 +5,7 @@
|
||||
// Created by david-swift on 27.02.2024.
|
||||
//
|
||||
|
||||
/// A macro that produces both a value and a string containing the
|
||||
/// source code that generated the value. For example,
|
||||
///
|
||||
/// #stringify(x + y)
|
||||
///
|
||||
/// produces a tuple `(x + y, "x + y")`.
|
||||
/// A macro that takes the YML syntax and converts it into enumerations.
|
||||
@freestanding(declaration, names: named(Localized), named(Loc))
|
||||
public macro localized(default defaultLanguage: String, yml: String) = #externalMacro(
|
||||
module: "LocalizedMacros",
|
||||
|
||||
@ -5,12 +5,10 @@
|
||||
// Created by david-swift on 27.02.2024.
|
||||
//
|
||||
|
||||
// swiftlint:disable force_unwrapping force_cast
|
||||
|
||||
import GenerationLibrary
|
||||
import MacroToolkit
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxMacros
|
||||
import Yams
|
||||
|
||||
/// Implementation of the `localized` macro, which takes YML
|
||||
/// as a string and converts it into two enumerations.
|
||||
@ -19,13 +17,6 @@ import Yams
|
||||
/// Use `Loc.key` for a quick access to the automatically localized value.
|
||||
public struct LocalizedMacro: DeclarationMacro {
|
||||
|
||||
/// Number of spaces for indentation 1.
|
||||
static let indentOne = 4
|
||||
/// Number of spaces for indentation 2.
|
||||
static let indentTwo = 8
|
||||
/// Number of spaces for indentation 3.
|
||||
static let indentThree = 12
|
||||
|
||||
/// The errors the expansion can throw.
|
||||
public enum LocalizedError: Error {
|
||||
|
||||
@ -45,199 +36,16 @@ public struct LocalizedMacro: DeclarationMacro {
|
||||
of node: some SwiftSyntax.FreestandingMacroExpansionSyntax,
|
||||
in context: some SwiftSyntaxMacros.MacroExpansionContext
|
||||
) throws -> [SwiftSyntax.DeclSyntax] {
|
||||
guard let `default` = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self) else {
|
||||
guard let `default` = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
|
||||
let defaultLanguage = StringLiteral(`default`).value?.description else {
|
||||
throw LocalizedError.invalidDefaultLanguage
|
||||
}
|
||||
guard let syntax = node.argumentList.last?.expression.as(StringLiteralExprSyntax.self) else {
|
||||
guard let syntax = node.argumentList.last?.expression.as(StringLiteralExprSyntax.self),
|
||||
var yml = StringLiteral(syntax).value else {
|
||||
throw LocalizedError.invalidStringLiteral
|
||||
}
|
||||
let dictionary = try Yams.load(yaml: StringLiteral(syntax).value!) as! [String: [String: String]]
|
||||
return [
|
||||
"""
|
||||
enum Localized {
|
||||
|
||||
static var yml: String {
|
||||
\"""
|
||||
\(raw: indent(StringLiteral(syntax).value!.description, by: indentTwo))
|
||||
\"""
|
||||
}
|
||||
|
||||
\(raw: generateEnumCases(dictionary: dictionary))
|
||||
|
||||
var string: String { string(for: System.getLanguage()) }
|
||||
|
||||
\(raw: generateTranslations(dictionary: dictionary))
|
||||
|
||||
\(raw: generateLanguageFunction(dictionary: dictionary, defaultLanguage: `default`))
|
||||
|
||||
}
|
||||
""",
|
||||
"""
|
||||
enum Loc {
|
||||
|
||||
\(raw: generateStaticLocVariables(dictionary: dictionary))
|
||||
|
||||
}
|
||||
"""
|
||||
]
|
||||
yml.append("\n\ndefault: \"\(defaultLanguage)\"")
|
||||
return try Generation.getCode(yml: yml).map { "\(raw: $0)" }
|
||||
}
|
||||
|
||||
/// Generate the cases for the `Localized` enumeration.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax.
|
||||
static func generateEnumCases(dictionary: [String: [String: String]]) -> String {
|
||||
var result = ""
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
if key.1.isEmpty {
|
||||
result.append("""
|
||||
case \(entry.key)
|
||||
""")
|
||||
} else {
|
||||
var line = "case \(key.0)("
|
||||
for argument in key.1 {
|
||||
line += "\(argument): String, "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ")"
|
||||
result.append("""
|
||||
\(line)
|
||||
""")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the static variables and functions for the `Loc` type.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax.
|
||||
static func generateStaticLocVariables(dictionary: [String: [String: String]]) -> String {
|
||||
var result = ""
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
if key.1.isEmpty {
|
||||
result.append("""
|
||||
static var \(entry.key): String { Localized.\(entry.key).string }
|
||||
""")
|
||||
} else {
|
||||
var line = "static func \(key.0)("
|
||||
for argument in key.1 {
|
||||
line += "\(argument): String, "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ") -> String {\n" + indent("Localized.\(key.0)(", by: indentOne)
|
||||
for argument in key.1 {
|
||||
line += "\(argument): \(argument), "
|
||||
}
|
||||
line.removeLast(", ".count)
|
||||
line += ").string"
|
||||
line += "\n}"
|
||||
result.append("""
|
||||
\(line)
|
||||
""")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the variables for the translations.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax.
|
||||
static func generateTranslations(dictionary: [String: [String: String]]) -> String {
|
||||
var result = ""
|
||||
for language in getLanguages(dictionary: dictionary) {
|
||||
var variable = indent("var \(language): String {", by: indentOne)
|
||||
variable += indent("\nswitch self {", by: indentTwo)
|
||||
for entry in dictionary {
|
||||
let key = parse(key: entry.key)
|
||||
if key.1.isEmpty {
|
||||
variable += indent("\ncase .\(entry.key):", by: indentTwo)
|
||||
variable += indent("\n\"\(entry.value[language]!)\"", by: indentThree)
|
||||
} else {
|
||||
let translation = parse(translation: entry.value[language]!, arguments: key.1)
|
||||
variable += indent("\ncase let .\(entry.key):", by: indentTwo)
|
||||
variable += indent("\n\"\(translation)\"", by: indentThree)
|
||||
}
|
||||
}
|
||||
variable += indent("\n }\n}", by: indentOne)
|
||||
result += """
|
||||
\(variable)
|
||||
"""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Generate the function for getting the translated string for a specified language code.
|
||||
/// - Parameters:
|
||||
/// - dictionary: The parsed YML.
|
||||
/// - defaultLanguage: The syntax for the default language.
|
||||
/// - Returns: The syntax.
|
||||
static func generateLanguageFunction(
|
||||
dictionary: [String: [String: String]],
|
||||
defaultLanguage: StringLiteralExprSyntax
|
||||
) -> String {
|
||||
let defaultLanguage = StringLiteral(defaultLanguage).value!.description
|
||||
var result = "func string(for language: String) -> String {\n"
|
||||
for language in getLanguages(dictionary: dictionary) where language != defaultLanguage {
|
||||
result += indent("if language.hasPrefix(\"\(language)\") {", by: indentTwo)
|
||||
result += indent("\nreturn \(language)", by: indentThree)
|
||||
result += indent("\n} else", by: indentTwo)
|
||||
}
|
||||
result += """
|
||||
{
|
||||
return \(defaultLanguage)
|
||||
}
|
||||
}
|
||||
"""
|
||||
return result
|
||||
}
|
||||
|
||||
/// Get the available languages.
|
||||
/// - Parameter dictionary: The parsed YML.
|
||||
/// - Returns: The syntax
|
||||
static func getLanguages(dictionary: [String: [String: String]]) -> [String] {
|
||||
dictionary.first?.value.map { $0.key } ?? []
|
||||
}
|
||||
|
||||
/// Parse the key for a phrase.
|
||||
/// - Parameter key: The key definition including parameters.
|
||||
/// - Returns: The key.
|
||||
static func parse(key: String) -> (String, [String]) {
|
||||
let parts = key.split(separator: "(")
|
||||
if parts.count == 1 {
|
||||
return (key, [])
|
||||
}
|
||||
let arguments = parts[1].dropLast().split(separator: ", ").map { String($0) }
|
||||
return (.init(parts[0]), arguments)
|
||||
}
|
||||
|
||||
/// Parse the translation for a phrase.
|
||||
/// - Parameters:
|
||||
/// - translation: The translation without correct escaping.
|
||||
/// - arguments: The arguments.
|
||||
/// - Returns: The syntax.
|
||||
static func parse(translation: String, arguments: [String]) -> String {
|
||||
var translation = translation
|
||||
for argument in arguments {
|
||||
translation.replace("(\(argument))", with: "\\(\(argument))")
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
||||
/// Indent each line of a text by a certain amount of whitespaces.
|
||||
/// - Parameters:
|
||||
/// - string: The text.
|
||||
/// - count: The indentation.
|
||||
/// - Returns: The syntax.
|
||||
static func indent(_ string: String, by count: Int) -> String {
|
||||
.init(
|
||||
string
|
||||
.components(separatedBy: "\n")
|
||||
.map { "\n" + Array(repeating: " ", count: count).joined() + $0 }
|
||||
.joined()
|
||||
.trimmingPrefix("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable force_unwrapping force_cast
|
||||
|
||||
43
Tests/MacroTests/Tests.swift
Normal file
43
Tests/MacroTests/Tests.swift
Normal file
@ -0,0 +1,43 @@
|
||||
//
|
||||
// Tests.swift
|
||||
// Localized
|
||||
//
|
||||
// Created by david-swift on 27.02.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Localized
|
||||
|
||||
#localized(default: "en", yml: """
|
||||
hello(name):
|
||||
en: Hello, (name)!
|
||||
de: Hallo, (name)!
|
||||
fr: Salut, (name)!
|
||||
|
||||
house:
|
||||
en: House
|
||||
de: Haus
|
||||
fr: Maison
|
||||
|
||||
helloPair(name1, name2):
|
||||
en: Hello, (name1) and (name2)!
|
||||
de: Hallo, (name1) und (name2)!
|
||||
fr: Salut, (name1) et (name2)!
|
||||
""")
|
||||
|
||||
/// Test cases for the `localized` macro.
|
||||
@main
|
||||
enum Tests {
|
||||
|
||||
/// Test the `localized` macro.
|
||||
static func main() {
|
||||
print("EN: \(Localized.hello(name: "Peter").en)")
|
||||
print("DE: \(Localized.hello(name: "Ruedi").de)")
|
||||
print("SYSTEM: \(Loc.hello(name: "Sams"))")
|
||||
print("FR: \(Localized.house.fr)")
|
||||
print("DE_CH: \(Localized.house.string(for: "de_CH"))")
|
||||
print("SYSTEM: \(Localized.house.string)")
|
||||
print("EN: \(Localized.helloPair(name1: "Max", name2: "Ruedi").en)")
|
||||
}
|
||||
|
||||
}
|
||||
16
Tests/PluginTests/Localized.yml
Normal file
16
Tests/PluginTests/Localized.yml
Normal file
@ -0,0 +1,16 @@
|
||||
default: en
|
||||
|
||||
hello(name):
|
||||
en: Hello, (name)!
|
||||
de: Hallo, (name)!
|
||||
fr: Salut, (name)!
|
||||
|
||||
house:
|
||||
en: House
|
||||
de: Haus
|
||||
fr: Maison
|
||||
|
||||
helloPair(name1, name2):
|
||||
en: Hello, (name1) and (name2)!
|
||||
de: Hallo, (name1) und (name2)!
|
||||
fr: Salut, (name1) et (name2)!
|
||||
25
Tests/PluginTests/Tests.swift
Normal file
25
Tests/PluginTests/Tests.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Tests.swift
|
||||
// Localized
|
||||
//
|
||||
// Created by david-swift on 27.02.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Test cases for the `GenerateLocalized` plugin.
|
||||
@main
|
||||
enum Tests {
|
||||
|
||||
/// Test the `GenerateLocalized` plugin.
|
||||
static func main() {
|
||||
print("EN: \(Localized.hello(name: "Peter").en)")
|
||||
print("DE: \(Localized.hello(name: "Ruedi").de)")
|
||||
print("SYSTEM: \(Loc.hello(name: "Sams"))")
|
||||
print("FR: \(Localized.house.fr)")
|
||||
print("DE_CH: \(Localized.house.string(for: "de_CH"))")
|
||||
print("SYSTEM: \(Localized.house.string)")
|
||||
print("EN: \(Localized.helloPair(name1: "Max", name2: "Ruedi").en)")
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user