From ab30341f593dfae6c40d5f0cb6eddbb57c76c2c9 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Wed, 2 Sep 2026 22:05:53 -0400 Subject: [PATCH 1/3] feat(mac): localization support added localizable types where necessary created Localizable.xcstrings for both the config app and the Keyman settings package --- mac/Config/Config.xcodeproj/project.pbxproj | 6 + mac/Config/Config/AddKeyboardView.swift | 5 +- mac/Config/Config/DownloadCoordinator.swift | 6 +- mac/Config/Config/IconButtonView.swift | 4 +- mac/Config/Config/InstallDebugView.swift | 58 +++-- mac/Config/Config/MainConfigView.swift | 2 +- mac/Config/Localizable.xcstrings | 141 +++++++++++ mac/KeymanSettings/Package.swift | 1 + .../KeymanSettings/Localizable.xcstrings | 237 ++++++++++++++++++ .../KeymanSettings/SettingsContainer.swift | 56 ++++- .../Persistence/PackageInstallHelper.swift | 26 +- .../Persistence/PackageRepository.swift | 82 +++++- 12 files changed, 571 insertions(+), 53 deletions(-) create mode 100644 mac/Config/Localizable.xcstrings create mode 100644 mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings diff --git a/mac/Config/Config.xcodeproj/project.pbxproj b/mac/Config/Config.xcodeproj/project.pbxproj index 1a24c59f002..1eb4f548bf7 100644 --- a/mac/Config/Config.xcodeproj/project.pbxproj +++ b/mac/Config/Config.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ D83272A82F57614400F71698 /* KeymanSettings in Frameworks */ = {isa = PBXBuildFile; productRef = D83272A72F57614400F71698 /* KeymanSettings */; }; D88B79CA30472B3B00F10D14 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D88B79C930472B3B00F10D14 /* Sentry-Dynamic */; }; + D8F10B623048F12A00C1E597 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D8F10B613048F12A00C1E597 /* Localizable.xcstrings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -32,6 +33,7 @@ D88F03C62F50ED5000C02A31 /* Keyman Configuration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Keyman Configuration.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D88F03D32F50ED5100C02A31 /* ConfigTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfigTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D88F03DD2F50ED5100C02A31 /* ConfigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D8F10B613048F12A00C1E597 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -95,6 +97,7 @@ D88F03BD2F50ED5000C02A31 = { isa = PBXGroup; children = ( + D8F10B613048F12A00C1E597 /* Localizable.xcstrings */, D87D6F492FAF95400083A95E /* Installation */, D88F03C82F50ED5000C02A31 /* Config */, D88F03E02F50ED5100C02A31 /* ConfigUITests */, @@ -243,6 +246,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D8F10B623048F12A00C1E597 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -361,6 +365,7 @@ REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -419,6 +424,7 @@ REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; }; name = Release; }; diff --git a/mac/Config/Config/AddKeyboardView.swift b/mac/Config/Config/AddKeyboardView.swift index 4fe3e5fa155..349cf54c6b3 100644 --- a/mac/Config/Config/AddKeyboardView.swift +++ b/mac/Config/Config/AddKeyboardView.swift @@ -32,11 +32,12 @@ struct AddKeyboardView: View { .font(.headline) // Native progress bar bound to the coordinator's value (0.0 to 1.0) - ProgressView(value: downloadCoordinator.downloadProgress, total: 1.0) + ProgressView(value: downloadCoordinator.downloadProgressFraction, total: 1.0) .progressViewStyle(.linear) .frame(width: 250) - Text("\(Int(downloadCoordinator.downloadProgress * 100))%") +// Text("\(Int(downloadCoordinator.downloadProgressFraction * 100))%") + Text(downloadCoordinator.downloadProgressFraction, format: .percent.precision(.fractionLength(0))) .font(.body) .foregroundColor(.secondary) } diff --git a/mac/Config/Config/DownloadCoordinator.swift b/mac/Config/Config/DownloadCoordinator.swift index 735887d737e..cfb12f02aa7 100644 --- a/mac/Config/Config/DownloadCoordinator.swift +++ b/mac/Config/Config/DownloadCoordinator.swift @@ -24,7 +24,7 @@ import OSLog public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelegate, WKDownloadDelegate { @Published var isDownloading = false // progress is between 0.0 and 1.0 - @Published var downloadProgress: Double = 0.0 + @Published var downloadProgressFraction: Double = 0.0 @Published var showConfirmPackageSheet = false @Published var installHelper: PackageInstallHelper? @@ -100,13 +100,13 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega // reset progress states self.isDownloading = true - self.downloadProgress = 0.0 + self.downloadProgressFraction = 0.0 progressObserver = download.progress.observe(\.fractionCompleted, options: [.new]) { [weak self] _, change in guard let newValue = change.newValue else { return } Task { @MainActor [weak self] in - self?.downloadProgress = newValue + self?.downloadProgressFraction = newValue } } } diff --git a/mac/Config/Config/IconButtonView.swift b/mac/Config/Config/IconButtonView.swift index 49bc963cd11..a5c2cedca68 100644 --- a/mac/Config/Config/IconButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -12,7 +12,7 @@ public struct IconButtonView: View { let action: () -> Void let systemImage: String let font: Font - let helpText: String + let helpText: LocalizedStringKey public var body: some View { @@ -32,7 +32,7 @@ public struct IconButtonView: View { // the view for buttons with a label public struct LabelButtonView: View { let action: () -> Void - let label: String + let label: LocalizedStringKey let systemImage: String let font: Font diff --git a/mac/Config/Config/InstallDebugView.swift b/mac/Config/Config/InstallDebugView.swift index bfc1abf1bc6..50b03ed3c36 100644 --- a/mac/Config/Config/InstallDebugView.swift +++ b/mac/Config/Config/InstallDebugView.swift @@ -3,7 +3,9 @@ * * Created by Shawn Schantz on 2026-02-26 * - * Main view used for configuring Keyman + * Test view used for configuring Keyman + * Not included in ConfigApp.swift, but can be added temporarily for testing purposes + * All text and labels are marked verbatim, so that they do not get extracted for localization */ import SwiftUI @@ -18,7 +20,7 @@ struct InstallDebugView: View { Image(systemName: "gear") .imageScale(.large) .foregroundColor(.accentColor) - Text("Current task = \(taskText)") + Text(verbatim: "Current task = \(taskText)") .onAppear() { if let installTask = installation.currentTask() { taskText = installTask.taskType.rawValue @@ -26,52 +28,78 @@ struct InstallDebugView: View { } } HStack { - Button("Next...") { + Button { installation.executeCurrentInstallationTask() if let installTask = installation.currentTask() { taskText = installTask.taskType.rawValue } + } label: { + Text(verbatim: "Next...") } .disabled(installation.isInstallationComplete()) - Button("Migrate Data") { + Button { _ = installation.migrateData() + } label: { + Text(verbatim: "Migrate Data") } - Button("Register Keyman") { + Button { _ = installation.registerKeymanInputMethod() + } label: { + Text(verbatim: "Register Keyman") } - Button("Enable Keyman") { + Button { _ = installation.enableKeymanInputMethod() + } label: { + Text(verbatim: "Enable Keyman") } - Button("Select Keyman") { + Button { _ = installation.selectKeymanInputMethod() + } label: { + Text(verbatim: "Select Keyman") } - Button("Check Permission") { + Button { installation.checkAccessibilityPermissionGranted() + } label: { + Text(verbatim: "Check Permission") } - Button("Request Permission") { + Button { _ = installation.requestAccessibility() + } label: { + Text(verbatim: "Request Permission") } Spacer() } .padding() HStack { - Button("Request Restart") { + Button { _ = installation.notifyUserPromptedToRestart() + } label: { + Text(verbatim: "Request Restart") } - Button("Check Restart") { + Button { _ = installation.validateUserHasRestarted() + } label: { + Text(verbatim: "Check Restart") } - Button("Set Displayed Complete") { + Button { installation.setHasDisplayedInstallationComplete() + } label: { + Text(verbatim: "Set Displayed Complete") } - Button("debug") { + Button { installation.debug() + } label: { + Text(verbatim: "debug") } - Button("Disable Keyman") { + Button { _ = installation.disableKeymanInputMethod() + } label: { + Text(verbatim: "Disable Keyman") } - Button("Kill Keyman") { + Button { _ = installation.killKeymanInputMethod() + } label: { + Text(verbatim: "Kill Keyman") } Spacer() } diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 31ea85eacc9..0baf935fb68 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -136,7 +136,7 @@ struct MainConfigView: View { .dropDestination(for: URL.self) { urls, _ in // reject drop if it is more than one file guard let droppedFileUrl = urls.first, urls.count < 2 else { - let error = DropKmpError.tooManyFiles + let error = DropKmpError.exceededFileDropLimit self.alertMessage = error.localizedDescription self.isShowingDropKmpAlert = true return false // the drop failed diff --git a/mac/Config/Localizable.xcstrings b/mac/Config/Localizable.xcstrings new file mode 100644 index 00000000000..e2a2bea6b2f --- /dev/null +++ b/mac/Config/Localizable.xcstrings @@ -0,0 +1,141 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Access has not been granted." : { + + }, + "Add Keyboard" : { + + }, + "Are you sure you want to delete the Keyman package '%@'?" : { + + }, + "Author:" : { + + }, + "Cancel" : { + + }, + "Checking..." : { + + }, + "Close" : { + + }, + "Configuration" : { + + }, + "Continue" : { + + }, + "Copy link" : { + + }, + "Copyright:" : { + + }, + "Delete" : { + + }, + "Downloading File..." : { + + }, + "Enable" : { + + }, + "Enable Keyman" : { + + }, + "enabled" : { + + }, + "Ensure Keyman.app is set to provide it with necessary control in System Settings > Privacy & Security > Accessibility." : { + + }, + "Finish installation" : { + + }, + "Fonts:" : { + + }, + "Grant Accessibility Permission" : { + + }, + "Help" : { + + }, + "Help not available." : { + + }, + "Install" : { + + }, + "Installation" : { + + }, + "Installation complete" : { + + }, + "Keyboards" : { + + }, + "Missing Keyman Components" : { + + }, + "OK" : { + + }, + "One or more Keyman components or permissions require your attention. Complete the following steps to restore your Keyman installation." : { + + }, + "Open Settings" : { + + }, + "Package Installation Failed" : { + + }, + "Package Version:" : { + + }, + "Proceed to continue with installation" : { + + }, + "Read me not available." : { + + }, + "Remove Package" : { + + }, + "Repairs Required" : { + + }, + "Resolve Issues" : { + + }, + "Restart Computer" : { + + }, + "Restart your Mac to complete the installation. After restarting, open Keyman Configuration again if it doesn't launch automatically." : { + + }, + "Run Keyman installer" : { + + }, + "Share Keyboard" : { + + }, + "Show keyboard help" : { + + }, + "To use Keyman, enable the Keyman input method in System Settings." : { + + }, + "Website:" : { + + }, + "Your Keyman input method is either missing or outdated. Run the Keyman installer to install a new version." : { + + } + }, + "version" : "1.2" +} \ No newline at end of file diff --git a/mac/KeymanSettings/Package.swift b/mac/KeymanSettings/Package.swift index ff06d9bdc21..15f9a23b764 100644 --- a/mac/KeymanSettings/Package.swift +++ b/mac/KeymanSettings/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "KeymanSettings", + defaultLocalization: "en", platforms: [ .iOS(.v16), .macOS(.v13) diff --git a/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings new file mode 100644 index 00000000000..f422b2d7d10 --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings @@ -0,0 +1,237 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "contains.no.files" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package contains no files." + } + } + } + }, + "contains.no.keyboards" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package contains no keyboards." + } + } + } + }, + "could.not.unzip" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package could not be unzipped." + } + } + } + }, + "error.internal" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "An internal error occurred." + } + } + } + }, + "exceeded.file.drop.limit" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Only a single .KMP file can be installed at a time." + } + } + } + }, + "font.copy.error" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "There was an error copying the font." + } + } + } + }, + "font.registration.error" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "There was an error registering the font." + } + } + } + }, + "installation.in.progress" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A package installation is already in progress." + } + } + } + }, + "insufficient.keyman.version" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The keyboard package '%1$@' requires Keyman version %2$@ but your version is %3$@." + } + } + } + }, + "invalid.url" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The URL is not valid." + } + } + } + }, + "keyboard.id.missing" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no ID." + } + } + } + }, + "keyboard.missing.kmx.file" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no corresponding KMX file." + } + } + } + }, + "keyboard.missing.version" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no version." + } + } + } + }, + "keyboard.name.missing" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "A keyboard in the package has no name." + } + } + } + }, + "kmp.json.not.found" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package's kmp.json file was not found." + } + } + } + }, + "kmp.json.unreadable" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package's kmp.json file could not be parsed." + } + } + } + }, + "non.kmp.file" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The file %@ is not a .KMP file." + } + } + } + }, + "prompt.new.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%@' is ready to install" + } + } + } + }, + "prompt.replace.newer.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%1$@' is ready to downgrade from version %2$@ to %3$@" + } + } + } + }, + "prompt.replace.older.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%1$@' is ready to update from version %2$@ to %3$@" + } + } + } + }, + "prompt.replace.same.version.package" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The package '%@' is ready to re-install" + } + } + } + } + }, + "version" : "1.2" +} \ No newline at end of file diff --git a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift index c8b53fa0500..dee8c60105a 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift @@ -33,12 +33,35 @@ public enum InstallPackageError: LocalizedError { case fontRegistrationError case internalError // due to invalid state, should never occur + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } public var errorDescription: String? { switch self { - case .packageInstallationAlreadyInProgress: return "A package installation is already in progress." - case .fontCopyError: return "There was an error copying the font." - case .fontRegistrationError: return "There was an error registering the font." - case .internalError: return "An internal error occurred." + case .packageInstallationAlreadyInProgress: + let resource = LocalizedStringResource( + "installation.in.progress", + defaultValue: "A package installation is already in progress.", + bundle: packageBundle) + return String(localized: resource) + case .fontCopyError: + let resource = LocalizedStringResource( + "font.copy.error", + defaultValue: "There was an error copying the font.", + bundle: packageBundle) + return String(localized: resource) + case .fontRegistrationError: + let resource = LocalizedStringResource( + "font.registration.error", + defaultValue: "There was an error registering the font.", + bundle: packageBundle) + return String(localized: resource) + case .internalError: + let resource = LocalizedStringResource( + "error.internal", + defaultValue: "An internal error occurred.", + bundle: packageBundle) + return String(localized: resource) } } } @@ -57,20 +80,27 @@ public extension Notification.Name { static let dataMigrated = Notification.Name("com.keyman.data.migrated") } -// define LocalizedError so that UI can present a localizable message -// when the attempt to install a KMP file using drag and drop fails public enum DropKmpError: LocalizedError { case invalidFileType(String) - case alreadyInstalled(String) - case installFailed(String) - case tooManyFiles + case exceededFileDropLimit + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } public var errorDescription: String? { switch self { - case .invalidFileType(let fileName): return "The file \(fileName) is not a .KMP file." - case .alreadyInstalled(let fileName): return "The package \(fileName) is already installed." - case .installFailed(let fileName): return "The file \(fileName) could not be installed." - case .tooManyFiles: return "Only a single .KMP file can be installed at a time." + case .invalidFileType(let fileName): + let resource = LocalizedStringResource( + "non.kmp.file", + defaultValue: "The file \(fileName) is not a .KMP file.", + bundle: packageBundle) + return String(localized: resource) + case .exceededFileDropLimit: + let resource = LocalizedStringResource( + "exceeded.file.drop.limit", + defaultValue: "Only a single .KMP file can be installed at a time.", + bundle: packageBundle) + return String(localized: resource) } } } diff --git a/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift index b5d7db38ca5..288c018aa82 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift @@ -21,14 +21,30 @@ public enum PackageInstallationType { public var prompt: LocalizedStringResource { switch self { - case .newPackage(let packageName): - return "The package '\(packageName)' is ready to install" + case .newPackage(let packageName): + return LocalizedStringResource( + "prompt.new.package", + defaultValue: "The package '\(packageName)' is ready to install", + bundle: .atURL(Bundle.module.bundleURL) + ) case .replaceSameVersionPackage(let packageName): - return "The package '\(packageName)' is ready to re-install" + return LocalizedStringResource( + "prompt.replace.same.version.package", + defaultValue: "The package '\(packageName)' is ready to re-install", + bundle: .atURL(Bundle.module.bundleURL) + ) case .replaceOlderPackage(let packageName, let existingVersion, let newVersion): - return "The package '\(packageName)' is ready to update from version \(existingVersion) to \(newVersion)" + return LocalizedStringResource( + "prompt.replace.older.package", + defaultValue: "The package '\(packageName)' is ready to update from version \(existingVersion) to \(newVersion)", + bundle: .atURL(Bundle.module.bundleURL) + ) case .replaceNewerPackage(let packageName, let existingVersion, let newVersion): - return "The package '\(packageName)' is ready to downgrade from version \(existingVersion) to \(newVersion)" + return LocalizedStringResource( + "prompt.replace.newer.package", + defaultValue: "The package '\(packageName)' is ready to downgrade from version \(existingVersion) to \(newVersion)", + bundle: .atURL(Bundle.module.bundleURL) + ) } } } diff --git a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift index 7d73104c104..659267018dc 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift @@ -23,20 +23,78 @@ public enum LoadPackageError: LocalizedError { case missingKeyboardVersion case missingKmxFile case insufficientKeymanVersion(packageName: String, requiredKeymanVersion: String, actualKeymanVersion: String) - + + private var packageBundle: LocalizedStringResource.BundleDescription { + .atURL(Bundle.module.bundleURL) + } public var errorDescription: String? { switch self { - case .invalidUrl: return "The URL is not valid." - case .unzipError: return "The keyboard package could not be unzipped." - case .containsNoFiles: return "The keyboard package contains no files." - case .containsNoKeyboards: return "The keyboard package contains no keyboards." - case .kmpJsonFileUnreadable: return "The package's kmp.json file could not be parsed." - case .kmpJsonFileNotFound: return "The package's kmp.json file was not found." - case .missingKeyboardName: return "A keyboard in the package has no name." - case .missingKeyboardId: return "A keyboard in the package has no ID." - case .missingKeyboardVersion: return "A keyboard in the package has no version." - case .missingKmxFile: return "A keyboard in the package has no corresponding KMX file." - case .insufficientKeymanVersion(let packageName, let requiredKeymanVersion, let actualKeymanVersion): return "The keyboard package '\(packageName)' requires Keyman version \(requiredKeymanVersion) but your version is \(actualKeymanVersion)." + case .invalidUrl: + let resource = LocalizedStringResource( + "invalid.url", + defaultValue: "The URL is not valid.", + bundle: packageBundle) + return String(localized: resource) + case .unzipError: + let resource = LocalizedStringResource( + "could.not.unzip", + defaultValue: "The keyboard package could not be unzipped.", + bundle: packageBundle) + return String(localized: resource) + case .containsNoFiles: + let resource = LocalizedStringResource( + "contains.no.files", + defaultValue: "The keyboard package contains no files.", + bundle: packageBundle) + return String(localized: resource) + case .containsNoKeyboards: + let resource = LocalizedStringResource( + "contains.no.keyboards", + defaultValue: "The keyboard package contains no keyboards.", + bundle: packageBundle) + return String(localized: resource) + case .kmpJsonFileUnreadable: + let resource = LocalizedStringResource( + "kmp.json.unreadable", + defaultValue: "The package's kmp.json file could not be parsed.", + bundle: packageBundle) + return String(localized: resource) + case .kmpJsonFileNotFound: + let resource = LocalizedStringResource( + "kmp.json.not.found", + defaultValue: "The package's kmp.json file was not found.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardName: + let resource = LocalizedStringResource( + "keyboard.name.missing", + defaultValue: "A keyboard in the package has no name.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardId: + let resource = LocalizedStringResource( + "keyboard.id.missing", + defaultValue: "A keyboard in the package has no ID.", + bundle: packageBundle) + return String(localized: resource) + case .missingKeyboardVersion: + let resource = LocalizedStringResource( + "keyboard.missing.version", + defaultValue: "A keyboard in the package has no version.", + bundle: packageBundle) + return String(localized: resource) + case .missingKmxFile: + let resource = LocalizedStringResource( + "keyboard.missing.kmx.file", + defaultValue: "A keyboard in the package has no corresponding KMX file.", + bundle: packageBundle) + return String(localized: resource) + case .insufficientKeymanVersion(let packageName, let requiredKeymanVersion, let actualKeymanVersion): + let resource = LocalizedStringResource( + "insufficient.keyman.version", + defaultValue: "The keyboard package '\(packageName)' requires Keyman version \(requiredKeymanVersion) but your version is \(actualKeymanVersion).", + bundle: packageBundle) + return String(localized: resource) } } } From dc3c1bce9587263d4839816dc992ff0f3fd7d8ff Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Thu, 3 Sep 2026 09:24:47 -0400 Subject: [PATCH 2/3] feat(mac): minor text change and filename change --- .../Contents.json | 0 .../enable-keyman-dark.pdf | Bin .../enable-keyman-light.pdf | Bin .../Sources/KeymanSettings/Localizable.xcstrings | 2 +- .../Sources/Persistence/PackageRepository.swift | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) rename mac/Config/Config/Assets.xcassets/{enable-Keyman.imageset => enable-keyman.imageset}/Contents.json (100%) rename mac/Config/Config/Assets.xcassets/{enable-Keyman.imageset => enable-keyman.imageset}/enable-keyman-dark.pdf (100%) rename mac/Config/Config/Assets.xcassets/{enable-Keyman.imageset => enable-keyman.imageset}/enable-keyman-light.pdf (100%) diff --git a/mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/Contents.json b/mac/Config/Config/Assets.xcassets/enable-keyman.imageset/Contents.json similarity index 100% rename from mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/Contents.json rename to mac/Config/Config/Assets.xcassets/enable-keyman.imageset/Contents.json diff --git a/mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/enable-keyman-dark.pdf b/mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-dark.pdf similarity index 100% rename from mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/enable-keyman-dark.pdf rename to mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-dark.pdf diff --git a/mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/enable-keyman-light.pdf b/mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-light.pdf similarity index 100% rename from mac/Config/Config/Assets.xcassets/enable-Keyman.imageset/enable-keyman-light.pdf rename to mac/Config/Config/Assets.xcassets/enable-keyman.imageset/enable-keyman-light.pdf diff --git a/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings index f422b2d7d10..c396257f1b3 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings +++ b/mac/KeymanSettings/Sources/KeymanSettings/Localizable.xcstrings @@ -128,7 +128,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "A keyboard in the package has no corresponding KMX file." + "value" : "A keyboard in the package has no corresponding .KMX file." } } } diff --git a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift index 659267018dc..ee90b10cd09 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift @@ -86,7 +86,7 @@ public enum LoadPackageError: LocalizedError { case .missingKmxFile: let resource = LocalizedStringResource( "keyboard.missing.kmx.file", - defaultValue: "A keyboard in the package has no corresponding KMX file.", + defaultValue: "A keyboard in the package has no corresponding .KMX file.", bundle: packageBundle) return String(localized: resource) case .insufficientKeymanVersion(let packageName, let requiredKeymanVersion, let actualKeymanVersion): From 853b793c51c0521b84f30d7fca90ddd351fa5c35 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Thu, 3 Sep 2026 10:56:35 -0400 Subject: [PATCH 3/3] feat(mac): deleted commented out code --- mac/Config/Config/AddKeyboardView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/mac/Config/Config/AddKeyboardView.swift b/mac/Config/Config/AddKeyboardView.swift index 349cf54c6b3..9daa3c0c7c8 100644 --- a/mac/Config/Config/AddKeyboardView.swift +++ b/mac/Config/Config/AddKeyboardView.swift @@ -36,7 +36,6 @@ struct AddKeyboardView: View { .progressViewStyle(.linear) .frame(width: 250) -// Text("\(Int(downloadCoordinator.downloadProgressFraction * 100))%") Text(downloadCoordinator.downloadProgressFraction, format: .percent.precision(.fractionLength(0))) .font(.body) .foregroundColor(.secondary)