diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 338f1d0..e008705 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,4 +28,4 @@ jobs: - name: Build the macOS app without signing working-directory: ./Lingua-App/Lingua run: | - xcodebuild -project Lingua.xcodeproj -scheme LinguaApp clean build CODE_SIGNING_ALLOWED=NO + xcodebuild -project Lingua.xcodeproj -scheme LinguaApp clean build CODE_SIGNING_ALLOWED=NO | xcpretty diff --git a/Example-App/iOS/Example-App/Example-App/ContentView.swift b/Example-App/iOS/Example-App/Example-App/ContentView.swift index f31d934..0126c3b 100644 --- a/Example-App/iOS/Example-App/Example-App/ContentView.swift +++ b/Example-App/iOS/Example-App/Example-App/ContentView.swift @@ -5,7 +5,3 @@ struct ContentView: View { Text(Lingua.General.success).padding() } } - -#Preview { - ContentView() -} diff --git a/Lingua-App/Lingua/Lingua.xcodeproj/project.pbxproj b/Lingua-App/Lingua/Lingua.xcodeproj/project.pbxproj index 71e7188..a10ebff 100644 --- a/Lingua-App/Lingua/Lingua.xcodeproj/project.pbxproj +++ b/Lingua-App/Lingua/Lingua.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 774A1A462A8E588500789A04 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A1A452A8E588500789A04 /* ContentView.swift */; }; 774A1A482A8E588800789A04 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 774A1A472A8E588800789A04 /* Assets.xcassets */; }; 774A1A562A8E588800789A04 /* LinguaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A1A552A8E588800789A04 /* LinguaTests.swift */; }; + 777814B02B13D5C700A1B678 /* SectionsInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777814AF2B13D5C700A1B678 /* SectionsInputView.swift */; }; 777BC5E12A93617000C74D72 /* ValidatingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BC5E02A93617000C74D72 /* ValidatingTextField.swift */; }; 777BC5E42A9361E200C74D72 /* ValidationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BC5E32A9361E200C74D72 /* ValidationRule.swift */; }; 777BC5E62A9361F000C74D72 /* RequiredRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BC5E52A9361F000C74D72 /* RequiredRule.swift */; }; @@ -39,6 +40,9 @@ 77F89A482A98DF3A009EE7B1 /* CustomSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F89A472A98DF3A009EE7B1 /* CustomSearchBar.swift */; }; 77FB97662ADC3928004CED84 /* LinguaAppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FB97652ADC3928004CED84 /* LinguaAppCommands.swift */; }; 77FB97692ADC3A8B004CED84 /* ProjectMenu.strings in Resources */ = {isa = PBXBuildFile; fileRef = 77FB97672ADC3A8B004CED84 /* ProjectMenu.strings */; }; + 970D1F7A2B10CB5D00F6CD8E /* ProjectListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D1F792B10CB5D00F6CD8E /* ProjectListView.swift */; }; + 970D1F7C2B10CBE900F6CD8E /* ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D1F7B2B10CBE900F6CD8E /* ConditionalModifier.swift */; }; + 970D1F802B10EC3F00F6CD8E /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D1F7F2B10EC3F00F6CD8E /* Window.swift */; }; 9941DA812ABE33140098B49A /* App.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9941DA7F2ABE33140098B49A /* App.strings */; }; /* End PBXBuildFile section */ @@ -62,6 +66,7 @@ 774A1A4C2A8E588800789A04 /* Lingua.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Lingua.entitlements; sourceTree = ""; }; 774A1A512A8E588800789A04 /* LinguaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LinguaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 774A1A552A8E588800789A04 /* LinguaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinguaTests.swift; sourceTree = ""; }; + 777814AF2B13D5C700A1B678 /* SectionsInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionsInputView.swift; sourceTree = ""; }; 777BC5E02A93617000C74D72 /* ValidatingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatingTextField.swift; sourceTree = ""; }; 777BC5E32A9361E200C74D72 /* ValidationRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationRule.swift; sourceTree = ""; }; 777BC5E52A9361F000C74D72 /* RequiredRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredRule.swift; sourceTree = ""; }; @@ -89,6 +94,9 @@ 77F89A472A98DF3A009EE7B1 /* CustomSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSearchBar.swift; sourceTree = ""; }; 77FB97652ADC3928004CED84 /* LinguaAppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinguaAppCommands.swift; sourceTree = ""; }; 77FB97682ADC3A8B004CED84 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ProjectMenu.strings; sourceTree = ""; }; + 970D1F792B10CB5D00F6CD8E /* ProjectListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectListView.swift; sourceTree = ""; }; + 970D1F7B2B10CBE900F6CD8E /* ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifier.swift; sourceTree = ""; }; + 970D1F7F2B10EC3F00F6CD8E /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; 9941DA802ABE33140098B49A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/App.strings; sourceTree = ""; }; /* End PBXFileReference section */ @@ -199,6 +207,7 @@ 777BC62E2A94B2A000C74D72 /* ProgressOverlay.swift */, 779F91F82A978AC8009C77D5 /* InformationHolderView.swift */, 77F89A472A98DF3A009EE7B1 /* CustomSearchBar.swift */, + 777814AF2B13D5C700A1B678 /* SectionsInputView.swift */, ); path = UI; sourceTree = ""; @@ -216,6 +225,7 @@ isa = PBXGroup; children = ( 777BC5EB2A93707B00C74D72 /* DirectoryAccessor.swift */, + 970D1F7B2B10CBE900F6CD8E /* ConditionalModifier.swift */, ); path = Helpers; sourceTree = ""; @@ -278,6 +288,7 @@ 77CF0A3E2A8E61EE00AB5A2D /* ProjectItemView.swift */, 77CF0A452A8E62F300AB5A2D /* ProjectsView.swift */, 77CF0A4A2A8F54B400AB5A2D /* ProjectsViewModel.swift */, + 970D1F792B10CB5D00F6CD8E /* ProjectListView.swift */, ); path = Projects; sourceTree = ""; @@ -296,6 +307,7 @@ children = ( 774A1A432A8E588500789A04 /* LinguaApp.swift */, 77FB97652ADC3928004CED84 /* LinguaAppCommands.swift */, + 970D1F7F2B10EC3F00F6CD8E /* Window.swift */, ); path = App; sourceTree = ""; @@ -413,10 +425,12 @@ 778E9F162B03B4E20063B50F /* ProjectFormViewModel.swift in Sources */, 777BC5EC2A93707B00C74D72 /* DirectoryAccessor.swift in Sources */, 779F91FB2A97A7B0009C77D5 /* Date+Extension.swift in Sources */, + 970D1F7C2B10CBE900F6CD8E /* ConditionalModifier.swift in Sources */, 77CF0A462A8E62F300AB5A2D /* ProjectsView.swift in Sources */, 77CF0A3F2A8E61EE00AB5A2D /* ProjectItemView.swift in Sources */, 77CF0A4B2A8F54B400AB5A2D /* ProjectsViewModel.swift in Sources */, 779F91F72A977378009C77D5 /* URL+Extension.swift in Sources */, + 970D1F7A2B10CB5D00F6CD8E /* ProjectListView.swift in Sources */, 774A1A462A8E588500789A04 /* ContentView.swift in Sources */, 777BC5E62A9361F000C74D72 /* RequiredRule.swift in Sources */, 779F91F92A978AC8009C77D5 /* InformationHolderView.swift in Sources */, @@ -430,6 +444,8 @@ 777BC5EF2A938B8400C74D72 /* ProjectFormView.swift in Sources */, 774A1A442A8E588500789A04 /* LinguaApp.swift in Sources */, 777BC5E82A93639E00C74D72 /* DirectoryInputField.swift in Sources */, + 777814B02B13D5C700A1B678 /* SectionsInputView.swift in Sources */, + 970D1F802B10EC3F00F6CD8E /* Window.swift in Sources */, 777BC62F2A94B2A000C74D72 /* ProgressOverlay.swift in Sources */, 777BC5E42A9361E200C74D72 /* ValidationRule.swift in Sources */, 777BC5E12A93617000C74D72 /* ValidatingTextField.swift in Sources */, @@ -631,7 +647,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -647,7 +663,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.povio.Lingua; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -667,7 +683,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -683,7 +699,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.povio.Lingua; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Lingua-App/Lingua/Lingua/App/LinguaApp.swift b/Lingua-App/Lingua/Lingua/App/LinguaApp.swift index e39aba2..e75438b 100644 --- a/Lingua-App/Lingua/Lingua/App/LinguaApp.swift +++ b/Lingua-App/Lingua/Lingua/App/LinguaApp.swift @@ -9,13 +9,14 @@ import SwiftUI @main struct LinguaApp: App { + @Environment(\.openWindow) private var openWindow @StateObject var viewModel = ProjectsViewModel() var commands: LinguaAppCommands { LinguaAppCommands(viewModel: viewModel) } var body: some Scene { - WindowGroup { + WindowGroup(id: Window.main.rawValue) { ContentView() .environmentObject(viewModel) } @@ -23,5 +24,34 @@ struct LinguaApp: App { commands.aboutApp() commands.projectCommands } + + MenuBarExtra(String.packageName, image: "lingua_menu_bar_icon") { + VStack(spacing: 0) { + ProjectListView(shouldAddLocalizeButton: true) + .environmentObject(viewModel) + + Divider() + + HStack(alignment: .center) { + Button(Lingua.App.settings) { + openMainWindow() + } + .frame(height: 26) + .buttonStyle(.plain) + .padding(.horizontal, 16) + .padding(.vertical, 4) + + Spacer() + } + } + } + .menuBarExtraStyle(.window) + } + + func openMainWindow() { + // Don't open the main app if the window is already opened + guard let windows = NSWindow.windowNumbers(), + windows.count <= 3 else { return } + openWindow(id: Window.main.rawValue) } } diff --git a/Lingua-App/Lingua/Lingua/App/Window.swift b/Lingua-App/Lingua/Lingua/App/Window.swift new file mode 100644 index 0000000..76f1fb9 --- /dev/null +++ b/Lingua-App/Lingua/Lingua/App/Window.swift @@ -0,0 +1,12 @@ +// +// Window.swift +// Lingua +// +// Created by Yll Fejziu on 24/11/2023. +// + +import Foundation + +enum Window: String { + case main +} diff --git a/Lingua-App/Lingua/Lingua/Model/Project.swift b/Lingua-App/Lingua/Lingua/Model/Project.swift index c0fd4de..83063d0 100644 --- a/Lingua-App/Lingua/Lingua/Model/Project.swift +++ b/Lingua-App/Lingua/Lingua/Model/Project.swift @@ -16,9 +16,11 @@ struct Project: Identifiable, Hashable, Equatable, Codable { var directoryPath: String var title: String var swiftCode: SwiftCode - var swiftCodeEnabled: Bool = true + var swiftCodeEnabled: Bool var createdAt: Date var lastLocalizedAt: Date? + var filterSectionsEnabled: Bool + var allowedSections: [String] init(id: UUID, type: LocalizationPlatform, @@ -27,8 +29,11 @@ struct Project: Identifiable, Hashable, Equatable, Codable { directoryPath: String = "", title: String = "", swiftCode: SwiftCode = .init(stringsDirectory: "", outputSwiftCodeFileDirectory: ""), + swiftCodeEnabled: Bool = true, createdAt: Date = .init(), - lastLocalizedAt: Date? = nil) { + lastLocalizedAt: Date? = nil, + filterSectionsEnabled: Bool = false, + allowedSections: [String] = []) { self.id = id self.type = type self.apiKey = apiKey @@ -36,8 +41,11 @@ struct Project: Identifiable, Hashable, Equatable, Codable { self.directoryPath = directoryPath self.title = title self.swiftCode = swiftCode + self.swiftCodeEnabled = swiftCodeEnabled self.createdAt = createdAt self.lastLocalizedAt = lastLocalizedAt + self.filterSectionsEnabled = filterSectionsEnabled + self.allowedSections = allowedSections } /// Custom initializer for decoding [Project] from persisted storage. @@ -57,6 +65,8 @@ struct Project: Identifiable, Hashable, Equatable, Codable { swiftCodeEnabled = try container.decode(Bool.self, forKey: .swiftCodeEnabled) createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date() lastLocalizedAt = try container.decodeIfPresent(Date.self, forKey: .lastLocalizedAt) + filterSectionsEnabled = try container.decodeIfPresent(Bool.self, forKey: .filterSectionsEnabled) ?? false + allowedSections = try container.decodeIfPresent([String].self, forKey: .allowedSections) ?? [] } } diff --git a/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/Contents.json b/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/Contents.json new file mode 100644 index 0000000..e6673bb --- /dev/null +++ b/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lingua_menu_bar_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/lingua_menu_bar_icon.svg b/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/lingua_menu_bar_icon.svg new file mode 100644 index 0000000..3634ab8 --- /dev/null +++ b/Lingua-App/Lingua/Lingua/Resources/Assets.xcassets/lingua_menu_bar_icon.imageset/lingua_menu_bar_icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Lingua-App/Lingua/Lingua/Resources/Localization/Lingua.swift b/Lingua-App/Lingua/Lingua/Resources/Localization/Lingua.swift index 60a1cfb..00b94d0 100644 --- a/Lingua-App/Lingua/Lingua/Resources/Localization/Lingua.swift +++ b/Lingua-App/Lingua/Lingua/Resources/Localization/Lingua.swift @@ -14,6 +14,8 @@ enum Lingua { static let copyrightYear = tr("App", "copyright_year") /// A unified localization management tool for iOS & Android static let description = tr("App", "description") + /// Lingua Settings... + static let settings = tr("App", "settings") } enum General { @@ -40,6 +42,14 @@ enum Lingua { static let apiKeyHelp = tr("ProjectForm", "api_key_help") /// Configuration static let configurationSection = tr("ProjectForm", "configuration_section") + /// Add section + static let filteringAddSectionButtonTitle = tr("ProjectForm", "filtering_add_section_button_title") + /// Add the sections that you want to include into the project, otherwise if it is disabled all the sections will be included + static let filteringSectionDescription = tr("ProjectForm", "filtering_section_description") + /// Enter a section + static let filteringSectionTextfieldPlaceholder = tr("ProjectForm", "filtering_section_textfield_placeholder") + /// Enable sections filtering + static let filteringSectionTitle = tr("ProjectForm", "filtering_section_title") /// Info static let infoHeader = tr("ProjectForm", "info_header") /// API Key * diff --git a/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/App.strings b/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/App.strings index 06467eb..4e01298 100644 --- a/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/App.strings +++ b/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/App.strings @@ -5,3 +5,4 @@ "description" = "A unified localization management tool for iOS & Android"; "copyright" = "Copyright"; "copyright_year" = "© 2023 Povio Inc."; +"settings" = "Lingua Settings..."; diff --git a/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/ProjectForm.strings b/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/ProjectForm.strings index b6db990..172c7f7 100644 --- a/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/ProjectForm.strings +++ b/Lingua-App/Lingua/Lingua/Resources/Localization/en.lproj/ProjectForm.strings @@ -47,3 +47,7 @@ https://docs.google.com/spreadsheets/d/ 1GpaPpO4JMleZPd8paSW4qPBQxjImm2xD8yJhvZO It serves as base language directory from where the Lingua.swift file will be created"; "lingua_swift_output_directory_help" = "This should be the directory where you want to store the generated Lingua.swift file"; "last_localized_subtitle" = "Last localized: %@"; +"filtering_section_title" = "Enable sections filtering"; +"filtering_section_description" = "Add the sections that you want to include into the project, otherwise if it is disabled all the sections will be included"; +"filtering_section_textfield_placeholder" = "Enter a section"; +"filtering_add_section_button_title" = "Add section"; diff --git a/Lingua-App/Lingua/Lingua/Scenes/ProjectFormView/ProjectFormView.swift b/Lingua-App/Lingua/Lingua/Scenes/ProjectFormView/ProjectFormView.swift index b9602f3..83b5e17 100644 --- a/Lingua-App/Lingua/Lingua/Scenes/ProjectFormView/ProjectFormView.swift +++ b/Lingua-App/Lingua/Lingua/Scenes/ProjectFormView/ProjectFormView.swift @@ -28,6 +28,7 @@ struct ProjectFormView: View { Form { basicConfigurationFormSection() swiftCodeFormSection() + filterSectionsFormSection() iOSInfoFormSection() } .toolbar { @@ -157,6 +158,25 @@ private extension ProjectFormView { } } + @ViewBuilder + func filterSectionsFormSection() -> some View { + Section { + Toggle(isOn: $viewModel.project.filterSectionsEnabled) { + Text(Lingua.ProjectForm.filteringSectionTitle) + .bold() + } + if viewModel.project.filterSectionsEnabled { + VStack(alignment: .leading, spacing: 8) { + Text(Lingua.ProjectForm.filteringSectionDescription) + .font(.subheadline) + Divider() + SectionsInputView(sections: $viewModel.project.allowedSections) + } + .padding(8) + } + } + } + @ViewBuilder func iOSInfoFormSection() -> some View { if viewModel.project.type == .ios { diff --git a/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectListView.swift b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectListView.swift new file mode 100644 index 0000000..a615b98 --- /dev/null +++ b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectListView.swift @@ -0,0 +1,103 @@ +// +// ProjectListView.swift +// Lingua +// +// Created by Yll Fejziu on 24/11/2023. +// + +import SwiftUI + +struct ProjectListView: View { + @EnvironmentObject private var viewModel: ProjectsViewModel + var shouldAddLocalizeButton: Bool = false + + var body: some View { + CustomSearchBar(searchTerm: $viewModel.searchTerm) + + List(viewModel.filteredProjects, selection: $viewModel.selectedProjectId) { project in + HStack { + ProjectItemView(project: project) + .swipeActions(edge: .trailing) { + duplicateButton(for: project) + .shouldAddView(!shouldAddLocalizeButton) + deletionButton(for: project) + .shouldAddView(!shouldAddLocalizeButton) + } + .contextMenu { + duplicateButton(for: project) + .shouldAddView(!shouldAddLocalizeButton) + deletionButton(for: project) + .shouldAddView(!shouldAddLocalizeButton) + } + + Button(action: { + Task { await viewModel.localizeProject(project) } + }) { + HStack { + Image(systemName: "globe") + Text(Lingua.ProjectForm.localizeButton) + } + } + .shouldAddView(shouldAddLocalizeButton) + } + } + .navigationSplitViewColumnWidth(min: 340, ideal: 340, max: 500) + .listStyle(DefaultListStyle()) + .toolbar { + Button(action: { + withAnimation { + viewModel.createNewProject() + } + }) { + Image(systemName: "plus") + } + .shouldAddView(!shouldAddLocalizeButton) + } + .overlay { + ProgressView() + .shouldAddView(shouldAddLocalizeButton && viewModel.isLocalizing) + } + .disabled(viewModel.isLocalizing) + .opacity(viewModel.isLocalizing ? 0.5 : 1) + } + + @ViewBuilder + func hudResultOverlay() -> some View { + switch viewModel.localizationResult { + case .success(let message): + HUDOverlay(message: message, isError: false) { + viewModel.localizationResult = nil + } + case .failure(let error): + HUDOverlay(message: error.localizedDescription, isError: true) { + viewModel.localizationResult = nil + } + case .none: + EmptyView() + } + } +} + +extension ProjectListView { + @ViewBuilder + func deletionButton(for project: Project) -> some View { + Button(action: { + viewModel.confirmDelete(for: project) + }) { + Text(Lingua.General.delete) + Image(systemName: "trash") + } + .tint(.red) + } + + @ViewBuilder + func duplicateButton(for project: Project) -> some View { + Button(action: { + viewModel.duplicate(project) + }) { + Text(Lingua.General.duplicate) + Image(systemName: "doc.on.doc") + } + .tint(.blue) + } +} diff --git a/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsView.swift b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsView.swift index eee7b9c..983f729 100644 --- a/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsView.swift +++ b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsView.swift @@ -21,30 +21,8 @@ struct ProjectsView: View { Spacer() } } content: { - CustomSearchBar(searchTerm: $viewModel.searchTerm) - - List(viewModel.filteredProjects, selection: $viewModel.selectedProjectId) { project in - ProjectItemView(project: project) - .swipeActions(edge: .trailing) { - duplicateButton(for: project) - deletionButton(for: project) - } - .contextMenu { - duplicateButton(for: project) - deletionButton(for: project) - } - } - .navigationSplitViewColumnWidth(min: 340, ideal: 340, max: 500) - .listStyle(DefaultListStyle()) - .toolbar { - Button(action: { - withAnimation { - viewModel.createNewProject() - } - }) { - Image(systemName: "plus") - } - } + ProjectListView() + .environmentObject(viewModel) } detail: { if let project = viewModel.selectedProject { projectFormView(for: project) @@ -82,7 +60,7 @@ private extension ProjectsView { viewModel.updateProject(updatedProject) }, onDelete: { deletedProject in - confirmDelete(for: deletedProject) + viewModel.confirmDelete(for: deletedProject) }, onLocalize: { projectToLocalize in Task { await viewModel.localizeProject(projectToLocalize) } @@ -107,28 +85,6 @@ private extension ProjectsView { } } - @ViewBuilder - func deletionButton(for project: Project) -> some View { - Button(action: { - confirmDelete(for: project) - }) { - Text(Lingua.General.delete) - Image(systemName: "trash") - } - .tint(.red) - } - - @ViewBuilder - func duplicateButton(for project: Project) -> some View { - Button(action: { - viewModel.duplicate(project) - }) { - Text(Lingua.General.duplicate) - Image(systemName: "doc.on.doc") - } - .tint(.blue) - } - func deletionAlert() -> Alert { Alert( title: Text(Lingua.Projects.deleteAlertTitle), @@ -139,9 +95,4 @@ private extension ProjectsView { }), secondaryButton: .cancel()) } - - func confirmDelete(for project: Project) { - viewModel.projectToDelete = project - viewModel.showDeleteAlert = true - } } diff --git a/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsViewModel.swift b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsViewModel.swift index 72635b7..14b75d0 100644 --- a/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsViewModel.swift +++ b/Lingua-App/Lingua/Lingua/Scenes/Projects/ProjectsViewModel.swift @@ -106,6 +106,11 @@ extension ProjectsViewModel { isLocalizing = false } } + + func confirmDelete(for project: Project) { + projectToDelete = project + showDeleteAlert = true + } } // MARK: - Private methods diff --git a/Lingua-App/Lingua/Lingua/Utils/Components/UI/SectionsInputView.swift b/Lingua-App/Lingua/Lingua/Utils/Components/UI/SectionsInputView.swift new file mode 100644 index 0000000..019533c --- /dev/null +++ b/Lingua-App/Lingua/Lingua/Utils/Components/UI/SectionsInputView.swift @@ -0,0 +1,53 @@ +// +// TagInputView.swift +// Lingua +// +// Created by Egzon Arifi on 26/11/2023. +// + +import SwiftUI + +struct SectionsInputView: View { + @State var currentInput: String = "" + @Binding var sections: [String] + + var body: some View { + VStack(alignment: .leading) { + HStack { + TextField(Lingua.ProjectForm.filteringSectionTextfieldPlaceholder, text: $currentInput, onCommit: addSection) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.vertical) + + Button(action: addSection) { + Text(Lingua.ProjectForm.filteringAddSectionButtonTitle) + } + } + + Divider() + + HStack { + ForEach(sections, id: \.self) { tag in + Button(action: { + removeSection(tag) + }, label: { + Text(tag) + .padding(.vertical, 4) + + Image(systemName: "xmark") + }) + } + } + } + } + + private func addSection() { + if !currentInput.isEmpty { + sections.append(currentInput) + currentInput = "" + } + } + + private func removeSection(_ tag: String) { + sections.removeAll { $0 == tag } + } +} diff --git a/Lingua-App/Lingua/Lingua/Utils/Helpers/ConditionalModifier.swift b/Lingua-App/Lingua/Lingua/Utils/Helpers/ConditionalModifier.swift new file mode 100644 index 0000000..2202070 --- /dev/null +++ b/Lingua-App/Lingua/Lingua/Utils/Helpers/ConditionalModifier.swift @@ -0,0 +1,24 @@ +// +// ConditionalModifier.swift +// Lingua +// +// Created by Yll Fejziu on 24/11/2023. +// + +import SwiftUI + +struct ConditionalModifier: ViewModifier { + var shouldShow: Bool + + func body(content: Content) -> some View { + if shouldShow { + content + } + } +} + +extension View { + func shouldAddView(_ condition: Bool) -> some View { + self.modifier(ConditionalModifier(shouldShow: condition)) + } +} diff --git a/Lingua-App/Lingua/Lingua/Utils/Manager/LocalizationManager.swift b/Lingua-App/Lingua/Lingua/Utils/Manager/LocalizationManager.swift index 4a36062..a8b2862 100644 --- a/Lingua-App/Lingua/Lingua/Utils/Manager/LocalizationManager.swift +++ b/Lingua-App/Lingua/Lingua/Utils/Manager/LocalizationManager.swift @@ -32,7 +32,8 @@ struct LocalizationManager { let config = Config.Localization(apiKey: project.apiKey, sheetId: project.sheetId, outputDirectory: project.directoryPath, - localizedSwiftCode: localizedSwiftCode) + localizedSwiftCode: localizedSwiftCode, + allowedSections: project.filterSectionsEnabled ? project.allowedSections : .none) let module = LocalizationModuleFactory.make(config: config) try await module.localize(for: project.type) diff --git a/README.md b/README.md index f26d65e..10104a6 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ You can download the macOS app from [App Store](https://apps.apple.com/us/app/li - **Settings Configuration:** Easily configure and manage your settings through the app's settings panel. - **Translation Initiation:** Initiate the translation process with a single click without the need for terminal commands. -### 2. Terminal App +### 2. macOS Terminal App For those who prefer using the terminal or require scriptable solutions, Lingua offers a terminal app that allows you to manage and initiate translations directly from the command line. @@ -107,7 +107,27 @@ $ brew tap poviolabs/lingua $ brew install lingua ``` -#### Configuration file +### 3. Linux Terminal App + +Lingua runs on Linux as well. + +#### Installation + +1. Download the latest release `Lingua_Linux` from [GitHub Releases](https://github.com/poviolabs/Lingua/releases) based on your machine, either `Lingua_Linux_x86_64` or `Lingua_Linux_arm64` + +2. Make the binary executable: + + ```shell + $ chmod +x /path/to/Lingua_Linux_x86_64 + $ mv Lingua_Linux lingua + $ sudo mv /path/to/lingua /usr/local/bin + ``` + +### Terminal Usage + +Please follow below instructions to use Lingua in terminal. + +##### Configuration file Create a configuration file as a starting point to adapt as your needs, `lingua_config.json` or any other `.json` file. @@ -125,7 +145,7 @@ Then in the configuration file created you need to provide your data, like below } ``` -#### Output directory +##### Output directory The output directory property should be the path where you want the tool to create localization files. @@ -133,7 +153,7 @@ The output directory property should be the path where you want the tool to crea * For Android, since the translation are placed in a specific project directory, the output directory it should look something like this: **`path/YourProject/app/src/main/res `** -#### iOS specific +##### iOS specific Since iOS does not have a built in feature to access the localization safely, we have made this possible using Lingua tool. In the configuration file you have to provide the path where the default language strings are stored and where the Swift file you want to be created. With that the tool will create **Lingua.swift** with an enumeration to easily access localizations in your app. diff --git a/Sources/LinguaLib/Common/Extensions/String+Extension.swift b/Sources/LinguaLib/Common/Extensions/String+Extension.swift index c133236..79f3737 100644 --- a/Sources/LinguaLib/Common/Extensions/String+Extension.swift +++ b/Sources/LinguaLib/Common/Extensions/String+Extension.swift @@ -2,7 +2,7 @@ import Foundation public extension String { static let packageName = "Lingua" - static let version = "1.0.1" + static let version = "1.0.3" static let swiftLocalizedName = "\(String.packageName).swift" static let fileHeader = """ This file was generated with Lingua command line tool. Please do not change it! diff --git a/Sources/LinguaLib/Domain/Entities/Config.swift b/Sources/LinguaLib/Domain/Entities/Config.swift index 81fc789..ca58c61 100644 --- a/Sources/LinguaLib/Domain/Entities/Config.swift +++ b/Sources/LinguaLib/Domain/Entities/Config.swift @@ -10,12 +10,20 @@ public extension Config { let sheetId: String let outputDirectory: String let localizedSwiftCode: LocalizedSwiftCode? + let allowedSections: [String]? - public init(apiKey: String, sheetId: String, outputDirectory: String, localizedSwiftCode: LocalizedSwiftCode?) { + public init( + apiKey: String, + sheetId: String, + outputDirectory: String, + localizedSwiftCode: LocalizedSwiftCode?, + allowedSections: [String]? = nil + ) { self.apiKey = apiKey self.sheetId = sheetId self.outputDirectory = outputDirectory self.localizedSwiftCode = localizedSwiftCode + self.allowedSections = allowedSections } } diff --git a/Sources/LinguaLib/Infrastructure/LocalizationGenerator/Generator/LocalizedFilesGenerator.swift b/Sources/LinguaLib/Infrastructure/LocalizationGenerator/Generator/LocalizedFilesGenerator.swift index 1f18069..5fb536e 100644 --- a/Sources/LinguaLib/Infrastructure/LocalizationGenerator/Generator/LocalizedFilesGenerator.swift +++ b/Sources/LinguaLib/Infrastructure/LocalizationGenerator/Generator/LocalizedFilesGenerator.swift @@ -27,6 +27,10 @@ extension LocalizedFilesGenerator: LocalizedFilesGenerating { let sections = Dictionary(grouping: sheet.entries, by: { $0.section }) for (sectionName, sectionEntries) in sections { + if let allowedSections = config.allowedSections, + !allowedSections.contains(where: { $0.lowercased() == sectionName.lowercased() }) { + continue + } try filesGenerator.createPlatformFiles(for: sectionEntries, sectionName: sectionName.formatSheetSection(), outputFolder: outputFolder, diff --git a/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/HTTPClient.swift b/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/HTTPClient.swift index 5065d1a..00da71e 100644 --- a/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/HTTPClient.swift +++ b/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/HTTPClient.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif protocol HTTPClient { func fetchData(with request: URLRequest) async throws -> (Data, HTTPURLResponse) diff --git a/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/URLSessionHTTPClient.swift b/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/URLSessionHTTPClient.swift index 1c04ddd..bf4323d 100644 --- a/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/URLSessionHTTPClient.swift +++ b/Sources/LinguaLib/Infrastructure/Networking/HTTPClient/URLSessionHTTPClient.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif class URLSessionHTTPClient: HTTPClient { private let urlSession: URLSession @@ -8,11 +11,33 @@ class URLSessionHTTPClient: HTTPClient { } func fetchData(with request: URLRequest) async throws -> (Data, HTTPURLResponse) { +#if canImport(FoundationNetworking) + return try await makeData(for: request) +#else let (data, response) = try await urlSession.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw InvalidHTTPResponseError(statusCode: 0, data: data) } - return (data, httpResponse) +#endif + } +} + +private extension URLSessionHTTPClient { + func makeData(for request: URLRequest) async throws -> (Data, HTTPURLResponse) { + try await withCheckedThrowingContinuation { continuation in + let task = urlSession.dataTask(with: request) { data, response, error in + if let error = error { + continuation.resume(throwing: error) + return + } + guard let data = data, let httpResponse = response as? HTTPURLResponse else { + continuation.resume(throwing: URLError(.badServerResponse)) + return + } + continuation.resume(returning: (data, httpResponse)) + } + task.resume() + } } } diff --git a/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/APIRequestExecutor.swift b/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/APIRequestExecutor.swift index d9f62e8..1c8eee6 100644 --- a/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/APIRequestExecutor.swift +++ b/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/APIRequestExecutor.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif final class APIRequestExecutor { private let requestBuilder: URLRequestBuilder diff --git a/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/URLRequestBuilder.swift b/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/URLRequestBuilder.swift index e5102e0..2dae1de 100644 --- a/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/URLRequestBuilder.swift +++ b/Sources/LinguaLib/Infrastructure/Networking/RequestExecutor/URLRequestBuilder.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif struct URLRequestBuilder { let baseURLString: String