Initial commit

This commit is contained in:
david-swift 2024-02-29 14:09:15 +01:00
commit 475e30e784
26 changed files with 1061 additions and 0 deletions

40
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Bug report
description: Something is not working as expected.
title: Description of the bug
labels: bug
body:
- type: textarea
attributes:
label: Describe the bug
description: >-
A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: >-
Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: >-
A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: >-
Add any other context about the problem here.

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: Description of the feature request
labels: enhancement
body:
- type: input
attributes:
label: Is your feature request related to a problem? Please describe.
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
placeholder: >-
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
placeholder: >-
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context or screenshots about the feature request here.
validations:
required: true

14
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,14 @@
## Steps
- [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file.
- [ ] Build the project on your machine. If it does not compile, fix the errors.
- [ ] Describe the purpose and approach of your pull request below.
- [ ] Submit the pull request. Thank you very much for your contribution!
## Purpose
_Describe the problem or feature._
_If there is a related issue, add the link._
## Approach
_Describe how this pull request solves the problem or adds the feature._
[1]: /Contributors.md

30
.github/workflows/swiftlint.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: SwiftLint
on:
push:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
env:
WORKING_DIRECTORY: Source

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/Package.resolved
.Ulysses-Group.plist

162
.swiftlint.yml Normal file
View File

@ -0,0 +1,162 @@
# Opt-In Rules
opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- comma_inheritance
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
- discouraged_optional_collection
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- explicit_init
- fallthrough
- file_header
- file_name
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- let_var_whitespace
- literal_expression_end_indentation
- local_doc_comment
- lower_acl_than_parent
- missing_docs
- modifier_order
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- no_extension_access_modifier
- no_grouping_extension
- no_magic_numbers
- number_separator
- operator_usage_whitespace
- optional_enum_case_matching
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prohibited_interface_builder
- redundant_nil_coalescing
- redundant_type_annotation
- return_value_from_void_function
- shorthand_optional_binding
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
- switch_case_on_newline
- toggle_bool
- trailing_closure
- type_contents_order
- unneeded_parentheses_in_closure_argument
- yoda_condition
# Disabled Rules
disabled_rules:
- block_based_kvo
- class_delegate_protocol
- dynamic_inline
- is_disjoint
- no_fallthrough_only
- notification_center_detachment
- ns_number_init_as_function_reference
- nsobject_prefer_isequal
- private_over_fileprivate
- redundant_objc_attribute
- self_in_property_initialization
- todo
- unavailable_condition
- valid_ibinspectable
- xctfail_message
# Custom Rules
custom_rules:
github_issue:
name: 'GitHub Issue'
regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/AparokshaUI/Localized/issues/\d))'
message: 'The related GitHub issue must be included in a TODO or FIXME.'
severity: warning
fatal_error:
name: 'Fatal Error'
regex: 'fatalError.*\(.*\)'
message: 'Fatal error should not be used.'
severity: error
enum_case_parameter:
name: 'Enum Case Parameter'
regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)'
message: 'The associated values of an enum case should have parameters.'
severity: warning
tab:
name: 'Whitespaces Instead of Tab'
regex: '\t'
message: 'Spaces should be used instead of tabs.'
severity: warning
# Thanks to the creator of the SwiftLint rule
# "empty_first_line"
# https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml
# in the GitHub repository
# "CotEditor"
# https://github.com/coteditor/CotEditor
empty_first_line:
name: 'Empty First Line'
regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)'
message: 'There should be an empty line after a declaration'
severity: error
# Analyzer Rules
analyzer_rules:
- unused_declaration
- unused_import
# Options
file_header:
required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// Localized\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n'
missing_docs:
warning: [internal, private]
error: [open, public]
excludes_extensions: false
excludes_inherited_types: false
type_contents_order:
order:
- case
- type_alias
- associated_type
- type_property
- instance_property
- ib_inspectable
- ib_outlet
- subscript
- initializer
- deinitializer
- subtype
- type_method
- view_life_cycle_method
- ib_action
- other_method

35
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,35 @@
# Contributing
Thank you very much for taking the time for contributing to this project.
## Report a Bug
Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
## Suggest a New Feature
Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
## Pull Requests
I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
### 1. Fork & Clone this Project
Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
### 2. Open the Project
Open the project folder in GNOME Builder, Xcode or another IDE.
### 3. Understand the Project Structure
- The `README.md` file contains a description of the app or package.
- The `Contributors.md` file contains the names or usernames of all the contributors with a link to their GitHub profile.
- The `LICENSE.md` contains a GPL-3.0 license.
- `CONTRIBUTING.md` is this file.
- Directory `Icons` that contains the icons.
- `Sources` contains the source code of the project.
### 4. Edit the Code
Edit the code. If you add a new type, add documentation in the code.
### 5. Commit to the Fork
Commit and push the fork.
### 6. Pull Request
Open GitHub to submit a pull request. Thank you very much for your contribution!

3
Contributors.md Normal file
View File

@ -0,0 +1,3 @@
# Contributors
- [david-swift](https://github.com/david-swift)

View File

@ -0,0 +1,7 @@
# Reference Documentation
## Enums
- [System](enums/System.md)
This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs)

View File

@ -0,0 +1,16 @@
**ENUM**
# `System`
The type system contains a function for parsing the system language.
## Properties
### `systemLanguage`
Remembers the system language after the first request.
## Methods
### `getLanguage()`
Get the system language.
- Returns: The system language.

View File

@ -0,0 +1,12 @@
# Reference Documentation
## Structs
- [LocalizedMacro](structs/LocalizedMacro.md)
- [LocalizedPlugin](structs/LocalizedPlugin.md)
## Enums
- [LocalizedMacro.LocalizedError](enums/LocalizedMacro.LocalizedError.md)
This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs)

View File

@ -0,0 +1,14 @@
**ENUM**
# `LocalizedMacro.LocalizedError`
The errors the expansion can throw.
## Cases
### `invalidStringLiteral`
The string literal syntax is invalid.
### `invalidDefaultLanguage`
The default language syntax is invalid.

View File

@ -0,0 +1,85 @@
**STRUCT**
# `LocalizedMacro`
Implementation of the `localized` macro, which takes YML
as a string and converts it into two enumerations.
Access a specific language using `Localized.key.language`, or use `Localized.key.string`
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:)`
Expand the `localized` macro.
- Parameters:
- 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.

View File

@ -0,0 +1,10 @@
**STRUCT**
# `LocalizedPlugin`
The compiler plugin offering the `localized` macro.
## Properties
### `providingMacros`
The macros.

9
Documentation/README.md Normal file
View File

@ -0,0 +1,9 @@
# Reference Documentation
## LocalizedMacros
Find documentation for the `localized` macro [here](LocalizedMacros/README.md).
## Localized
Find documentation for the `System` enumeration [here](Localized/README.md).

BIN
Icons/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
Icons/Icon.pxd Normal file

Binary file not shown.

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 david-swift
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
Makefile Normal file
View File

@ -0,0 +1,6 @@
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
swiftlint:
@swiftlint --autocorrect

51
Package.swift Normal file
View File

@ -0,0 +1,51 @@
// swift-tools-version: 5.9
//
// Package.swift
// Localized
//
// Created by david-swift on 27.02.24.
//
import CompilerPluginSupport
import PackageDescription
/// The Localized package.
let package = Package(
name: "Localized",
platforms: [.macOS(.v13)],
products: [
.library(
name: "Localized",
targets: ["Localized"]
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"),
.package(url: "https://github.com/jpsim/Yams", from: "5.0.6"),
.package(url: "https://github.com/stackotter/swift-macro-toolkit", from: "0.3.1")
],
targets: [
.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")
]
),
.target(
name: "Localized",
dependencies: [
"LocalizedMacros"
]
),
.executableTarget(
name: "LocalizedTests",
dependencies: [
"Localized"
],
path: "Tests"
)
]
)

127
README.md Normal file
View File

@ -0,0 +1,127 @@
<p align="center">
<img width="256" alt="Localized Icon" src="Icons/Icon.png">
<h1 align="center">Localized</h1>
</p>
<p align="center">
<a href="https://github.com/AparokshaUI/Localized">
GitHub
</a>
·
<a href="Documentation/README.md">
Contributor Docs
</a>
</p>
_Localized_ provides a macro for localizing cross-platform Swift code.
Use YML syntax for defining available phrases:
```yml
hello(name):
en: Hello, (name)!
de: Hallo, (name)!
fr: Salut, (name)!
house:
en: House
de: Haus
fr: Maison
```
Then, access the localized strings safely in your code:
```swift
// Use the system language
print(Loc.hello(name: "Peter"))
print(Loc.house)
// Access the translation for a specific language
print(Localized.hello(name: "Peter").en)
print(Localized.house.fr)
```
## Table of Contents
- [Installation][4]
- [Usage][5]
- [Thanks][6]
## Installation
1. Open your Swift package in GNOME Builder, Xcode, or any other IDE.
2. Open the `Package.swift` file.
3. Into the `Package` initializer, under `dependencies`, paste:
```swift
.package(url: "https://github.com/AparokshaUI/Localized", from: "0.1.0")
```
## Usage
### Definition
Define the available phrases using YML.
```swift
#localized(default: "en", yml: """
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.
### Usage
In most cases, you want to get the translated string in the system language.
This can be accomplished using the following syntax.
```swift
let export = Loc.export
let send = Loc.send(message: "Hello", name: "Peter")
```
You can access a specific language as well.
```swift
let export = Localized.export.en
let send = Localized.send(message: "Hallo", name: "Peter").de
```
If you want to get the translation for a specific language code, use the following syntax.
This function will return the translation for the default language if there's no translation for the prefix of that code available.
```swift
let export = Localized.export.string(for: "de-CH")
```
## Thanks
### Dependencies
- [Swift Syntax](https://github.com/apple/swift-syntax) licensed under the [Apache 2.0 license](https://github.com/apple/swift-syntax/blob/main/LICENSE.txt)
- [Yams](https://github.com/jpsim/Yams) licensed under the [MIT license](https://github.com/jpsim/Yams/blob/main/LICENSE)
- [Swift Macro Toolkit](https://github.com/stackotter/swift-macro-toolkit) licensed under the [Apache 2.0 license](https://github.com/stackotter/swift-macro-toolkit/blob/main/LICENSE)
### Other Thanks
- The [contributors][7]
- [SwiftLint][8] for checking whether code style conventions are violated
- The programming language [Swift][9]
- [SourceDocs][10] used for generating the [docs][11]
[1]: Tests/
[2]: #goals
[3]: #widgets
[4]: #installation
[5]: #usage
[6]: #thanks
[7]: Contributors.md
[8]: https://github.com/realm/SwiftLint
[9]: https://github.com/apple/swift
[10]: https://github.com/SourceDocs/SourceDocs
[11]: Documentation/README.md

View File

@ -0,0 +1,18 @@
//
// Localized.swift
// Localized
//
// 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")`.
@freestanding(declaration, names: named(Localized), named(Loc))
public macro localized(default defaultLanguage: String, yml: String) = #externalMacro(
module: "LocalizedMacros",
type: "LocalizedMacro"
)

View File

@ -0,0 +1,48 @@
//
// System.swift
// Localized
//
// Created by david-swift on 28.02.2024.
//
import Foundation
/// The type system contains a function for parsing the system language.
public enum System {
/// Remembers the system language after the first request.
static var systemLanguage: String?
/// Get the system language.
/// - Returns: The system language.
public static func getLanguage() -> String {
if systemLanguage == nil {
#if os(Linux)
guard let lang = ProcessInfo.processInfo.environment["LANG"] else {
return "en"
}
let components = lang.split(separator: "_")
systemLanguage = .init(components.first ?? "en")
#endif
#if os(macOS)
systemLanguage = Locale.preferredLanguages.first
#endif
#if os(Windows)
let process = Process()
process.executableURL = .init(
fileURLWithPath: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
)
process.arguments = ["-Command", "[System.Globalization.CultureInfo]::CurrentUICulture.Name"]
let pipe = Pipe()
process.standardOutput = pipe
try? process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
systemLanguage = .init(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
#endif
}
return systemLanguage ?? "en"
}
}

View File

@ -0,0 +1,243 @@
//
// LocalizedMacro.swift
// Localized
//
// Created by david-swift on 27.02.2024.
//
// swiftlint:disable force_unwrapping force_cast
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.
/// Access a specific language using `Localized.key.language`, or use `Localized.key.string`
/// which automatically uses the system language on Linux, macOS and Windows.
/// 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 {
/// The string literal syntax is invalid.
case invalidStringLiteral
/// The default language syntax is invalid.
case invalidDefaultLanguage
}
/// Expand the `localized` macro.
/// - Parameters:
/// - node: Information about the macro call.
/// - context: The expansion context.
/// - Returns: The enumerations `Localized` and `Loc`.
public static func expansion(
of node: some SwiftSyntax.FreestandingMacroExpansionSyntax,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.DeclSyntax] {
guard let `default` = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self) else {
throw LocalizedError.invalidDefaultLanguage
}
guard let syntax = node.argumentList.last?.expression.as(StringLiteralExprSyntax.self) 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))
}
"""
]
}
/// 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

View File

@ -0,0 +1,20 @@
//
// LocalizedPlugin.swift
// Localized
//
// Created by david-swift on 27.02.2024.
//
import SwiftCompilerPlugin
import SwiftSyntaxMacros
/// The compiler plugin offering the `localized` macro.
@main
struct LocalizedPlugin: CompilerPlugin {
/// The macros.
let providingMacros: [Macro.Type] = [
LocalizedMacro.self
]
}

43
Tests/Tests.swift Normal file
View 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)")
}
}