diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..ee1a591
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -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.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..c244dbb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -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
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..f0ed3f3
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -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
diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml
new file mode 100644
index 0000000..5348bdb
--- /dev/null
+++ b/.github/workflows/swiftlint.yml
@@ -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
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..3cd1d9d
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,159 @@
+# 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
+ - 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
+ - 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/Localizer/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// Localizer\n//\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*'
+missing_docs:
+ 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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..42aa4a1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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 `data` that contains files with information about the app as well as SVG 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!
diff --git a/Contributors.md b/Contributors.md
new file mode 100644
index 0000000..3cf3f03
--- /dev/null
+++ b/Contributors.md
@@ -0,0 +1,3 @@
+# Contributors
+
+- [david-swift](https://github.com/david-swift)
diff --git a/Package.swift b/Package.swift
index 7f7e1c9..e5e8e96 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,20 +1,38 @@
// swift-tools-version: 5.8
-// The swift-tools-version declares the minimum version of Swift required to build this package.
+//
+// Package.swift
+// Localizer
+//
import PackageDescription
let package = Package(
- name: "Adwaita Template",
+ name: "Localizer",
dependencies: [
- .package(url: "https://github.com/AparokshaUI/Adwaita", from: "0.2.0")
+ .package(url: "https://github.com/AparokshaUI/Adwaita", from: "0.2.5"),
+ .package(url: "https://github.com/AparokshaUI/Localized", from: "0.2.2"),
+ .package(url: "https://github.com/jpsim/Yams", from: "5.0.6")
],
targets: [
.executableTarget(
- name: "AdwaitaTemplate",
+ name: "Localizer",
dependencies: [
+ "Model",
.product(name: "Adwaita", package: "Adwaita")
+ ]
+ ),
+ .target(
+ name: "Model",
+ dependencies: [
+ .product(name: "Localized", package: "Localized"),
+ .product(name: "Yams", package: "Yams")
+ ],
+ resources: [
+ .process("Localized.yml")
],
- path: "Sources"
+ plugins: [
+ .plugin(name: "GenerateLocalized", package: "Localized")
+ ]
)
]
)
diff --git a/README.md b/README.md
index 9fe0107..e9d22af 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,11 @@
-
-
Adwaita Template
+
+ Localizer
-_Adwaita Template_ is a template application for the [Adwaita package](https://github.com/AparokshaUI/Adwaita/).
+Localize GNOME apps built using Swift, and other Swift programs utilizing the [Localized](https://github.com/AparokshaUI/Localized) package.
+
+![Localizer screenshot](data/icons/screenshot.png)
## Table of Contents
@@ -13,69 +15,19 @@ _Adwaita Template_ is a template application for the [Adwaita package](https://g
## Installation
-### Install the Swift 5 Freedesktop SDK Extension
-
-1. Install `flatpak`.
-2. Add Flathub to Flatpak:
-```
-flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-```
-3. Install the newest version of the Freedesktop SDK:
-```
-flatpak install flathub org.freedesktop.Sdk
-```
-4. Install the LLVM 16 Freedesktop extension:
-```
-flatpak install flathub org.freedesktop.Sdk.Extension.llvm16/x86_64/23.08
-```
-5. Install the GNOME SDK. It's not required for building the Swift Freedesktop SDK extension, but later for building the application:
-```
-flatpak install flathub org.gnome.Sdk
-```
-6. Install the Swift 5 Freedesktop SDK Extension.
-```
-flatpak install flathub org.freedesktop.Sdk.Extension.swift5
-```
-
-### Install Other Tools
-
-The following tools are required or recommended for editing this repository:
-- [GNOME Builder](https://flathub.org/apps/org.gnome.Builder)
-- [App Icon Preview](https://flathub.org/apps/org.gnome.design.AppIconPreview)
-- [Inkscape](https://flathub.org/apps/org.inkscape.Inkscape)
+Download the Flatpak file from the latest release, open it in GNOME Software, and press the install button.
## Usage
-1. Open this project in GNOME Builder. Copy the path to the containing folder of this file (in the sidebar) and replace the following piece of text in the `io.github.AparokshaUI.AdwaitaTemplate.json` file:
-```
-https://github.com/AparokshaUI/AdwaitaTemplate
-```
-with the following syntax (replace `/path/to/directoy`):
-```
-file:///path/to/directory
-```
-2. Build and run the application.
-3. Change the app's name and other information about the application in the following files (and file names):
- - `README.md`
- - `Package.swift`
- - `io.github.AparokshaUI.AdwaitaTemplate.json`
- - `Sources/AdwaitaTemplate.swift`
- - `data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml`
- - `data/io.github.AparokshaUI.AdwaitaTemplate.desktop`
- - `data/icons/io.github.AparokshaUI.AdwaitaTemplate.Source.svg`
- - `data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg`
- - `data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg`
-4. Edit the code. Help is available [here](https://david-swift.gitbook.io/adwaita/), ask questions in the [discussions](https://github.com/AparokshaUI/Adwaita/discussions/).
-5. Edit the app's icons using the previously installed tools according to [this](https://blogs.gnome.org/tbernard/2019/12/30/designing-an-icon-for-your-app/) tutorial.
-6. In GNOME Builder, click on the dropdown next to the hammer and then on `Export`. Wait until the file explorer appears, open the `.flatpak` file and install the app on your device!
+Clone the app's repository and locate the `Localized.yml` file.
+Import it into this app.
-### Flatpak SPM Generator
+Then, start translating with the UI.
-If you want to e.g. publish your app on Flathub and there is no internet connection allowed while running the build commands,
-you can use [this tool](https://github.com/flatpak/flatpak-builder-tools/tree/master/spm) that lets you generate a Flatpak manifest JSON from a Swift package.
+Once you're ready to commit, use again git and submit to the repository.
## Thanks
### Dependencies
-- [Adwaita](https://github.com/AparokshaUI/Adwaita) licensed under the [GPL-3.0 license](https://github.com/AparokshaUI/Adwaita/blob/main/LICENSE.md)
-
+- [Adwaita](https://github.com/AparokshaUI/Adwaita) licensed under the [MIT license](https://github.com/AparokshaUI/Adwaita/blob/main/LICENSE.md)
+- [Localized](https://github.com/AparokshaUI/Localized) licensed under the [MIT license](https://github.com/AparokshaUI/Localized/blob/master/LICENSE.md)
diff --git a/Sources/AdwaitaTemplate.swift b/Sources/AdwaitaTemplate.swift
deleted file mode 100644
index b5f7efa..0000000
--- a/Sources/AdwaitaTemplate.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-// The Swift Programming Language
-// https://docs.swift.org/swift-book
-
-import Adwaita
-
-@main
-struct AdwaitaTemplate: App {
-
- let id = "io.github.AparokshaUI.AdwaitaTemplate"
- var app: GTUIApp!
-
- var scene: Scene {
- Window(id: "main") { window in
- Text("Hello, world!")
- .padding()
- .topToolbar {
- ToolbarView(app: app, window: window)
- }
- }
- .defaultSize(width: 450, height: 300)
- }
-
-}
diff --git a/Sources/Localizer/ArgumentsRow.swift b/Sources/Localizer/ArgumentsRow.swift
new file mode 100644
index 0000000..d08c3bb
--- /dev/null
+++ b/Sources/Localizer/ArgumentsRow.swift
@@ -0,0 +1,62 @@
+//
+// ArgumentsRow.swift
+// Localizer
+//
+
+import Adwaita
+import Model
+
+struct ArgumentsRow: View {
+
+ @Binding var arguments: [String]
+ @State private var expanded = false
+ @State private var focus = Signal()
+
+ var view: Body {
+ ExpanderRow()
+ .title(Loc.arguments)
+ .subtitle(Loc.argumentsDescription)
+ .suffix {
+ VStack {
+ Button(icon: .default(icon: .listAdd)) {
+ arguments.append("")
+ expanded = true
+ focus.signal()
+ }
+ .style("flat")
+ .tooltip(Loc.addArgument)
+ }
+ .valign(.center)
+ }
+ .rows {
+ List(.init(arguments.indices), selection: nil) { index in
+ let keyword = arguments[safe: index] ?? ""
+ EntryRow(Loc.argument, text: .init {
+ keyword
+ } set: { newValue in
+ arguments[safe: index] = newValue
+ })
+ .suffix {
+ VStack {
+ Button(icon: .default(icon: .userTrash)) {
+ arguments = arguments.filter { $0 != keyword }
+ }
+ .style("flat")
+ .tooltip(Loc.removeArgument)
+ }
+ .valign(.center)
+ }
+ .entryActivated {
+ arguments.append("")
+ focus.signal()
+ }
+ .focus(index == arguments.count - 1 ? focus : .init())
+ }
+ .style("boxed-list")
+ .padding()
+ }
+ .enableExpansion(.constant(!arguments.isEmpty))
+ .expanded($expanded)
+ }
+
+}
diff --git a/Sources/Localizer/LanguagesView.swift b/Sources/Localizer/LanguagesView.swift
new file mode 100644
index 0000000..4cc2818
--- /dev/null
+++ b/Sources/Localizer/LanguagesView.swift
@@ -0,0 +1,43 @@
+//
+// LanguagesView.swift
+// Localizer
+//
+
+import Adwaita
+import Foundation
+import Model
+import Yams
+
+struct LanguagesView: View {
+
+ @Binding var stack: NavigationStack
+
+ var view: Body {
+ ScrollView {
+ List(LocInfo.languages, selection: nil) { language in
+ let isDefault = LocInfo.defaultLanguage == language
+ let percentage = isDefault ? Loc.defaultLanguage : Loc.complete(
+ percentage: LocInfo.completion(for: language)
+ )
+ ActionRow()
+ .title("\(language)")
+ .subtitle(percentage)
+ .suffix {
+ ButtonContent()
+ .iconName(Icon.default(icon: .goNext).string)
+ }
+ .activatableWidget {
+ Button()
+ .activate {
+ stack.push(.localize(language: language))
+ }
+ }
+ }
+ .style("boxed-list")
+ .formWidth()
+ .padding(20)
+ Text("")
+ }
+ }
+
+}
diff --git a/Sources/Localizer/LocalizePhraseView.swift b/Sources/Localizer/LocalizePhraseView.swift
new file mode 100644
index 0000000..2811900
--- /dev/null
+++ b/Sources/Localizer/LocalizePhraseView.swift
@@ -0,0 +1,170 @@
+//
+// LocalizePhraseView.swift
+// Localizer
+//
+
+import Adwaita
+import Model
+
+struct LocalizePhraseView: View {
+
+ @State private var errorDialog = false
+ @State private var showArgumentsDialog = false
+ var selectedPhrase: String?
+ var phrase: Phrase
+ var language: String
+ var focusNext: () -> Void
+
+ var defaultLanguage: String { LocInfo.defaultLanguage }
+ var translation: Translation? { phrase.translations.first { $0.language == language } }
+ var defaultTranslation: String? {
+ phrase.translations.first { translation in
+ translation.language == self.defaultLanguage && translation.conditions.isEmpty
+ }?.translation
+ }
+
+ var view: Body {
+ Form {
+ if let defaultTranslation {
+ ActionRow()
+ .title(phrase.id)
+ .subtitle(defaultTranslation)
+ .suffix {
+ argumentsSuffix
+ }
+ .subtitleSelectable()
+ .titleSelectable()
+ .style("property")
+ } else {
+ noDefaultTranslationRow
+ }
+ EntryRow(
+ Loc.translation,
+ text: .init {
+ translation?.translation ?? ""
+ } set: { newValue in
+ try? LocInfo.updateTranslation(phrase: phrase.id, lang: language, translation: newValue)
+ }
+ )
+ .entryActivated {
+ focusNext()
+ }
+ .focused(.constant(selectedPhrase == phrase.key))
+ }
+ .formWidth()
+ .padding(10, .vertical)
+ }
+
+ var noDefaultTranslationRow: View {
+ ActionRow()
+ .title(phrase.key)
+ .suffix {
+ HStack {
+ Button(icon: .default(icon: .dialogError)) {
+ errorDialog = true
+ }
+ .padding(10, .vertical)
+ .style("flat")
+ .tooltip(Loc.noTranslation(defaultLanguage: self.defaultLanguage))
+ .popover(visible: $errorDialog) {
+ Text(Loc.noTranslation(defaultLanguage: self.defaultLanguage))
+ .padding()
+ }
+ if !phrase.arguments.isEmpty {
+ Button(icon: .default(icon: .viewListBullet)) {
+ showArgumentsDialog = true
+ }
+ }
+ }
+ .modifyContent(VStack.self) { $0.spacing(10) }
+ }
+ .style("error")
+ }
+
+ @ViewBuilder var argumentsSuffix: View {
+ if !phrase.arguments.isEmpty {
+ Button(icon: .default(icon: .viewListBullet)) {
+ showArgumentsDialog = true
+ }
+ .padding(10, .vertical)
+ .style("flat")
+ .dialog(
+ visible: $showArgumentsDialog,
+ title: Loc.conditionalTranslations(phrase: defaultTranslation ?? phrase.key),
+ height: 400
+ ) {
+ argumentsDialog
+ }
+ }
+ }
+
+ var argumentsDialog: View {
+ ScrollView {
+ ForEach(phrase.arguments) { argument in
+ let prefix = "\(argument) == "
+ FormSection(argument) {
+ ForEach(
+ phrase.translations.filter { translation in
+ translation.conditions[safe: 0]?.hasPrefix(prefix) ?? false
+ && translation.language == language
+ }
+ ) { translation in
+ argumentsForm(translation: translation, argument: argument)
+ }
+ }
+ .suffix {
+ Button(icon: .default(icon: .listAdd)) {
+ try? LocInfo.updateTranslation(
+ phrase: phrase.id,
+ lang: language,
+ translation: "",
+ conditions: [prefix + "\"\""]
+ )
+ }
+ .padding(10, .vertical)
+ .style("flat")
+ }
+ }
+ .padding(20)
+ .formWidth()
+ }
+ .topToolbar {
+ HeaderBar.empty()
+ }
+ .stopModifiers()
+ }
+
+ @ViewBuilder
+ func argumentsForm(translation: Translation, argument: String) -> View {
+ let prefix = "\(argument) == "
+ Form {
+ EntryRow("Value", text: .init {
+ var value = translation.conditions[safe: 0] ?? ""
+ value.removeFirst(prefix.count + 1)
+ value.removeLast()
+ return value
+ } set: { newValue in
+ try? LocInfo.updateTranslation(
+ phrase: phrase.id,
+ lang: language,
+ translation: translation.translation,
+ conditions: [prefix + "\"\(newValue)\""],
+ previousConditions: translation.conditions
+ )
+ })
+ EntryRow(Loc.translation, text: .init {
+ translation.translation
+ } set: { newValue in
+ try? LocInfo.updateTranslation(
+ phrase: phrase.id,
+ lang: language,
+ translation: newValue,
+ conditions: translation.conditions,
+ previousConditions: translation.conditions
+ )
+ })
+ }
+ .padding(10, .vertical)
+ }
+
+}
diff --git a/Sources/Localizer/LocalizeView.swift b/Sources/Localizer/LocalizeView.swift
new file mode 100644
index 0000000..db665c5
--- /dev/null
+++ b/Sources/Localizer/LocalizeView.swift
@@ -0,0 +1,34 @@
+//
+// LocalizeView.swift
+// Localizer
+//
+
+import Adwaita
+import Foundation
+import Model
+import Yams
+
+struct LocalizeView: View {
+
+ @State private var focusedEntry: String?
+ var language: String
+
+ var view: Body {
+ ScrollView {
+ ForEach(LocInfo.phrases) { phrase in
+ LocalizePhraseView(
+ selectedPhrase: focusedEntry,
+ phrase: phrase,
+ language: language
+ ) {
+ let index = LocInfo.phrases.firstIndex { $0.key == phrase.key }
+ focusedEntry = LocInfo.phrases[safe: (index ?? -1) + 1]?.key
+ focusedEntry = nil
+ }
+ }
+ .formWidth()
+ .padding(20)
+ }
+ }
+
+}
diff --git a/Sources/Localizer/Localizer.swift b/Sources/Localizer/Localizer.swift
new file mode 100644
index 0000000..436f19d
--- /dev/null
+++ b/Sources/Localizer/Localizer.swift
@@ -0,0 +1,229 @@
+//
+// Localizer.swift
+// Localizer
+//
+
+import Adwaita
+import Foundation
+import Model
+
+@main
+struct Localizer: App {
+
+ static let localizerID = "io.github.AparokshaUI.Localizer"
+
+ @State private var destination = NavigationStack()
+ @State private var appendingLanguage = false
+ @State private var languageEntry = ""
+ @State private var appendingPhrase = false
+ @State private var idEntry = ""
+ @State private var defaultLanguageEntry = ""
+ @State private var defaultLanguageEntryFocus = Signal()
+ @State private var arguments: [String] = []
+
+ @State("width", folder: localizerID)
+ private var width = 600
+ @State("height", folder: localizerID)
+ private var height = 450
+
+ let id = localizerID
+ var app: GTUIApp!
+
+ var scene: Scene {
+ mainWindow
+ dialog
+ }
+
+ @SceneBuilder var mainWindow: Scene {
+ Window(id: "main") { window in
+ NavigationView($destination, Loc.openFile) { destination in
+ switch destination {
+ case .overview:
+ Overview(destination: $destination)
+ .valign(.center)
+ .padding()
+ .topToolbar {
+ ToolbarView()
+ }
+ case .languages:
+ LanguagesView(stack: $destination)
+ .topToolbar {
+ ToolbarView()
+ }
+ .modifyContent(HeaderBar.self) { languagesHeaderBar(headerBar: $0) }
+ case let .localize(language: language):
+ LocalizeView(language: language)
+ .topToolbar {
+ ToolbarView()
+ }
+ .modifyContent(HeaderBar.self) { localizeHeaderBar(headerBar: $0, lang: language) }
+ }
+ } initialView: {
+ openFile(window: window)
+ }
+ }
+ .size(width: $width, height: $height)
+ .closeShortcut()
+ .quitShortcut()
+ }
+
+ @SceneBuilder var dialog: Scene {
+ FileDialog(
+ importer: "open",
+ extensions: ["yml", "yaml"]
+ ) { url in
+ LocInfo.url = url
+ destination.push(.overview)
+ } onClose: {
+ }
+ }
+
+ init() {
+ LocInfo.updateClosure = {
+ State.updateViews(force: true)
+ }
+ }
+
+ func openFile(window: GTUIApplicationWindow) -> View {
+ StatusPage(
+ Loc.localizeApp,
+ icon: .custom(name: "io.github.AparokshaUI.Localizer"),
+ description: Loc.openFileDescription
+ ) {
+ Button(Loc.openFile) {
+ app.addWindow("open", parent: window)
+ }
+ .style("suggested-action")
+ .style("pill")
+ .horizontalCenter()
+ }
+ .topToolbar {
+ ToolbarView()
+ }
+ }
+
+ @ViewBuilder
+ func languagesHeaderBar(headerBar: HeaderBar) -> View {
+ let insensitive = LocInfo.languages.contains { $0 == languageEntry }
+ headerBar
+ .start {
+ Button(icon: .default(icon: .listAdd)) {
+ appendingLanguage.toggle()
+ }
+ .tooltip(Loc.addLanguage)
+ .popover(visible: $appendingLanguage) {
+ languagePopover(insensitive: insensitive)
+ }
+ }
+ }
+
+ @ViewBuilder
+ func localizeHeaderBar(headerBar: HeaderBar, lang: String) -> View {
+ let insensitive = idEntry.isEmpty || defaultLanguageEntry.isEmpty
+ headerBar
+ .titleWidget {
+ WindowTitle(
+ subtitle: Loc.complete(percentage: LocInfo.completion(for: lang)),
+ title: Loc.localizeLanguage(language: lang)
+ )
+ }
+ .start {
+ Button(icon: .default(icon: .listAdd)) {
+ appendingPhrase.toggle()
+ }
+ .tooltip(Loc.addPhrase)
+ .popover(visible: $appendingPhrase) {
+ phrasePopover(insensitive: insensitive)
+ }
+ }
+ }
+
+ func languagePopover(insensitive: Bool) -> View {
+ VStack {
+ Text(Loc.addLanguage)
+ .style("title-2")
+ Form {
+ EntryRow(Loc.languages(count: 1), text: $languageEntry)
+ .entryActivated {
+ if !insensitive {
+ addLanguage()
+ }
+ }
+ }
+ HStack {
+ Button(Loc.cancel) {
+ cancelLanguage()
+ }
+ Text("")
+ .hexpand()
+ Button(Loc.add) {
+ addLanguage()
+ }
+ .style("suggested-action")
+ .insensitive(insensitive)
+ }
+ }
+ .spacing(20)
+ .popoverWidth()
+ .padding(20)
+ }
+
+ func phrasePopover(insensitive: Bool) -> View {
+ VStack {
+ Text(Loc.addPhrase)
+ .style("title-2")
+ Form {
+ EntryRow(Loc.key, text: $idEntry)
+ .entryActivated {
+ defaultLanguageEntryFocus.signal()
+ }
+ EntryRow(Loc.translationInDefaultLanguage, text: $defaultLanguageEntry)
+ .entryActivated {
+ if !insensitive {
+ addPhrase()
+ }
+ }
+ .focus(defaultLanguageEntryFocus)
+ ArgumentsRow(arguments: $arguments)
+ }
+ HStack {
+ Button(Loc.cancel) {
+ cancelPhrase()
+ }
+ Text("")
+ .hexpand()
+ Button(Loc.add) {
+ addPhrase()
+ }
+ .style("suggested-action")
+ .insensitive(insensitive)
+ }
+ }
+ .spacing(20)
+ .popoverWidth()
+ .padding(20)
+ }
+
+ func addLanguage() {
+ try? LocInfo.addLanguage(languageEntry)
+ cancelLanguage()
+ }
+
+ func cancelLanguage() {
+ languageEntry = ""
+ appendingLanguage = false
+ }
+
+ func addPhrase() {
+ try? LocInfo.addPhrase(id: idEntry, default: defaultLanguageEntry, arguments: arguments)
+ cancelPhrase()
+ }
+
+ func cancelPhrase() {
+ idEntry = ""
+ defaultLanguageEntry = ""
+ appendingPhrase = false
+ arguments = []
+ }
+
+}
diff --git a/Sources/Localizer/Overview.swift b/Sources/Localizer/Overview.swift
new file mode 100644
index 0000000..4c808a3
--- /dev/null
+++ b/Sources/Localizer/Overview.swift
@@ -0,0 +1,38 @@
+//
+// Overview.swift
+// Localizer
+//
+
+import Adwaita
+import Foundation
+import Model
+
+struct Overview: View {
+
+ @Binding var destination: NavigationStack
+
+ var view: Body {
+ HStack {
+ field(data: LocInfo.phrases.count, text: Loc.phrases(count: LocInfo.phrases.count))
+ field(data: LocInfo.languages.count, text: Loc.languages(count: LocInfo.languages.count))
+ }
+ .halign(.center)
+ .padding(30, .bottom)
+ Button(Loc.localizeProject) {
+ destination.push(.languages)
+ }
+ .style("suggested-action")
+ .style("pill")
+ .horizontalCenter()
+ }
+
+ func field(data: Int, text: String) -> View {
+ VStack {
+ Text("\(data)")
+ .style("title-1")
+ Text(text)
+ }
+ .padding()
+ }
+
+}
diff --git a/Sources/Localizer/ToolbarView.swift b/Sources/Localizer/ToolbarView.swift
new file mode 100644
index 0000000..fcc7de7
--- /dev/null
+++ b/Sources/Localizer/ToolbarView.swift
@@ -0,0 +1,31 @@
+//
+// ToolbarView.swift
+// Localizer
+//
+
+import Adwaita
+import Model
+
+struct ToolbarView: View {
+
+ @State private var about = false
+
+ var view: Body {
+ HeaderBar.end {
+ Button(icon: .custom(name: "io.github.AparokshaUI.Localizer.about-symbolic")) {
+ about = true
+ }
+ .tooltip(Loc.mainMenu)
+ .aboutDialog(
+ visible: $about,
+ app: "Localizer",
+ developer: "david-swift",
+ version: "0.1.0",
+ icon: .custom(name: "io.github.AparokshaUI.Localizer"),
+ website: .init(string: "https://github.com/AparokshaUI/Localizer"),
+ issues: .init(string: "https://github.com/AparokshaUI/Localizer/issues")
+ )
+ }
+ }
+
+}
diff --git a/Sources/Localizer/View.swift b/Sources/Localizer/View.swift
new file mode 100644
index 0000000..bb85463
--- /dev/null
+++ b/Sources/Localizer/View.swift
@@ -0,0 +1,18 @@
+//
+// View.swift
+// Localizer
+//
+
+import Adwaita
+
+extension View {
+
+ func formWidth() -> View {
+ frame(maxWidth: 500)
+ }
+
+ func popoverWidth() -> View {
+ frame(maxWidth: 300)
+ }
+
+}
diff --git a/Sources/Model/DestinationURL.swift b/Sources/Model/DestinationURL.swift
new file mode 100644
index 0000000..926d6dc
--- /dev/null
+++ b/Sources/Model/DestinationURL.swift
@@ -0,0 +1,30 @@
+//
+// DestinationURL.swift
+// Localizer
+//
+
+import Foundation
+
+/// The destination URL for navigation.
+public enum DestinationURL: CustomStringConvertible {
+
+ /// The overview.
+ case overview
+ /// The languages list.
+ case languages
+ /// The localization view.
+ case localize(language: String)
+
+ /// The title for a destination view.
+ public var description: String {
+ switch self {
+ case .overview:
+ Loc.overview
+ case .languages:
+ Loc.languages(count: "undefined")
+ case let .localize(language):
+ Loc.localizeLanguage(language: language)
+ }
+ }
+
+}
diff --git a/Sources/Model/Extensions/String.swift b/Sources/Model/Extensions/String.swift
new file mode 100644
index 0000000..8c6caf8
--- /dev/null
+++ b/Sources/Model/Extensions/String.swift
@@ -0,0 +1,16 @@
+//
+// String.swift
+// Localizer
+//
+
+extension String: Identifiable {
+
+ /// The identifier for a string.
+ public var id: Self { self }
+
+ /// Parse the string as an input.
+ public func parseInput() -> String {
+ replacingOccurrences(of: "\"", with: "\\\\\\\"")
+ }
+
+}
diff --git a/Sources/Model/Localized.yml b/Sources/Model/Localized.yml
new file mode 100644
index 0000000..5cb5250
--- /dev/null
+++ b/Sources/Model/Localized.yml
@@ -0,0 +1,116 @@
+default: en
+
+about:
+ de: "Info zu Localizer"
+ en: "About Localizer"
+
+add:
+ de: "Hinzufügen"
+ en: "Add"
+
+addArgument:
+ de: "Argument hinzufügen"
+ en: "Add Argument"
+
+addLanguage:
+ de: "Sprache hinzufügen"
+ en: "Add Language"
+
+addPhrase:
+ de: "Ausdruck hinzufügen"
+ en: "Add Phrase"
+
+argument:
+ de: "Argument"
+ en: "Argument"
+
+arguments:
+ de: "Argumente"
+ en: "Arguments"
+
+argumentsDescription:
+ de: "Zusätzliche Informationen den übersetzten Strings übergeben"
+ en: "Pass additional information to the translated strings"
+
+cancel:
+ de: "Abbrechen"
+ en: "Cancel"
+
+complete(percentage):
+ de: "(percentage)% fertig"
+ de(percentage == "10"): ""
+ de(percentage == "5"): ""
+ en: "(percentage)% complete"
+
+conditionalTranslations(phrase):
+ de: "Konditionale Übersetzungen für \\\"(phrase)\\\""
+ de(phrase == ""): ""
+ en: "Conditional Translations for \\\"(phrase)\\\""
+
+conditions:
+ de: "Bedingungen"
+ en: "Conditions"
+
+defaultLanguage:
+ de: "Standardsprache"
+ en: "Default Language"
+
+key:
+ de: "Schlüssel"
+ en: "Key"
+
+languages(count):
+ de: "Sprachen"
+ de(count == "1"): "Sprache"
+ en: "Languages"
+ en(count == "1"): "Language"
+
+localizeApp:
+ de: "Übersetze eine App"
+ en: "Localize an App"
+
+localizeLanguage(language):
+ de: "Übersetze auf \\\"(language)\\\""
+ en: "Localize \\\"(language)\\\""
+
+localizeProject:
+ de: "Übersetze das Projekt"
+ en: "Localize Project"
+
+mainMenu:
+ de: "Hauptmenü"
+ en: "Main Menu"
+
+noTranslation(defaultLanguage):
+ de: "Keine Übersetzung in der Standardsprache \\\"(defaultLanguage)\\\" verfügbar"
+ en: "No translation in default language \\\"(defaultLanguage)\\\" available"
+
+openFile:
+ de: "Datei öffnen"
+ en: "Open File"
+
+openFileDescription:
+ de: "Öffne deine Localized.yml-Datei"
+ en: "Open your Localized.yml file"
+
+overview:
+ de: "Übersicht"
+ en: "Overview"
+
+phrases(count):
+ de: "Ausdrücke"
+ de(count == "1"): "Ausdruck"
+ en: "Phrases"
+ en(count == "1"): "Phrase"
+
+removeArgument:
+ de: "Argument entfernen"
+ en: "Remove Argument"
+
+translation:
+ de: "Übersetzung"
+ en: "Translation"
+
+translationInDefaultLanguage:
+ de: "Übersetzung in die Standardsprache"
+ en: "Translation in Default Language"
diff --git a/Sources/Model/LocalizedInformation.swift b/Sources/Model/LocalizedInformation.swift
new file mode 100644
index 0000000..78c25c5
--- /dev/null
+++ b/Sources/Model/LocalizedInformation.swift
@@ -0,0 +1,256 @@
+//
+// LocalizedInformation.swift
+// Localizer
+//
+
+import Foundation
+import Yams
+
+/// A type containing variables and functions for reading and writing the yml data.
+public enum LocalizedInformation {
+
+ /// The prefix for empty languages.
+ static var emptyPrefix: String { "# empty: " }
+ /// The key for the default language.
+ static var defaultKey: String { "default" }
+ /// The yml file's URL.
+ public static var url: URL? {
+ didSet {
+ cache.reset()
+ }
+ }
+
+ /// The closure that updates the views.
+ public static var updateClosure: () -> Void = { }
+ /// The cache data.
+ static var cache: LocalizedCache = .init()
+
+ /// The content of the yml file.
+ static var content: String {
+ guard let url else {
+ return ""
+ }
+ if let cache = cache.content {
+ return cache
+ }
+ let data = (try? String(contentsOf: url)) ?? ""
+ cache.content = data
+ return data
+ }
+
+ static var ymlDictionary: [String: Any] {
+ if let cache = cache.dictionary, url != nil {
+ return cache
+ }
+ if let dict = try? Yams.load(yaml: content) as? [String: Any] {
+ cache.dictionary = dict
+ return dict
+ }
+ return [:]
+ }
+
+ /// The default language.
+ public static var defaultLanguage: String {
+ if let defaultLanguage = ymlDictionary[defaultKey] as? String {
+ return defaultLanguage
+ }
+ return "en"
+ }
+
+ /// The available languages.
+ public static var languages: [String] {
+ if let cache = cache.languages, url != nil {
+ return cache
+ }
+ var languages: Set = []
+ for key in phrases {
+ languages = languages.union(key.languages)
+ }
+ for var line in content.split(separator: "\n") where line.hasPrefix(emptyPrefix) {
+ line.removeFirst(emptyPrefix.count)
+ let language = parseKey(.init(line))
+ languages.insert(language.key)
+ }
+ let sorted = languages.sorted()
+ cache.languages = sorted
+ return sorted
+ }
+
+ /// The available phrases.
+ public static var phrases: [Phrase] {
+ if let cache = cache.phrases, url != nil {
+ return cache
+ }
+ var dict = ymlDictionary
+ dict[defaultKey] = nil
+ if let dictionary = dict as? [String: [String: String]] {
+ let phrases: [Phrase] = dictionary.map { entry in
+ let key = parseKey(entry.key)
+ return .init(
+ key: key.key,
+ arguments: key.arguments,
+ translations: entry.value.reduce(into: [Translation]()) { result, value in
+ let key = parseKey(value.key)
+ result.append(
+ .init(language: key.key, conditions: key.arguments, translation: parseValue(value.value))
+ )
+ }
+ .sorted { $0.id < $1.id }
+ )
+ }
+ .sorted { $0.key < $1.key }
+ cache.phrases = phrases
+ return phrases
+ }
+ return []
+ }
+
+ /// The cache data's structure.
+ struct LocalizedCache {
+
+ // swiftlint:disable discouraged_optional_collection
+ /// The content of the yml file.
+ var content: String?
+ /// The file's content parsed as a dictionary.
+ var dictionary: [String: Any]?
+ /// The languages defined in the file.
+ var languages: [String]?
+ /// The phrases in the file.
+ var phrases: [Phrase]?
+ // swiftlint:enable discouraged_optional_collection
+
+ /// Reset the cache.
+ mutating func reset() {
+ content = nil
+ dictionary = nil
+ languages = nil
+ phrases = nil
+ }
+
+ }
+
+ /// Get the available translations for a language.
+ /// - Parameter language: The language.
+ /// - Returns: The keys and translations in the given language for the phrases.
+ public static func translations(for language: String) -> [(String, [Translation])] {
+ phrases.map { ($0.key, $0.translations.filter { $0.language == language }) }
+ }
+
+ /// Get the percentage of completion for a language.
+ /// - Parameter language: The language.
+ /// - Returns: The percentage.
+ public static func completion(for language: String) -> Int {
+ Int(Double(translations(for: language).filter { !$0.1.isEmpty }.count) / .init(phrases.count) * 100)
+ }
+
+ /// Add a new language.
+ /// - Parameter language: The language.
+ public static func addLanguage(_ language: String) throws {
+ let content = emptyPrefix + language + "\n" + content
+ guard let url else {
+ return
+ }
+ try content.write(to: url, atomically: true, encoding: String.Encoding.utf8)
+ updateViews()
+ }
+
+ /// Add a new phrase.
+ /// - Parameters:
+ /// - id: The phrase's identifier.
+ /// - defaultTranslation: The translation in the default language.
+ /// - arguments: The arguments for the translation.
+ public static func addPhrase(id: String, default defaultTranslation: String, arguments: [String]) throws {
+ var content = content
+ var argumentsCode = arguments.joined(separator: ", ")
+ if !arguments.isEmpty {
+ argumentsCode = "(\(argumentsCode))"
+ }
+ content.append("""
+
+ \(id)\(argumentsCode):
+ \(defaultLanguage): "\(defaultTranslation.parseInput())"
+
+ """)
+ guard let url else {
+ return
+ }
+ try content.write(to: url, atomically: true, encoding: .utf8)
+ updateViews()
+ }
+
+ /// Update a translation.
+ /// - Parameters:
+ /// - phrase: The identifier of the phrase.
+ /// - lang: The identifier of the language.
+ /// - conditions: The new conditions for the translation.
+ /// - previousConditions: The conditions for the translation before the update.
+ /// - translation: The translation string.
+ public static func updateTranslation(
+ phrase: String,
+ lang: String,
+ translation: String,
+ conditions: [String] = [],
+ previousConditions: [String] = []
+ ) throws {
+ var newPhrases = phrases
+ if let index = newPhrases.firstIndex(where: { $0.id == phrase }) {
+ if let translationIndex = newPhrases[index].translations.firstIndex(
+ where: { $0.language == lang && $0.conditions == previousConditions }
+ ) {
+ newPhrases[index].translations[translationIndex].conditions = conditions
+ newPhrases[index].translations[translationIndex].translation = translation
+ } else {
+ newPhrases[index].translations.append(
+ .init(language: lang, conditions: conditions, translation: translation)
+ )
+ }
+ }
+ var content = ""
+ content += """
+ default: \(defaultLanguage)
+
+
+ """
+ for phrase in newPhrases {
+ content += """
+ \(phrase.code)
+
+ """
+ }
+ content.removeLast()
+ guard let url else {
+ return
+ }
+ try content.write(to: url, atomically: true, encoding: .utf8)
+ updateViews()
+ }
+
+ /// Parse a translation string from the yml file.
+ /// - Parameter value: The string.
+ /// - Returns: The parsed string.
+ static func parseValue(_ value: String) -> String {
+ value
+ .replacingOccurrences(of: "\\\"", with: "\"")
+ }
+
+ /// Parse the key for a phrase.
+ /// - Parameter key: The key definition including parameters.
+ /// - Returns: The key.
+ static func parseKey(_ key: String) -> (key: String, arguments: [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)
+ }
+
+ static func updateViews() {
+ cache.reset()
+ updateClosure()
+ }
+
+}
+
+/// A short form for the localized information.
+public typealias LocInfo = LocalizedInformation
diff --git a/Sources/Model/Phrase.swift b/Sources/Model/Phrase.swift
new file mode 100644
index 0000000..8504db5
--- /dev/null
+++ b/Sources/Model/Phrase.swift
@@ -0,0 +1,58 @@
+//
+// Phrase.swift
+// Localizer
+//
+
+import Foundation
+
+/// A piece of text to be translated.
+public struct Phrase: Identifiable {
+
+ /// The phrase's yml key.
+ public var key: String
+ /// The phrase's arguments.
+ public var arguments: [String]
+ /// The phrase's translations.
+ public var translations: [Translation]
+
+ /// A unique identifier.
+ public var id: String { key }
+
+ /// The languages with translations for the phrase.
+ public var languages: [String] {
+ translations.compactMap { $0.language }
+ }
+ /// The languages without empty translations.
+ public var filteredTranslations: [Translation] {
+ translations.filter { !$0.translation.isEmpty }
+ }
+
+ /// The code for the phrase.
+ public var code: String {
+ var arguments = ""
+ for argument in self.arguments {
+ arguments += argument + ", "
+ }
+ if !arguments.isEmpty {
+ arguments.removeLast(2)
+ arguments = "(\(arguments))"
+ }
+ var content = """
+ \(key)\(arguments):
+
+ """
+ for translation in translations.sorted(by: { translation1, translation2 in
+ if translation1.language == translation2.language {
+ return translation1.conditions.count < translation2.conditions.count
+ }
+ return translation1.language < translation2.language
+ }) {
+ content.append("""
+ \(translation.code)
+
+ """)
+ }
+ return content
+ }
+
+}
diff --git a/Sources/Model/Translation.swift b/Sources/Model/Translation.swift
new file mode 100644
index 0000000..1ec8908
--- /dev/null
+++ b/Sources/Model/Translation.swift
@@ -0,0 +1,36 @@
+//
+// Translation.swift
+// Localizer
+//
+
+import Foundation
+
+/// A translation.
+public struct Translation: Identifiable {
+
+ /// The translation's language.
+ public var language: String
+ /// The conditions for the translation.
+ public var conditions: [String]
+ /// The translation string.
+ public var translation: String
+
+ /// A unique identifier representing the translation.
+ public var id: String {
+ language + "::" + conditions.joined(separator: "::")
+ }
+
+ /// The yml syntax for the translation.
+ public var code: String {
+ var conditions = ""
+ for condition in self.conditions {
+ conditions += condition + ", "
+ }
+ if !conditions.isEmpty {
+ conditions.removeLast(2)
+ conditions = "(\(conditions))"
+ }
+ return "\(language)\(conditions): \"\(translation.parseInput())\""
+ }
+
+}
diff --git a/Sources/ToolbarView.swift b/Sources/ToolbarView.swift
deleted file mode 100644
index e542dc3..0000000
--- a/Sources/ToolbarView.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-import Adwaita
-
-struct ToolbarView: View {
-
- var app: GTUIApp
- var window: GTUIApplicationWindow
-
- var view: Body {
- HeaderBar.end {
- Menu(icon: .default(icon: .openMenu), app: app, window: window) {
- MenuButton("New Window", window: false) {
- app.addWindow("main")
- }
- .keyboardShortcut("n".ctrl())
- MenuButton("Close Window") {
- window.close()
- }
- .keyboardShortcut("w".ctrl())
- MenuSection {
- MenuButton("Quit", window: false) {
- app.quit()
- }
- .keyboardShortcut("q".ctrl())
- }
- }
- }
- }
-
-}
diff --git a/data/icons/about-symbolic.svg b/data/icons/about-symbolic.svg
new file mode 100644
index 0000000..6ad5ddb
--- /dev/null
+++ b/data/icons/about-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/icons/banner.svg b/data/icons/banner.svg
new file mode 100644
index 0000000..7d9d180
--- /dev/null
+++ b/data/icons/banner.svg
@@ -0,0 +1,56 @@
+
+
+
+
diff --git a/data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg b/data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg
deleted file mode 100644
index fd68537..0000000
--- a/data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
diff --git a/data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg b/data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg
deleted file mode 100644
index e695e81..0000000
--- a/data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
diff --git a/data/icons/io.github.AparokshaUI.Localizer-symbolic.svg b/data/icons/io.github.AparokshaUI.Localizer-symbolic.svg
new file mode 100644
index 0000000..4ede3e4
--- /dev/null
+++ b/data/icons/io.github.AparokshaUI.Localizer-symbolic.svg
@@ -0,0 +1,38 @@
+
+
diff --git a/data/icons/io.github.AparokshaUI.AdwaitaTemplate.Source.svg b/data/icons/io.github.AparokshaUI.Localizer.Source.svg
similarity index 80%
rename from data/icons/io.github.AparokshaUI.AdwaitaTemplate.Source.svg
rename to data/icons/io.github.AparokshaUI.Localizer.Source.svg
index d61a069..6e04f74 100644
--- a/data/icons/io.github.AparokshaUI.AdwaitaTemplate.Source.svg
+++ b/data/icons/io.github.AparokshaUI.Localizer.Source.svg
@@ -9,12 +9,13 @@
height="152"
id="svg11300"
sodipodi:version="0.32"
- inkscape:version="1.3.1 (91b66b0783, 2023-11-16)"
- sodipodi:docname="io.github.AparokshaUI.AdwaitaTemplate.Source.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
+ sodipodi:docname="io.github.AparokshaUI.Localizer.Source.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0"
style="display:inline;enable-background:new"
viewBox="0 0 192 152"
+ xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -22,52 +23,29 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
- xmlns:dc="http://purl.org/dc/elements/1.1/">
- Adwaita Icon Template
-
-
-
-
-
- Adwaita Icon Template
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
- GNOME Design Team
-
-
-
- Adwaita Icon Template
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xmlGNOME Design TeamAdwaita Icon Template
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hicolor
- HicolorSymbolic
- Symbolic
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- battery is full and there is no a/c connected.
- battery is full and there is no a/c connected.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ x="0" />
diff --git a/data/icons/io.github.AparokshaUI.Localizer.svg b/data/icons/io.github.AparokshaUI.Localizer.svg
new file mode 100644
index 0000000..d418306
--- /dev/null
+++ b/data/icons/io.github.AparokshaUI.Localizer.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/data/icons/screenshot.png b/data/icons/screenshot.png
new file mode 100644
index 0000000..2fd7b9b
Binary files /dev/null and b/data/icons/screenshot.png differ
diff --git a/data/io.github.AparokshaUI.AdwaitaTemplate.desktop b/data/io.github.AparokshaUI.Localizer.desktop
similarity index 62%
rename from data/io.github.AparokshaUI.AdwaitaTemplate.desktop
rename to data/io.github.AparokshaUI.Localizer.desktop
index abb4dac..8e160b1 100644
--- a/data/io.github.AparokshaUI.AdwaitaTemplate.desktop
+++ b/data/io.github.AparokshaUI.Localizer.desktop
@@ -2,10 +2,10 @@
Version=1.0
Type=Application
-Name=Adwaita Template
+Name=Localizer
Comment=A template for creating GNOME apps with Swift
Categories=Development;GNOME;
-Icon=io.github.AparokshaUI.AdwaitaTemplate
-Exec=AdwaitaTemplate
+Icon=io.github.AparokshaUI.Localizer
+Exec=Localizer
Terminal=false
\ No newline at end of file
diff --git a/data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml b/data/io.github.AparokshaUI.Localizer.metainfo.xml
similarity index 73%
rename from data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml
rename to data/io.github.AparokshaUI.Localizer.metainfo.xml
index 4418220..871001e 100644
--- a/data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml
+++ b/data/io.github.AparokshaUI.Localizer.metainfo.xml
@@ -1,25 +1,25 @@
- io.github.AparokshaUI.AdwaitaTemplate
-
- Adwaita Template
+ io.github.AparokshaUI.Localizer
+
+ Localizer
A template for creating GNOME apps with Swift
-
+
MIT
LGPL-3.0-or-later
-
+
pointing
keyboard
touch
-
+
This is a long description of this project. Yes - that is required!
-
- io.github.AparokshaUI.AdwaitaTemplate.desktop
+
+ io.github.AparokshaUI.Localizer.desktop
diff --git a/io.github.AparokshaUI.AdwaitaTemplate.json b/io.github.AparokshaUI.AdwaitaTemplate.json
deleted file mode 100644
index 075133e..0000000
--- a/io.github.AparokshaUI.AdwaitaTemplate.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "app-id": "io.github.AparokshaUI.AdwaitaTemplate",
- "runtime": "org.gnome.Platform",
- "runtime-version": "45",
- "sdk": "org.gnome.Sdk",
- "sdk-extensions": [
- "org.freedesktop.Sdk.Extension.swift5"
- ],
- "command": "AdwaitaTemplate",
- "finish-args": [
- "--share=ipc",
- "--socket=fallback-x11",
- "--device=dri",
- "--socket=wayland"
- ],
- "build-options": {
- "append-path": "/usr/lib/sdk/swift5/bin",
- "prepend-ld-library-path": "/usr/lib/sdk/swift5/lib"
- },
- "cleanup": [
- "/include",
- "/lib/pkgconfig",
- "/man",
- "/share/doc",
- "/share/gtk-doc",
- "/share/man",
- "/share/pkgconfig",
- "*.la",
- "*.a"
- ],
- "modules": [
- {
- "name": "AdwaitaTemplate",
- "builddir": true,
- "buildsystem": "simple",
- "sources": [
- {
- "type": "git",
- "url": "https://github.com/AparokshaUI/AdwaitaTemplate",
- "branch": "main"
- }
- ],
- "build-commands": [
- "swift build -c release --static-swift-stdlib",
- "install -Dm755 .build/release/AdwaitaTemplate /app/bin/AdwaitaTemplate",
- "install -Dm644 data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml $DESTDIR/app/share/metainfo/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml",
- "install -Dm644 data/io.github.AparokshaUI.AdwaitaTemplate.desktop $DESTDIR/app/share/applications/io.github.AparokshaUI.AdwaitaTemplate.desktop",
- "install -Dm644 data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg $DESTDIR/app/share/icons/hicolor/scalable/apps/io.github.AparokshaUI.AdwaitaTemplate.svg",
- "install -Dm644 data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg $DESTDIR/app/share/icons/hicolor/symbolic/apps/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg"
- ]
- }
- ]
-}
diff --git a/io.github.AparokshaUI.Localizer.json b/io.github.AparokshaUI.Localizer.json
new file mode 100644
index 0000000..4a8a272
--- /dev/null
+++ b/io.github.AparokshaUI.Localizer.json
@@ -0,0 +1,53 @@
+{
+ "app-id": "io.github.AparokshaUI.Localizer",
+ "runtime": "org.gnome.Platform",
+ "runtime-version": "46",
+ "sdk": "org.gnome.Sdk",
+ "sdk-extensions": [
+ "org.freedesktop.Sdk.Extension.swift5"
+ ],
+ "command": "Localizer",
+ "finish-args": [
+ "--share=ipc",
+ "--socket=fallback-x11",
+ "--device=dri",
+ "--socket=wayland"
+ ],
+ "build-options": {
+ "append-path": "/usr/lib/sdk/swift5/bin",
+ "prepend-ld-library-path": "/usr/lib/sdk/swift5/lib"
+ },
+ "cleanup": [
+ "/include",
+ "/lib/pkgconfig",
+ "/man",
+ "/share/doc",
+ "/share/gtk-doc",
+ "/share/man",
+ "/share/pkgconfig",
+ "*.la",
+ "*.a"
+ ],
+ "modules": [
+ {
+ "name": "Localizer",
+ "builddir": true,
+ "buildsystem": "simple",
+ "sources": [
+ {
+ "type": "dir",
+ "path": "."
+ }
+ ],
+ "build-commands": [
+ "swift build -c release --static-swift-stdlib",
+ "install -Dm755 .build/release/Localizer /app/bin/Localizer",
+ "install -Dm644 data/io.github.AparokshaUI.Localizer.metainfo.xml $DESTDIR/app/share/metainfo/io.github.AparokshaUI.Localizer.metainfo.xml",
+ "install -Dm644 data/io.github.AparokshaUI.Localizer.desktop $DESTDIR/app/share/applications/io.github.AparokshaUI.Localizer.desktop",
+ "install -Dm644 data/icons/io.github.AparokshaUI.Localizer.svg $DESTDIR/app/share/icons/hicolor/scalable/apps/io.github.AparokshaUI.Localizer.svg",
+ "install -Dm644 data/icons/io.github.AparokshaUI.Localizer-symbolic.svg $DESTDIR/app/share/icons/hicolor/symbolic/apps/io.github.AparokshaUI.Localizer-symbolic.svg",
+ "install -Dm644 data/icons/about-symbolic.svg $DESTDIR/app/share/icons/hicolor/symbolic/apps/io.github.AparokshaUI.Localizer.about-symbolic.svg"
+ ]
+ }
+ ]
+}