diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..f4965dfa --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,16 @@ +only_rules: # Rules to run + - custom_rules + +# If true, SwiftLint will treat all warnings as errors. +strict: true + +custom_rules: + no_ns_localized_string: + included: + - "Sources/.*\\.swift" + name: "No NSLocalizedString" + regex: "NSLocalizedString\\(" + match_kinds: + - identifier + message: "Use `SDKLocalizedString()` instead of `NSLocalizedString()`." + severity: error diff --git a/Demo/Gravatar-Demo.xcodeproj/project.pbxproj b/Demo/Gravatar-Demo.xcodeproj/project.pbxproj index 0e00db8e..50d3be62 100644 --- a/Demo/Gravatar-Demo.xcodeproj/project.pbxproj +++ b/Demo/Gravatar-Demo.xcodeproj/project.pbxproj @@ -287,7 +287,7 @@ isa = PBXNativeTarget; buildConfigurationList = 49C5D6182B5B33E20067C2A8 /* Build configuration list for PBXNativeTarget "Gravatar SwiftUI" */; buildPhases = ( - 1E3FA23E2C74B808002901F2 /* ShellScript */, + 1E3FA23E2C74B808002901F2 /* Generate Secrets.swift */, 49C5D6062B5B33E20067C2A8 /* Sources */, 49C5D6072B5B33E20067C2A8 /* Frameworks */, 49C5D6082B5B33E20067C2A8 /* Resources */, @@ -385,7 +385,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1E3FA23E2C74B808002901F2 /* ShellScript */ = { + 1E3FA23E2C74B808002901F2 /* Generate Secrets.swift */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -395,6 +395,7 @@ inputPaths = ( "$(SRCROOT)/Demo/Secrets.tpl", ); + name = "Generate Secrets.swift"; outputFileListPaths = ( ); outputPaths = ( diff --git a/Demo/Gravatar-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/Gravatar-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index abd02be5..78bbb441 100644 --- a/Demo/Gravatar-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/Gravatar-Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "829ecc0bf847b439cbb46739f35d9014d6ce6f42eaa72db961e1c0c2d202fa8e", + "originHash" : "873e748e6cd0092c005bb2eeeb80dd830b1cffbb6a8974436ad78419281827e3", "pins" : [ { "identity" : "swift-snapshot-testing", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax", "state" : { - "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", - "version" : "510.0.1" + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" } }, { @@ -27,6 +27,15 @@ "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", "version" : "0.54.0" } + }, + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "6c3d6c32a37224179dc290f21e03d1238f3d963b", + "version" : "0.56.2" + } } ], "version" : 3 diff --git a/Package.resolved b/Package.resolved index f94ac4bb..f1e5bd25 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "85a761808437c26b26a29368f9cc9aa509cdd039f95eff656309c72fa6ff2557", + "originHash" : "2d82ed06a27431c1da79790f8b215b8abf6d2a7397f42f02e364c7a92f86a5ab", "pins" : [ { "identity" : "swift-snapshot-testing", @@ -27,6 +27,15 @@ "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", "version" : "0.54.0" } + }, + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "6c3d6c32a37224179dc290f21e03d1238f3d963b", + "version" : "0.56.2" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 979411bd..0431b86d 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,9 @@ let package = Package( name: "Gravatar", defaultLocalization: "en", platforms: [ + // Platforms specifies os version minimums. It does not limit which platforms are supported. .iOS(.v15), + .macOS(.v12) // The SDK does not support macOS, this satisfies SwiftLint requirements ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. @@ -23,6 +25,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"), + .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "0.56.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -31,7 +34,8 @@ let package = Package( name: "Gravatar", swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") - ] + ], + plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] ), .testTarget( name: "GravatarTests", @@ -47,7 +51,8 @@ let package = Package( resources: [.process("Resources")], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") - ] + ], + plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] ), .testTarget( name: "GravatarUITests", diff --git a/Sources/GravatarUI/ProfileFields/Model/ClaimProfileModel.swift b/Sources/GravatarUI/ProfileFields/Model/ClaimProfileModel.swift index 6df355ab..29ba7e15 100644 --- a/Sources/GravatarUI/ProfileFields/Model/ClaimProfileModel.swift +++ b/Sources/GravatarUI/ProfileFields/Model/ClaimProfileModel.swift @@ -1,23 +1,20 @@ import UIKit struct ClaimProfileModel: ProfileModel { - let description: String = NSLocalizedString( + let description: String = SDKLocalizedString( "ClaimProfile.Label.AboutMe", - bundle: .module, value: "Tell the world who you are. Your avatar and bio that follows you across the web.", comment: "Text on a sample Gravatar profile, appearing in the place where a Gravatar profile would display your short biography." ) - let location: String = NSLocalizedString( + let location: String = SDKLocalizedString( "ClaimProfile.Label.Location", - bundle: .module, value: "Add your location, pronouns, etc", comment: "Text on a sample Gravatar profile, appearing in the place where a Gravatar profile would display information like location, your preferred pronouns, etc." ) - var displayName: String = NSLocalizedString( + var displayName: String = SDKLocalizedString( "ClaimProfile.Label.DisplayName", - bundle: .module, value: "Your Name", comment: "Text on a sample Gravatar profile, appearing in the place where your name would normally appear on your Gravatar profile after you claim it." ) diff --git a/Sources/GravatarUI/ProfileFields/ProfileButtonBuilder.swift b/Sources/GravatarUI/ProfileFields/ProfileButtonBuilder.swift index 3fcbd527..1ac34e67 100644 --- a/Sources/GravatarUI/ProfileFields/ProfileButtonBuilder.swift +++ b/Sources/GravatarUI/ProfileFields/ProfileButtonBuilder.swift @@ -17,23 +17,20 @@ extension ProfileButtonStyle { var localizedTitle: String { switch self { case .view: - NSLocalizedString( + SDKLocalizedString( "ProfileButton.title.view", - bundle: .module, value: "View profile", comment: "Title for a button that allows you to view your Gravatar profile" ) case .edit: - NSLocalizedString( + SDKLocalizedString( "ProfileButton.title.edit", - bundle: .module, value: "Edit profile", comment: "Title for a button that allows you to edit your Gravatar profile" ) case .create: - NSLocalizedString( + SDKLocalizedString( "ProfileButton.title.create", - bundle: .module, value: "Claim profile", comment: "Title for a button that allows you to claim a new Gravatar profile" ) diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerProfileView.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerProfileView.swift index c509b9ca..0c93adef 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerProfileView.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerProfileView.swift @@ -87,9 +87,8 @@ struct AvatarPickerProfileView: View { extension AvatarPickerProfileView { private enum Localized { - static let viewProfileButtonTitle = NSLocalizedString( + static let viewProfileButtonTitle = SDKLocalizedString( "AvatarPickerProfile.Button.ViewProfile.title", - bundle: .module, value: "View profile →", comment: "Title of a button that will take you to your Gravatar profile, with an arrow indicating that this action will cause you to leave this view" ) diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift index 5a55209a..1ef64287 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift @@ -313,29 +313,25 @@ private enum AvatarPicker { } enum Localized { - static let buttonUploadImage = NSLocalizedString( + static let buttonUploadImage = SDKLocalizedString( "AvatarPicker.ContentLoading.Success.ctaButtonTitle", - bundle: .module, value: "Upload image", comment: "Title of a button that allow for uploading an image" ) - static let buttonRetry = NSLocalizedString( + static let buttonRetry = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.Retry.ctaButtonTitle", - bundle: .module, value: "Try again", comment: "Title of a button that allows the user to try loading their avatars again" ) enum Header { - static let title = NSLocalizedString( + static let title = SDKLocalizedString( "AvatarPicker.Header.title", - bundle: .module, value: "Avatars", comment: "Title appearing in the header of a view that allows users to manage their avatars" ) - static let subtitle = NSLocalizedString( + static let subtitle = SDKLocalizedString( "AvatarPicker.Header.subtitle", - bundle: .module, value: "Choose or upload your favorite avatar images and connect them to your email address.", comment: "A message describing the purpose of this view" ) @@ -343,15 +339,13 @@ private enum AvatarPicker { enum ContentLoading { enum Success { - static let title = NSLocalizedString( + static let title = SDKLocalizedString( "AvatarPicker.ContentLoading.success.title", - bundle: .module, value: "Let's setup your avatar", comment: "Title of a message advising the user to setup their avatar" ) - static let subtext = NSLocalizedString( + static let subtext = SDKLocalizedString( "AvatarPicker.ContentLoading.Success.subtext", - bundle: .module, value: "Choose or upload your favorite avatar images and connect them to your email address.", comment: "A message describing the actions a user can take to setup their avatar" ) @@ -359,38 +353,33 @@ private enum AvatarPicker { enum Failure { enum SessionExpired { - static let title = NSLocalizedString( + static let title = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.SessionExpired.title", - bundle: .module, value: "Session expired", comment: "Title of a message advising the user that their login session has expired." ) enum Close { - static let buttonTitle = NSLocalizedString( + static let buttonTitle = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.SessionExpired.Close.buttonTitle", - bundle: .module, value: "Close", comment: "Title of a button that will close the Avatar Picker, appearing beneath a message that advises the user that their login session has expired." ) - static let subtext = NSLocalizedString( + static let subtext = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.SessionExpired.Close.subtext", - bundle: .module, value: "Sorry, it looks like your session has expired. Make sure you're logged in to update your Avatar.", comment: "A message describing the error and advising the user to login again to resolve the issue" ) } enum LogIn { - static let buttonTitle = NSLocalizedString( + static let buttonTitle = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.SessionExpired.LogIn.buttonTitle", - bundle: .module, value: "Log in", comment: "Title of a button that will begin the process of authenticating the user, appearing beneath a message that advises the user that their login session has expired." ) - static let subtext = NSLocalizedString( + static let subtext = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.SessionExpired.LogIn.subtext", - bundle: .module, value: "Session expired for security reasons. Please log in to update your Avatar.", comment: "A message describing the error and advising the user to login again to resolve the issue" ) @@ -398,15 +387,13 @@ private enum AvatarPicker { } enum Retry { - static let title = NSLocalizedString( + static let title = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.Retry.title", - bundle: .module, value: "Ooops", comment: "Title of a message advising the user that something went wrong while loading their avatars" ) - static let subtext = NSLocalizedString( + static let subtext = SDKLocalizedString( "AvatarPicker.ContentLoading.Failure.Retry.subtext", - bundle: .module, value: "Something went wrong and we couldn’t connect to Gravatar servers.", comment: "A message asking the user to try again" ) diff --git a/Sources/GravatarUI/Utility/SDKLocalizedString.swift b/Sources/GravatarUI/Utility/SDKLocalizedString.swift new file mode 100644 index 00000000..fe5b13af --- /dev/null +++ b/Sources/GravatarUI/Utility/SDKLocalizedString.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Use this function instead of `NSLocalizedString` to reference localized strings **from the library module**. +/// +/// You should use this `SDKLocalizedString` method in place of `NSLocalizedString` for all localized strings in the SDK. +/// This ensures that an app target that imports this module will perform localization lookup in the module, and not in the main app bundle, +/// which is the default when using `NSLocalizedStrings()` without specifying `bundle = .module`. +/// +/// - Note: +/// Tooling: Be sure to pass this function's name as a custom routine when parsing the code to generate the main `.strings` file, +/// using `genstrings -s SDKLocalizedString`, so that this helper method is recognized. You will also have to +/// exclude this very file from being parsed by `genstrings`, so that it won't accidentally misinterpret that routine/function definition +/// below as a call site and generate an error because of it. +/// +/// - Parameters: +/// - key: An identifying value used to reference a localized string. +/// - tableName: The basename of the `.strings` file **in the app bundle** containing +/// the localized values. If `tableName` is `nil`, the `Localizable` table is used. +/// - value: The English/default copy for the string. This is the user-visible string that the +/// translators will use as original to translate, and also the string returned when the localized string for +/// `key` cannot be found in the table. If `value` is `nil` or empty, `key` would be returned instead. +/// - comment: A note to the translator describing the context where the localized string is presented to the user. +/// +/// - Returns: A localized version of the string designated by `key` in the table identified by `tableName`. +/// If the localized string for `key` cannot be found within the table, `value` is returned. +/// (However, `key` is returned instead when `value` is `nil` or the empty string). +func SDKLocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String { + Bundle.module.localizedString(forKey: key, value: value, table: tableName) +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f024b148..70a8ece2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -14,6 +14,7 @@ XCODEPROJ_PATH = File.join(PROJECT_ROOT_FOLDER, 'Gravatar-Demo.xcodeproj') DEMO_APPS_SOURCES_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'Demo') XCCONFIG_PROTOTYPE_BUILD_SWIFTUI = File.join(DEMO_APPS_SOURCES_FOLDER, 'Gravatar-SwiftUI-Demo', 'Gravatar-SwiftUI-Demo.Release.xcconfig') XCCONFIG_PROTOTYPE_BUILD_UIKIT = File.join(DEMO_APPS_SOURCES_FOLDER, 'Gravatar-UIKit-Demo', 'Gravatar-UIKit-Demo.Release.xcconfig') +COMMON_XCARGS = ['-skipPackagePluginValidation'] # Allow SwiftPM plugins (e.g. swiftlint) called from Xcode to be used on CI without prior manual approval GITHUB_REPO = 'Automattic/Gravatar-SDK-iOS' GITHUB_URL = "https://github.com/#{GITHUB_REPO}".freeze @@ -53,6 +54,7 @@ platform :ios do run_tests( package_path: '.', scheme: 'Gravatar-Package', + xcargs: COMMON_XCARGS, device: IPHONE_DEVICE, prelaunch_simulator: true, clean: true, @@ -65,10 +67,11 @@ platform :ios do lane :build_demo do |scheme: 'Gravatar-UIKit-Demo'| # We only need to build for testing to ensure that the project builds. # There are no tests in the the Demo apps - scan( + run_tests( project: XCODEPROJ_PATH, scheme: scheme, configuration: 'Debug', + xcargs: COMMON_XCARGS, device: IPHONE_DEVICE, clean: true, build_for_testing: true, @@ -88,9 +91,10 @@ platform :ios do configuration: 'Release', export_method: 'enterprise', output_directory: ARTIFACTS_FOLDER, - xcargs: { - CURRENT_PROJECT_VERSION: build_number - } + xcargs: [ + "CURRENT_PROJECT_VERSION=#{build_number}", + *COMMON_XCARGS + ] ) end diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index 470b644f..25a97673 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -93,6 +93,8 @@ SOURCES_TO_LOCALIZE.each do |source| ios_generate_strings_file_from_code( paths: source.source_paths, + exclude: ['**/SDKLocalizedString.swift'], + routines: ['SDKLocalizedString'], output_dir: source.base_localization_root )