Skip to content

Commit

Permalink
Add SDKLocalizedString global function (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewdmontgomery authored Sep 9, 2024
2 parents a42b9cd + 3eae5b8 commit 32df00e
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 52 deletions.
16 changes: 16 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions Demo/Gravatar-Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */,
Expand Down Expand Up @@ -385,7 +385,7 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
1E3FA23E2C74B808002901F2 /* ShellScript */ = {
1E3FA23E2C74B808002901F2 /* Generate Secrets.swift */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
Expand All @@ -395,6 +395,7 @@
inputPaths = (
"$(SRCROOT)/Demo/Secrets.tpl",
);
name = "Generate Secrets.swift";
outputFileListPaths = (
);
outputPaths = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "829ecc0bf847b439cbb46739f35d9014d6ce6f42eaa72db961e1c0c2d202fa8e",
"originHash" : "873e748e6cd0092c005bb2eeeb80dd830b1cffbb6a8974436ad78419281827e3",
"pins" : [
{
"identity" : "swift-snapshot-testing",
Expand All @@ -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"
}
},
{
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "85a761808437c26b26a29368f9cc9aa509cdd039f95eff656309c72fa6ff2557",
"originHash" : "2d82ed06a27431c1da79790f8b215b8abf6d2a7397f42f02e364c7a92f86a5ab",
"pins" : [
{
"identity" : "swift-snapshot-testing",
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -31,7 +34,8 @@ let package = Package(
name: "Gravatar",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
],
plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")]
),
.testTarget(
name: "GravatarTests",
Expand All @@ -47,7 +51,8 @@ let package = Package(
resources: [.process("Resources")],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
],
plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")]
),
.testTarget(
name: "GravatarUITests",
Expand Down
Original file line number Diff line number Diff line change
@@ -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."
)
Expand Down
9 changes: 3 additions & 6 deletions Sources/GravatarUI/ProfileFields/ProfileButtonBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
39 changes: 13 additions & 26 deletions Sources/GravatarUI/SwiftUI/AvatarPicker/AvatarPickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,100 +313,87 @@ 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"
)
}

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"
)
}

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"
)
}
}

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"
)
Expand Down
29 changes: 29 additions & 0 deletions Sources/GravatarUI/Utility/SDKLocalizedString.swift
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 8 additions & 4 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions fastlane/lanes/localization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down

0 comments on commit 32df00e

Please sign in to comment.