diff --git a/Documentation/Generation/README.md b/Documentation/Generation/README.md
new file mode 100644
index 0000000..4885ef7
--- /dev/null
+++ b/Documentation/Generation/README.md
@@ -0,0 +1,7 @@
+# Reference Documentation
+
+## Enums
+
+- [Generation](enums/Generation.md)
+
+This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs)
\ No newline at end of file
diff --git a/Documentation/Generation/enums/Generation.md b/Documentation/Generation/enums/Generation.md
new file mode 100644
index 0000000..f083ee9
--- /dev/null
+++ b/Documentation/Generation/enums/Generation.md
@@ -0,0 +1,10 @@
+**ENUM**
+
+# `Generation`
+
+A type containing the generation function for the plugin.
+
+## Methods
+### `main()`
+
+Generate the Swift code for the plugin.
diff --git a/Documentation/GenerationLibrary/README.md b/Documentation/GenerationLibrary/README.md
new file mode 100644
index 0000000..2753a4c
--- /dev/null
+++ b/Documentation/GenerationLibrary/README.md
@@ -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)
\ No newline at end of file
diff --git a/Documentation/GenerationLibrary/enums/Generation.GenerationError.md b/Documentation/GenerationLibrary/enums/Generation.GenerationError.md
new file mode 100644
index 0000000..fc08781
--- /dev/null
+++ b/Documentation/GenerationLibrary/enums/Generation.GenerationError.md
@@ -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.
diff --git a/Documentation/GenerationLibrary/enums/Generation.md b/Documentation/GenerationLibrary/enums/Generation.md
new file mode 100644
index 0000000..1287720
--- /dev/null
+++ b/Documentation/GenerationLibrary/enums/Generation.md
@@ -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.
diff --git a/Documentation/LocalizedMacros/structs/LocalizedMacro.md b/Documentation/LocalizedMacros/structs/LocalizedMacro.md
index 51f7226..6651326 100644
--- a/Documentation/LocalizedMacros/structs/LocalizedMacro.md
+++ b/Documentation/LocalizedMacros/structs/LocalizedMacro.md
@@ -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.
diff --git a/Documentation/README.md b/Documentation/README.md
index b02961f..b287490 100644
--- a/Documentation/README.md
+++ b/Documentation/README.md
@@ -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).
diff --git a/Makefile b/Makefile
index a4b27b5..1b0232d 100644
--- a/Makefile
+++ b/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
diff --git a/Package.swift b/Package.swift
index c318e74..5c0869d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -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"
+ ]
)
]
)
diff --git a/Plugins/GenerateLocalized/Plugin.swift b/Plugins/GenerateLocalized/Plugin.swift
new file mode 100644
index 0000000..221f514
--- /dev/null
+++ b/Plugins/GenerateLocalized/Plugin.swift
@@ -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]
+ )
+ ]
+ }
+
+}
diff --git a/README.md b/README.md
index 2ae0231..ae93f45 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-_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"]
+)
+```
+
+
+
+ Use the Swift macro alternatively
+
+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.
+
+
### Usage
diff --git a/Sources/Generation/Generation.swift b/Sources/Generation/Generation.swift
new file mode 100644
index 0000000..98252c6
--- /dev/null
+++ b/Sources/Generation/Generation.swift
@@ -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)
+ )
+ }
+
+}
diff --git a/Sources/GenerationLibrary/Generation.swift b/Sources/GenerationLibrary/Generation.swift
new file mode 100644
index 0000000..d0f7777
--- /dev/null
+++ b/Sources/GenerationLibrary/Generation.swift
@@ -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 = []
+ 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")
+ )
+ }
+
+}
diff --git a/Sources/Localized/Localized.swift b/Sources/Localized/Localized.swift
index 65c0360..2ceb04b 100644
--- a/Sources/Localized/Localized.swift
+++ b/Sources/Localized/Localized.swift
@@ -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",
diff --git a/Sources/LocalizedMacros/LocalizedMacro.swift b/Sources/LocalizedMacros/LocalizedMacro.swift
index c20d4b0..941dbaf 100644
--- a/Sources/LocalizedMacros/LocalizedMacro.swift
+++ b/Sources/LocalizedMacros/LocalizedMacro.swift
@@ -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
diff --git a/Tests/MacroTests/Tests.swift b/Tests/MacroTests/Tests.swift
new file mode 100644
index 0000000..d0e6a37
--- /dev/null
+++ b/Tests/MacroTests/Tests.swift
@@ -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)")
+ }
+
+}
diff --git a/Tests/PluginTests/Localized.yml b/Tests/PluginTests/Localized.yml
new file mode 100644
index 0000000..0a489b4
--- /dev/null
+++ b/Tests/PluginTests/Localized.yml
@@ -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)!
\ No newline at end of file
diff --git a/Tests/PluginTests/Tests.swift b/Tests/PluginTests/Tests.swift
new file mode 100644
index 0000000..9eafff2
--- /dev/null
+++ b/Tests/PluginTests/Tests.swift
@@ -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)")
+ }
+
+}