├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── swiftlint.yml ├── .gitignore ├── .swiftlint.yml ├── CONTRIBUTING.md ├── Contributors.md ├── Documentation ├── Generation │ ├── README.md │ └── enums │ │ └── Generation.md ├── GenerationLibrary │ ├── README.md │ └── enums │ │ ├── Generation.GenerationError.md │ │ └── Generation.md ├── Localized │ ├── README.md │ └── enums │ │ └── System.md ├── LocalizedMacros │ ├── README.md │ ├── enums │ │ └── LocalizedMacro.LocalizedError.md │ └── structs │ │ ├── LocalizedMacro.md │ │ └── LocalizedPlugin.md └── README.md ├── Icons ├── Icon.png └── Icon.pxd ├── LICENSE.md ├── Makefile ├── Package.swift ├── Plugins └── GenerateLocalized │ └── Plugin.swift ├── README.md ├── Sources ├── Generation │ └── Generation.swift ├── GenerationLibrary │ └── Generation.swift └── Localized │ └── System.swift └── Tests └── PluginTests ├── Localized.yml └── Tests.swift /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Something is not working as expected. 3 | title: Description of the bug 4 | labels: bug 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the bug 10 | description: >- 11 | A clear and concise description of what the bug is. 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: To Reproduce 18 | description: >- 19 | Steps to reproduce the behavior. 20 | placeholder: | 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Expected behavior 31 | description: >- 32 | A clear and concise description of what you expected to happen. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Additional context 39 | description: >- 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: Description of the feature request 4 | labels: enhancement 5 | 6 | body: 7 | - type: input 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | validations: 12 | required: false 13 | 14 | - type: textarea 15 | attributes: 16 | label: Describe the solution you'd like 17 | placeholder: >- 18 | A clear and concise description of what you want to happen. 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered 25 | placeholder: >- 26 | A clear and concise description of any alternative solutions or features you've considered. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | attributes: 32 | label: Additional context 33 | placeholder: >- 34 | Add any other context or screenshots about the feature request here. 35 | validations: 36 | required: true 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps 2 | - [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file. 3 | - [ ] Build the project on your machine. If it does not compile, fix the errors. 4 | - [ ] Describe the purpose and approach of your pull request below. 5 | - [ ] Submit the pull request. Thank you very much for your contribution! 6 | 7 | ## Purpose 8 | _Describe the problem or feature._ 9 | _If there is a related issue, add the link._ 10 | 11 | ## Approach 12 | _Describe how this pull request solves the problem or adds the feature._ 13 | 14 | [1]: /Contributors.md 15 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | pull_request: 10 | paths: 11 | - '.github/workflows/swiftlint.yml' 12 | - '.swiftlint.yml' 13 | - '**/*.swift' 14 | workflow_dispatch: 15 | paths: 16 | - '.github/workflows/swiftlint.yml' 17 | - '.swiftlint.yml' 18 | - '**/*.swift' 19 | 20 | jobs: 21 | SwiftLint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: SwiftLint 26 | uses: norio-nomura/action-swiftlint@3.2.1 27 | with: 28 | args: --strict 29 | env: 30 | WORKING_DIRECTORY: Source 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | /Package.resolved 11 | .Ulysses-Group.plist 12 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Opt-In Rules 2 | opt_in_rules: 3 | - anonymous_argument_in_multiline_closure 4 | - array_init 5 | - attributes 6 | - closure_body_length 7 | - closure_end_indentation 8 | - closure_spacing 9 | - collection_alignment 10 | - comma_inheritance 11 | - conditional_returns_on_newline 12 | - contains_over_filter_count 13 | - contains_over_filter_is_empty 14 | - contains_over_first_not_nil 15 | - contains_over_range_nil_comparison 16 | - convenience_type 17 | - discouraged_none_name 18 | - discouraged_object_literal 19 | - discouraged_optional_boolean 20 | - discouraged_optional_collection 21 | - empty_collection_literal 22 | - empty_count 23 | - empty_string 24 | - enum_case_associated_values_count 25 | - explicit_init 26 | - fallthrough 27 | - file_header 28 | - file_name 29 | - file_name_no_space 30 | - first_where 31 | - flatmap_over_map_reduce 32 | - force_unwrapping 33 | - function_default_parameter_at_end 34 | - identical_operands 35 | - implicit_return 36 | - implicitly_unwrapped_optional 37 | - joined_default_parameter 38 | - last_where 39 | - legacy_multiple 40 | - let_var_whitespace 41 | - literal_expression_end_indentation 42 | - local_doc_comment 43 | - lower_acl_than_parent 44 | - missing_docs 45 | - modifier_order 46 | - multiline_arguments 47 | - multiline_arguments_brackets 48 | - multiline_function_chains 49 | - multiline_literal_brackets 50 | - multiline_parameters 51 | - multiline_parameters_brackets 52 | - no_extension_access_modifier 53 | - no_grouping_extension 54 | - no_magic_numbers 55 | - number_separator 56 | - operator_usage_whitespace 57 | - optional_enum_case_matching 58 | - prefer_self_in_static_references 59 | - prefer_self_type_over_type_of_self 60 | - prefer_zero_over_explicit_init 61 | - prohibited_interface_builder 62 | - redundant_nil_coalescing 63 | - redundant_type_annotation 64 | - return_value_from_void_function 65 | - shorthand_optional_binding 66 | - sorted_first_last 67 | - sorted_imports 68 | - static_operator 69 | - strict_fileprivate 70 | - switch_case_on_newline 71 | - toggle_bool 72 | - trailing_closure 73 | - type_contents_order 74 | - unneeded_parentheses_in_closure_argument 75 | - yoda_condition 76 | 77 | # Disabled Rules 78 | disabled_rules: 79 | - block_based_kvo 80 | - class_delegate_protocol 81 | - dynamic_inline 82 | - is_disjoint 83 | - no_fallthrough_only 84 | - notification_center_detachment 85 | - ns_number_init_as_function_reference 86 | - nsobject_prefer_isequal 87 | - private_over_fileprivate 88 | - redundant_objc_attribute 89 | - self_in_property_initialization 90 | - todo 91 | - unavailable_condition 92 | - valid_ibinspectable 93 | - xctfail_message 94 | 95 | # Custom Rules 96 | custom_rules: 97 | github_issue: 98 | name: 'GitHub Issue' 99 | regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/AparokshaUI/Localized/issues/\d))' 100 | message: 'The related GitHub issue must be included in a TODO or FIXME.' 101 | severity: warning 102 | 103 | fatal_error: 104 | name: 'Fatal Error' 105 | regex: 'fatalError.*\(.*\)' 106 | message: 'Fatal error should not be used.' 107 | severity: error 108 | 109 | enum_case_parameter: 110 | name: 'Enum Case Parameter' 111 | regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)' 112 | message: 'The associated values of an enum case should have parameters.' 113 | severity: warning 114 | 115 | tab: 116 | name: 'Whitespaces Instead of Tab' 117 | regex: '\t' 118 | message: 'Spaces should be used instead of tabs.' 119 | severity: warning 120 | 121 | # Thanks to the creator of the SwiftLint rule 122 | # "empty_first_line" 123 | # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml 124 | # in the GitHub repository 125 | # "CotEditor" 126 | # https://github.com/coteditor/CotEditor 127 | empty_first_line: 128 | name: 'Empty First Line' 129 | regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)' 130 | message: 'There should be an empty line after a declaration' 131 | severity: error 132 | 133 | # Analyzer Rules 134 | analyzer_rules: 135 | - unused_declaration 136 | - unused_import 137 | 138 | # Options 139 | file_header: 140 | 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' 141 | missing_docs: 142 | warning: [internal, private] 143 | error: [open, public] 144 | excludes_extensions: false 145 | excludes_inherited_types: false 146 | type_contents_order: 147 | order: 148 | - case 149 | - type_alias 150 | - associated_type 151 | - type_property 152 | - instance_property 153 | - ib_inspectable 154 | - ib_outlet 155 | - subscript 156 | - initializer 157 | - deinitializer 158 | - subtype 159 | - type_method 160 | - view_life_cycle_method 161 | - ib_action 162 | - other_method 163 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you very much for taking the time for contributing to this project. 4 | 5 | ## Report a Bug 6 | 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! 7 | 8 | ## Suggest a New Feature 9 | Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution! 10 | 11 | ## Pull Requests 12 | 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! 13 | 14 | ### 1. Fork & Clone this Project 15 | Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer. 16 | 17 | ### 2. Open the Project 18 | Open the project folder in GNOME Builder, Xcode or another IDE. 19 | 20 | ### 3. Understand the Project Structure 21 | - The `README.md` file contains a description of the app or package. 22 | - The `Contributors.md` file contains the names or usernames of all the contributors with a link to their GitHub profile. 23 | - The `LICENSE.md` contains a GPL-3.0 license. 24 | - `CONTRIBUTING.md` is this file. 25 | - Directory `Icons` that contains the icons. 26 | - `Sources` contains the source code of the project. 27 | 28 | ### 4. Edit the Code 29 | Edit the code. If you add a new type, add documentation in the code. 30 | 31 | ### 5. Commit to the Fork 32 | Commit and push the fork. 33 | 34 | ### 6. Pull Request 35 | Open GitHub to submit a pull request. Thank you very much for your contribution! 36 | -------------------------------------------------------------------------------- /Contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - [david-swift](https://github.com/david-swift) 4 | -------------------------------------------------------------------------------- /Documentation/Generation/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Enums 4 | 5 | - [Generation](enums/Generation.md) 6 | 7 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) -------------------------------------------------------------------------------- /Documentation/Generation/enums/Generation.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `Generation` 4 | 5 | A type containing the generation function for the plugin. 6 | 7 | ## Methods 8 | ### `main()` 9 | 10 | Generate the Swift code for the plugin. 11 | -------------------------------------------------------------------------------- /Documentation/GenerationLibrary/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Enums 4 | 5 | - [Generation](enums/Generation.md) 6 | - [Generation.GenerationError](enums/Generation.GenerationError.md) 7 | 8 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) -------------------------------------------------------------------------------- /Documentation/GenerationLibrary/enums/Generation.GenerationError.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `Generation.GenerationError` 4 | 5 | An error that occurs during code generation. 6 | 7 | ## Cases 8 | ### `missingTranslationInDefaultLanguage(key:)` 9 | 10 | A translation in the default language missing for a specific key. 11 | Missing translations in other languages will cause the default language to be used. 12 | 13 | ### `unknownYMLPasingError` 14 | 15 | An unknown error occured while parsing the YML. 16 | 17 | ### `missingDefaultLanguage` 18 | 19 | The default language information is missing. 20 | -------------------------------------------------------------------------------- /Documentation/GenerationLibrary/enums/Generation.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `Generation` 4 | 5 | Generate the Swift code for the plugin and macro. 6 | 7 | ## Properties 8 | ### `indentOne` 9 | 10 | Number of spaces for indentation 1. 11 | 12 | ### `indentTwo` 13 | 14 | Number of spaces for indentation 2. 15 | 16 | ### `indentThree` 17 | 18 | Number of spaces for indentation 3. 19 | 20 | ## Methods 21 | ### `getCode(yml:)` 22 | 23 | Get the Swift code for the plugin and macro. 24 | - Parameter yml: The YML code. 25 | - Returns: The code. 26 | 27 | ### `generateEnumCases(dictionary:)` 28 | 29 | Generate the cases for the `Localized` enumeration. 30 | - Parameter dictionary: The parsed YML. 31 | - Returns: The syntax. 32 | 33 | ### `generateStaticLocVariables(dictionary:)` 34 | 35 | Generate the static variables and functions for the `Loc` type. 36 | - Parameter dictionary: The parsed YML. 37 | - Returns: The syntax. 38 | 39 | ### `generateTranslations(dictionary:defaultLanguage:)` 40 | 41 | Generate the variables for the translations. 42 | - Parameters: 43 | - dictionary: The parsed YML. 44 | - defaultLanguage: The default language. 45 | - Returns: The syntax. 46 | 47 | ### `parseValue(defaultTranslation:translations:language:arguments:)` 48 | 49 | Parse the content of a switch case. 50 | - Parameters: 51 | - defaultTranslation: The translation without any conditions (always required). 52 | - translations: All the available translations for an entry. 53 | - language: The language. 54 | - arguments: The arguments of the entry. 55 | - Returns: The syntax. 56 | 57 | ### `generateLanguageFunction(dictionary:defaultLanguage:)` 58 | 59 | Generate the function for getting the translated string for a specified language code. 60 | - Parameters: 61 | - dictionary: The parsed YML. 62 | - defaultLanguage: The default language. 63 | - Returns: The syntax. 64 | 65 | ### `getLanguages(dictionary:)` 66 | 67 | Get the available languages. 68 | - Parameter dictionary: The parsed YML. 69 | - Returns: The syntax 70 | 71 | ### `parse(key:)` 72 | 73 | Parse the key for a phrase. 74 | - Parameter key: The key definition including parameters. 75 | - Returns: The key. 76 | 77 | ### `parse(translation:arguments:)` 78 | 79 | Parse the translation for a phrase. 80 | - Parameters: 81 | - translation: The translation without correct escaping. 82 | - arguments: The arguments. 83 | - Returns: The syntax. 84 | 85 | ### `indent(_:by:)` 86 | 87 | Indent each line of a text by a certain amount of whitespaces. 88 | - Parameters: 89 | - string: The text. 90 | - count: The indentation. 91 | - Returns: The syntax. 92 | -------------------------------------------------------------------------------- /Documentation/Localized/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Enums 4 | 5 | - [System](enums/System.md) 6 | 7 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) -------------------------------------------------------------------------------- /Documentation/Localized/enums/System.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `System` 4 | 5 | The type system contains a function for parsing the system language. 6 | 7 | ## Properties 8 | ### `systemLanguage` 9 | 10 | Remembers the system language after the first request. 11 | 12 | ## Methods 13 | ### `getLanguage()` 14 | 15 | Get the system language. 16 | - Returns: The system language. 17 | -------------------------------------------------------------------------------- /Documentation/LocalizedMacros/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Structs 4 | 5 | - [LocalizedMacro](structs/LocalizedMacro.md) 6 | - [LocalizedPlugin](structs/LocalizedPlugin.md) 7 | 8 | ## Enums 9 | 10 | - [LocalizedMacro.LocalizedError](enums/LocalizedMacro.LocalizedError.md) 11 | 12 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) -------------------------------------------------------------------------------- /Documentation/LocalizedMacros/enums/LocalizedMacro.LocalizedError.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `LocalizedMacro.LocalizedError` 4 | 5 | The errors the expansion can throw. 6 | 7 | ## Cases 8 | ### `invalidStringLiteral` 9 | 10 | The string literal syntax is invalid. 11 | 12 | ### `invalidDefaultLanguage` 13 | 14 | The default language syntax is invalid. 15 | -------------------------------------------------------------------------------- /Documentation/LocalizedMacros/structs/LocalizedMacro.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `LocalizedMacro` 4 | 5 | Implementation of the `localized` macro, which takes YML 6 | as a string and converts it into two enumerations. 7 | Access a specific language using `Localized.key.language`, or use `Localized.key.string` 8 | which automatically uses the system language on Linux, macOS and Windows. 9 | Use `Loc.key` for a quick access to the automatically localized value. 10 | 11 | ## Methods 12 | ### `expansion(of:in:)` 13 | 14 | Expand the `localized` macro. 15 | - Parameters: 16 | - node: Information about the macro call. 17 | - context: The expansion context. 18 | - Returns: The enumerations `Localized` and `Loc`. 19 | -------------------------------------------------------------------------------- /Documentation/LocalizedMacros/structs/LocalizedPlugin.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `LocalizedPlugin` 4 | 5 | The compiler plugin offering the `localized` macro. 6 | 7 | ## Properties 8 | ### `providingMacros` 9 | 10 | The macros. 11 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## LocalizedMacros 4 | 5 | Find documentation for the `localized` macro [here](LocalizedMacros/README.md). 6 | 7 | ## Localized 8 | 9 | Find documentation for the `System` enumeration [here](Localized/README.md). 10 | 11 | ## Generation 12 | 13 | Find documentation for the generation executable used by the plugin [here](Generation/README.md). 14 | 15 | ## GenerationLibrary 16 | 17 | Find documentation for the generation used by the plugin and macro [here](GenerationLibrary/README.md). 18 | -------------------------------------------------------------------------------- /Icons/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AparokshaUI/Localized/f201dfa38ad0c9d3ab759de44a5aff0b910838cc/Icons/Icon.png -------------------------------------------------------------------------------- /Icons/Icon.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AparokshaUI/Localized/f201dfa38ad0c9d3ab759de44a5aff0b910838cc/Icons/Icon.pxd -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 david-swift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docs: 2 | @sourcedocs generate --min-acl private -r --spm-module Localized --output-folder Documentation/Localized 3 | @sourcedocs generate --min-acl private -r --spm-module LocalizedMacros --output-folder Documentation/LocalizedMacros 4 | @sourcedocs generate --min-acl private -r --spm-module Generation --output-folder Documentation/Generation 5 | @sourcedocs generate --min-acl private -r --spm-module GenerationLibrary --output-folder Documentation/GenerationLibrary 6 | 7 | swiftlint: 8 | @swiftlint --autocorrect 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // 3 | // Package.swift 4 | // Localized 5 | // 6 | // Created by david-swift on 27.02.24. 7 | // 8 | 9 | import CompilerPluginSupport 10 | import PackageDescription 11 | 12 | /// The Localized package. 13 | let package = Package( 14 | name: "Localized", 15 | platforms: [.macOS(.v13)], 16 | products: [ 17 | .library( 18 | name: "Localized", 19 | targets: ["Localized"] 20 | ), 21 | .plugin( 22 | name: "GenerateLocalized", 23 | targets: ["GenerateLocalized"] 24 | ) 25 | ], 26 | dependencies: [ 27 | .package(url: "https://github.com/jpsim/Yams", from: "5.0.6") 28 | ], 29 | targets: [ 30 | .target( 31 | name: "GenerationLibrary", 32 | dependencies: [ 33 | .product(name: "Yams", package: "Yams") 34 | ] 35 | ), 36 | .executableTarget( 37 | name: "Generation", 38 | dependencies: [ 39 | "GenerationLibrary" 40 | ] 41 | ), 42 | .plugin( 43 | name: "GenerateLocalized", 44 | capability: .buildTool(), 45 | dependencies: [ 46 | "Generation" 47 | ] 48 | ), 49 | .target( 50 | name: "Localized" 51 | ), 52 | .executableTarget( 53 | name: "PluginTests", 54 | dependencies: [ 55 | "Localized" 56 | ], 57 | path: "Tests/PluginTests", 58 | resources: [ 59 | .process("Localized.yml") 60 | ], 61 | plugins: [ 62 | "GenerateLocalized" 63 | ] 64 | ) 65 | ] 66 | ) 67 | -------------------------------------------------------------------------------- /Plugins/GenerateLocalized/Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plugin.swift 3 | // Localized 4 | // 5 | // Created by david-swift on 02.03.24. 6 | // 7 | 8 | import Foundation 9 | import PackagePlugin 10 | 11 | /// The build tool plugin for generating Swift code from the `Localized.yml` file. 12 | @main 13 | struct Plugin: BuildToolPlugin { 14 | 15 | /// Create the commands for generating the code. 16 | /// - Parameters: 17 | /// - context: The plugin context. 18 | /// - target: The target. 19 | /// - Returns: The commands. 20 | func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { 21 | guard let target = target.sourceModule, 22 | let inputFile = target.sourceFiles.first( 23 | where: { ["Localized.yml", "Localized.yaml"].contains($0.path.lastComponent) } 24 | ) else { 25 | return [] 26 | } 27 | let outputFile = context.pluginWorkDirectory.appending(subpath: "Localized.swift") 28 | return [ 29 | .buildCommand( 30 | displayName: "Generating Localized.swift", 31 | executable: try context.tool(named: "Generation").path, 32 | arguments: [inputFile.path.string, outputFile.string], 33 | inputFiles: [inputFile.path], 34 | outputFiles: [outputFile] 35 | ) 36 | ] 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 3 | > **This project has moved. You can find it [here](https://git.aparoksha.dev/aparoksha/localized).** 4 | > 5 | > The decision is based on [this article](https://sfconservancy.org/GiveUpGitHub/). 6 | > 7 | > Thanks to [No GitHub](https://codeberg.org/NoGitHub) for the badge used below. 8 | > 9 | > [](https://sfconservancy.org/GiveUpGitHub/) 10 | 11 |
12 |
13 |
17 | 18 | GitHub 19 | 20 | · 21 | 22 | Contributor Docs 23 | 24 |
25 | 26 | _Localized_ provides a Swift package plugin for localizing cross-platform Swift code. 27 | 28 | Use YML syntax for defining available phrases: 29 | 30 | ```yml 31 | hello(name): 32 | en: Hello, (name)! 33 | de: Hallo, (name)! 34 | fr: Salut, (name)! 35 | 36 | house: 37 | en: House 38 | de: Haus 39 | fr: Maison 40 | 41 | houses(count): 42 | en(count == "1"): There is one house. 43 | en: There are (count) houses. 44 | de(count == "1"): Es gibt ein Haus. 45 | de: Es gibt (count) Häuser. 46 | ``` 47 | 48 | Then, access the localized strings safely in your code: 49 | 50 | ```swift 51 | // Use the system language 52 | print(Loc.hello(name: "Peter")) 53 | print(Loc.house) 54 | print(Loc.houses(count: 1)) 55 | 56 | // Access the translation for a specific language 57 | print(Localized.hello(name: "Peter").en) 58 | print(Localized.house.fr) 59 | ``` 60 | 61 | ## Table of Contents 62 | 63 | - [Installation][4] 64 | - [Usage][5] 65 | - [Thanks][6] 66 | 67 | ## Installation 68 | 69 | 1. Open your Swift package in GNOME Builder, Xcode, or any other IDE. 70 | 2. Open the `Package.swift` file. 71 | 3. Into the `Package` initializer, under `dependencies`, paste: 72 | ```swift 73 | .package(url: "https://github.com/AparokshaUI/Localized", from: "0.1.0") 74 | ``` 75 | 76 | ## Usage 77 | 78 | ### Definition 79 | 80 | Define the available phrases in a file called `Localized.yml`. 81 | 82 | ```yml 83 | default: en 84 | 85 | export: 86 | en: Export Document 87 | de: Exportiere das Dokument 88 | 89 | send(message, name): 90 | en(name == ""): Send (message). 91 | en: Send (message) to (name). 92 | de: Sende (message) to (name). 93 | ``` 94 | 95 | As you can see, you can add parameters using brackets after the key, 96 | and conditions using brackets after the language (e.g. for pluralization). 97 | 98 | The line `default: en` sets English as the fallback language. 99 | 100 | Then, add the `Localized` dependency, the plugin and the `Localized.yml` resource 101 | to the target in the `Package.swift` file. 102 | 103 | ```swift 104 | .executableTarget( 105 | name: "PluginTests", 106 | dependencies: ["Localized"], 107 | resources: [.process("Localized.yml")], 108 | plugins: ["GenerateLocalized"] 109 | ) 110 | ``` 111 | 112 | ### Usage 113 | 114 | In most cases, you want to get the translated string in the system language. 115 | This can be accomplished using the following syntax. 116 | 117 | ```swift 118 | let export = Loc.export 119 | let send = Loc.send(message: "Hello", name: "Peter") 120 | ``` 121 | 122 | You can access a specific language as well. 123 | 124 | ```swift 125 | let export = Localized.export.en 126 | let send = Localized.send(message: "Hallo", name: "Peter").de 127 | ``` 128 | 129 | If you want to get the translation for a specific language code, use the following syntax. 130 | This function will return the translation for the default language if there's no translation for the prefix of that code available. 131 | 132 | ```swift 133 | let export = Localized.export.string(for: "de-CH") 134 | ``` 135 | 136 | ## Thanks 137 | 138 | ### Dependencies 139 | - [Yams](https://github.com/jpsim/Yams) licensed under the [MIT license](https://github.com/jpsim/Yams/blob/main/LICENSE) 140 | 141 | ### Other Thanks 142 | - The [contributors][7] 143 | - [SwiftLint][8] for checking whether code style conventions are violated 144 | - The programming language [Swift][9] 145 | - [SourceDocs][10] used for generating the [docs][11] 146 | 147 | [1]: Tests/ 148 | [2]: #goals 149 | [3]: #widgets 150 | [4]: #installation 151 | [5]: #usage 152 | [6]: #thanks 153 | [7]: Contributors.md 154 | [8]: https://github.com/realm/SwiftLint 155 | [9]: https://github.com/apple/swift 156 | [10]: https://github.com/SourceDocs/SourceDocs 157 | [11]: Documentation/README.md 158 | 159 | -------------------------------------------------------------------------------- /Sources/Generation/Generation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generation.swift 3 | // Localized 4 | // 5 | // Created by david-swift on 02.03.2024. 6 | // 7 | 8 | import Foundation 9 | import GenerationLibrary 10 | 11 | try Generation.main() 12 | 13 | /// A type containing the generation function for the plugin. 14 | public enum Generation { 15 | 16 | /// Generate the Swift code for the plugin. 17 | public static func main() throws { 18 | let yml = try String(contentsOfFile: CommandLine.arguments[1]) 19 | let content = try GenerationLibrary.Generation.getCode(yml: yml) 20 | let outputPathIndex = 2 21 | FileManager.default.createFile( 22 | atPath: CommandLine.arguments[outputPathIndex], 23 | contents: .init(("import Localized" + "\n\n" + content[0] + "\n\n" + content[1]).utf8) 24 | ) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/GenerationLibrary/Generation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generation.swift 3 | // Localized 4 | // 5 | // Created by david-swift on 02.03.2024. 6 | // 7 | 8 | import Yams 9 | 10 | /// Generate the Swift code for the plugin and macro. 11 | public enum Generation { 12 | 13 | /// Number of spaces for indentation 1. 14 | static let indentOne = 4 15 | /// Number of spaces for indentation 2. 16 | static let indentTwo = 8 17 | /// Number of spaces for indentation 3. 18 | static let indentThree = 12 19 | 20 | /// An error that occurs during code generation. 21 | public enum GenerationError: Error { 22 | 23 | /// A translation in the default language missing for a specific key. 24 | /// Missing translations in other languages will cause the default language to be used. 25 | case missingTranslationInDefaultLanguage(key: String) 26 | /// An unknown error occured while parsing the YML. 27 | case unknownYMLPasingError 28 | /// The default language information is missing. 29 | case missingDefaultLanguage 30 | 31 | } 32 | 33 | /// Get the Swift code for the plugin and macro. 34 | /// - Parameter yml: The YML code. 35 | /// - Returns: The code. 36 | public static func getCode(yml: String) throws -> [String] { 37 | guard var dict = try Yams.load(yaml: yml) as? [String: Any] else { 38 | throw GenerationError.unknownYMLPasingError 39 | } 40 | guard let defaultLanguage = dict["default"] as? String else { 41 | throw GenerationError.missingDefaultLanguage 42 | } 43 | dict["default"] = nil 44 | guard let dictionary = dict as? [String: [String: String]] else { 45 | throw GenerationError.unknownYMLPasingError 46 | } 47 | return [ 48 | """ 49 | public enum Localized { 50 | 51 | public static var yml: String { 52 | \""" 53 | \(indent(yml, by: indentTwo)) 54 | \""" 55 | } 56 | 57 | \(generateEnumCases(dictionary: dictionary)) 58 | 59 | public var string: String { string(for: System.getLanguage()) } 60 | 61 | \(try generateTranslations(dictionary: dictionary, defaultLanguage: defaultLanguage)) 62 | 63 | \(generateLanguageFunction(dictionary: dictionary, defaultLanguage: defaultLanguage)) 64 | 65 | } 66 | """, 67 | """ 68 | public enum Loc { 69 | 70 | \(generateStaticLocVariables(dictionary: dictionary)) 71 | 72 | } 73 | """ 74 | ] 75 | } 76 | 77 | /// Generate the cases for the `Localized` enumeration. 78 | /// - Parameter dictionary: The parsed YML. 79 | /// - Returns: The syntax. 80 | static func generateEnumCases(dictionary: [String: [String: String]]) -> String { 81 | var result = "" 82 | for entry in dictionary { 83 | let key = parse(key: entry.key) 84 | if key.1.isEmpty { 85 | result.append(""" 86 | case \(entry.key) 87 | 88 | """) 89 | } else { 90 | var line = "case \(key.0)(" 91 | for argument in key.1 { 92 | line += "\(argument): CustomStringConvertible, " 93 | } 94 | line.removeLast(", ".count) 95 | line += ")" 96 | result.append(""" 97 | \(line) 98 | 99 | """) 100 | } 101 | } 102 | return result 103 | } 104 | 105 | /// Generate the static variables and functions for the `Loc` type. 106 | /// - Parameter dictionary: The parsed YML. 107 | /// - Returns: The syntax. 108 | static func generateStaticLocVariables(dictionary: [String: [String: String]]) -> String { 109 | var result = "" 110 | for entry in dictionary { 111 | let key = parse(key: entry.key) 112 | if key.1.isEmpty { 113 | result.append(""" 114 | public static var \(entry.key): String { Localized.\(entry.key).string } 115 | 116 | """) 117 | } else { 118 | var line = "public static func \(key.0)(" 119 | for argument in key.1 { 120 | line += "\(argument): CustomStringConvertible, " 121 | } 122 | line.removeLast(", ".count) 123 | line += ") -> String {\n" + indent("Localized.\(key.0)(", by: indentOne) 124 | for argument in key.1 { 125 | line += "\(argument): \(argument), " 126 | } 127 | line.removeLast(", ".count) 128 | line += ").string" 129 | line += "\n}" 130 | result.append(""" 131 | \(line) 132 | 133 | """) 134 | } 135 | } 136 | return result 137 | } 138 | 139 | /// Generate the variables for the translations. 140 | /// - Parameters: 141 | /// - dictionary: The parsed YML. 142 | /// - defaultLanguage: The default language. 143 | /// - Returns: The syntax. 144 | static func generateTranslations(dictionary: [String: [String: String]], defaultLanguage: String) throws -> String { 145 | var result = "" 146 | for language in getLanguages(dictionary: dictionary) { 147 | var variable = indent("public var \(language): String {", by: indentOne) 148 | variable += indent("\nswitch self {", by: indentTwo) 149 | for entry in dictionary { 150 | let key = parse(key: entry.key) 151 | guard let valueForLanguage = entry.value[language] ?? entry.value[defaultLanguage] else { 152 | throw GenerationError.missingTranslationInDefaultLanguage(key: key.0) 153 | } 154 | let value = parseValue( 155 | defaultTranslation: valueForLanguage, 156 | translations: entry.value, 157 | language: language, 158 | arguments: key.1 159 | ) 160 | if key.1.isEmpty { 161 | variable += indent("\ncase .\(entry.key):", by: indentTwo) 162 | variable += value 163 | } else { 164 | variable += indent("\ncase let .\(entry.key):", by: indentTwo) 165 | variable += value 166 | } 167 | } 168 | variable += indent("\n }\n}", by: indentOne) 169 | result += """ 170 | \(variable) 171 | 172 | """ 173 | } 174 | return result 175 | } 176 | 177 | /// Parse the content of a switch case. 178 | /// - Parameters: 179 | /// - defaultTranslation: The translation without any conditions (always required). 180 | /// - translations: All the available translations for an entry. 181 | /// - language: The language. 182 | /// - arguments: The arguments of the entry. 183 | /// - Returns: The syntax. 184 | static func parseValue( 185 | defaultTranslation: String, 186 | translations: [String: String], 187 | language: String, 188 | arguments: [String] = [] 189 | ) -> String { 190 | var value = "\n" 191 | let conditionTranslations = translations.filter { $0.key.hasPrefix(language + "(") } 192 | let lastTranslation = parse(translation: defaultTranslation, arguments: arguments) 193 | for argument in arguments { 194 | value += "let \(argument) = \(argument).description\n" 195 | } 196 | if conditionTranslations.isEmpty { 197 | return indent(value + "return \"\(lastTranslation)\"", by: indentThree) 198 | } 199 | for translation in conditionTranslations { 200 | var condition = translation.key.split(separator: "(")[1] 201 | condition.removeLast() 202 | value.append(indent(""" 203 | if \(condition) { 204 | return \"\(parse(translation: translation.value, arguments: arguments))\" 205 | } else 206 | """, by: indentThree)) 207 | } 208 | value.append(""" 209 | { 210 | return \"\(lastTranslation)\" 211 | } 212 | """) 213 | return value 214 | } 215 | 216 | /// Generate the function for getting the translated string for a specified language code. 217 | /// - Parameters: 218 | /// - dictionary: The parsed YML. 219 | /// - defaultLanguage: The default language. 220 | /// - Returns: The syntax. 221 | static func generateLanguageFunction( 222 | dictionary: [String: [String: String]], 223 | defaultLanguage: String 224 | ) -> String { 225 | var result = "public func string(for language: String) -> String {\n" 226 | let languages = getLanguages(dictionary: dictionary) 227 | for language in languages.sorted().reversed() where language != defaultLanguage { 228 | result += indent("if language.hasPrefix(\"\(language)\") {", by: indentTwo) 229 | result += indent("\nreturn \(language)", by: indentThree) 230 | result += indent("\n} else", by: indentTwo) 231 | } 232 | if languages.count <= 1 { 233 | result += """ 234 | return \(defaultLanguage) 235 | } 236 | """ 237 | } else { 238 | result += """ 239 | { 240 | return \(defaultLanguage) 241 | } 242 | } 243 | """ 244 | } 245 | return result 246 | } 247 | 248 | /// Get the available languages. 249 | /// - Parameter dictionary: The parsed YML. 250 | /// - Returns: The syntax 251 | static func getLanguages(dictionary: [String: [String: String]]) -> [String] { 252 | var languages: Set