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.
|
which automatically uses the system language on Linux, macOS and Windows.
|
||||||
Use `Loc.key` for a quick access to the automatically localized value.
|
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
|
## Methods
|
||||||
### `expansion(of:in:)`
|
### `expansion(of:in:)`
|
||||||
|
|
||||||
@ -29,57 +16,3 @@ Expand the `localized` macro.
|
|||||||
- node: Information about the macro call.
|
- node: Information about the macro call.
|
||||||
- context: The expansion context.
|
- context: The expansion context.
|
||||||
- Returns: The enumerations `Localized` and `Loc`.
|
- 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
|
## Localized
|
||||||
|
|
||||||
Find documentation for the `System` enumeration [here](Localized/README.md).
|
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:
|
docs:
|
||||||
@sourcedocs generate --min-acl private -r --spm-module Localized --output-folder Documentation/Localized
|
@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 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:
|
||||||
@swiftlint --autocorrect
|
@swiftlint --autocorrect
|
||||||
|
|||||||
@ -17,6 +17,10 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "Localized",
|
name: "Localized",
|
||||||
targets: ["Localized"]
|
targets: ["Localized"]
|
||||||
|
),
|
||||||
|
.plugin(
|
||||||
|
name: "GenerateLocalized",
|
||||||
|
targets: ["GenerateLocalized"]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@ -25,13 +29,32 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/stackotter/swift-macro-toolkit", from: "0.3.1")
|
.package(url: "https://github.com/stackotter/swift-macro-toolkit", from: "0.3.1")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "GenerationLibrary",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Yams", package: "Yams")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.executableTarget(
|
||||||
|
name: "Generation",
|
||||||
|
dependencies: [
|
||||||
|
"GenerationLibrary"
|
||||||
|
]
|
||||||
|
),
|
||||||
.macro(
|
.macro(
|
||||||
name: "LocalizedMacros",
|
name: "LocalizedMacros",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||||
.product(name: "MacroToolkit", package: "swift-macro-toolkit"),
|
.product(name: "MacroToolkit", package: "swift-macro-toolkit"),
|
||||||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
||||||
.product(name: "Yams", package: "Yams")
|
"GenerationLibrary"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.plugin(
|
||||||
|
name: "GenerateLocalized",
|
||||||
|
capability: .buildTool(),
|
||||||
|
dependencies: [
|
||||||
|
"Generation"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
@ -41,11 +64,24 @@ let package = Package(
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "LocalizedTests",
|
name: "MacroTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Localized"
|
"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>
|
</a>
|
||||||
</p>
|
</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:
|
Use YML syntax for defining available phrases:
|
||||||
|
|
||||||
@ -60,7 +60,44 @@ print(Localized.house.fr)
|
|||||||
|
|
||||||
### Definition
|
### 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
|
```swift
|
||||||
#localized(default: "en", yml: """
|
#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
|
### 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.
|
// Created by david-swift on 27.02.2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
/// A macro that produces both a value and a string containing the
|
/// A macro that takes the YML syntax and converts it into enumerations.
|
||||||
/// source code that generated the value. For example,
|
|
||||||
///
|
|
||||||
/// #stringify(x + y)
|
|
||||||
///
|
|
||||||
/// produces a tuple `(x + y, "x + y")`.
|
|
||||||
@freestanding(declaration, names: named(Localized), named(Loc))
|
@freestanding(declaration, names: named(Localized), named(Loc))
|
||||||
public macro localized(default defaultLanguage: String, yml: String) = #externalMacro(
|
public macro localized(default defaultLanguage: String, yml: String) = #externalMacro(
|
||||||
module: "LocalizedMacros",
|
module: "LocalizedMacros",
|
||||||
|
|||||||
@ -5,12 +5,10 @@
|
|||||||
// Created by david-swift on 27.02.2024.
|
// Created by david-swift on 27.02.2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
// swiftlint:disable force_unwrapping force_cast
|
import GenerationLibrary
|
||||||
|
|
||||||
import MacroToolkit
|
import MacroToolkit
|
||||||
import SwiftSyntax
|
import SwiftSyntax
|
||||||
import SwiftSyntaxMacros
|
import SwiftSyntaxMacros
|
||||||
import Yams
|
|
||||||
|
|
||||||
/// Implementation of the `localized` macro, which takes YML
|
/// Implementation of the `localized` macro, which takes YML
|
||||||
/// as a string and converts it into two enumerations.
|
/// 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.
|
/// Use `Loc.key` for a quick access to the automatically localized value.
|
||||||
public struct LocalizedMacro: DeclarationMacro {
|
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.
|
/// The errors the expansion can throw.
|
||||||
public enum LocalizedError: Error {
|
public enum LocalizedError: Error {
|
||||||
|
|
||||||
@ -45,199 +36,16 @@ public struct LocalizedMacro: DeclarationMacro {
|
|||||||
of node: some SwiftSyntax.FreestandingMacroExpansionSyntax,
|
of node: some SwiftSyntax.FreestandingMacroExpansionSyntax,
|
||||||
in context: some SwiftSyntaxMacros.MacroExpansionContext
|
in context: some SwiftSyntaxMacros.MacroExpansionContext
|
||||||
) throws -> [SwiftSyntax.DeclSyntax] {
|
) 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
|
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
|
throw LocalizedError.invalidStringLiteral
|
||||||
}
|
}
|
||||||
let dictionary = try Yams.load(yaml: StringLiteral(syntax).value!) as! [String: [String: String]]
|
yml.append("\n\ndefault: \"\(defaultLanguage)\"")
|
||||||
return [
|
return try Generation.getCode(yml: yml).map { "\(raw: $0)" }
|
||||||
"""
|
|
||||||
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))
|
|
||||||
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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