生成各种数值的格式化字符串表示往往是相当棘手的,尤其是当我们希望这些字符串能够完全适应用户的语言、地域和其他系统范围内的偏好时。
值得庆幸的是,苹果公司提供了一套相当全面的完全本地化的格式化工具,作为他们每个平台的一部分,虽然我们已经看了其中最常用的--如 DateFormatter和 NumberFormatter- 在这篇文章中,让我们转而探讨一些鲜为人知的 Formatter 子类,以及它们如何在某些情况下被证明是非常有用的。
人名
如果说所有内置在系统中的Formatter 类型有一个共同点,那就是它们所执行的任务都比最初看起来要复杂得多。
以用户名,或一般人的名字为例。假设我们正在开发一个应用程序,使用一个定义在User 数据模型中的单一fullName 属性来存储每个用户的全名--像这样:
struct User {
var fullName: String
...
}
然后,假设我们目前正在假设,如果我们将一个给定的用户的fullName 字符串分割成空格分隔的组件,那么第一个组件将总是用户的 "名字"--然后我们将其作为该用户的 "显示名称 "在我们应用程序的用户界面上使用。
extension User {
func displayName() -> String {
String(fullName.split(separator: " ")[0])
}
}
上述情况是一个非常常见的错误。不仅世界上不同的语言、国家和文化对名字的各种成分的偏好顺序不同,而且名字往往还有许多成分,而不仅仅是 "名 "和 "姓",而我们的代码目前并没有考虑到这一点。
例如,如果一个用户输入 "Dr. John Appleseed "作为他们的全名呢?那么这个用户的显示名称目前将被计算为 "博士"。这不是很好。
值得庆幸的是,我们在苹果的朋友已经解决了这个问题。进入PersonNameComponentsFormatter ,它处理所有涉及正确解析和格式化一个人的名字的复杂问题。下面是我们如何更新我们的displayName 方法,以使用该格式化器:
extension User {
func displayName() -> String {
let formatter = PersonNameComponentsFormatter()
let components = formatter.personNameComponents(from: fullName)
return components?.givenName ?? name
}
}
上述新版本不仅会产生更正确的结果,而不考虑用户的名字或地区,而且我们代码的意图现在也可以说是更清晰了。
PersonNameComponentsFormatter 也使我们能够把一组名字的组件也变成一个完全本地化的名字,这在处理已经被分割成独立组件的用户名时,可以说是非常有用的--比如这样:
struct User {
var firstName: String
var lastName: String
...
}
extension User {
func fullName() -> String {
let formatter = PersonNameComponentsFormatter()
var components = PersonNameComponents()
components.givenName = firstName
components.familyName = lastName
return formatter.string(from: components)
}
}
我们将所有的名字计算代码实现为方法,而不是计算属性,是为了清楚地表明我们的逻辑需要一定量的处理,因为它不只是返回一个可以在O(1) 时间内计算的值。要了解更多关于这种方法的信息,请查看《Swift中的计算属性》。
地址
接下来,让我们看看作为Contacts 框架一部分的Formatter 类型 -CNPostalAddressFormatter ,它提供了一种将地址格式化为本地化字符串的简单方法。
就像名字一样,地址的格式在不同的国家有很大的不同,而我们自己处理所有这些复杂的问题将是非常令人难以承受的。
作为一个例子,假设我们正在开发某种形式的购物应用程序,其中包含以下ShippingAddress 。
struct ShippingAddress {
var street: String
var postalCode: String
var city: String
var country: String
}
如果我们现在想为这样的地址生成一个格式化的字符串表示(例如,向用户显示一个给定的产品将被运送到什么地址),那么我们所要做的就是将我们的送货地址数据转换成一个CNPostalAddress 实例(这可以使用其可变的对应类型CNMutablePostalAddress ),然后要求CNPostalAddressFormatter 将该实例转换成一个字符串--像这样:
import Contacts
extension ShippingAddress {
func formattedString() -> String {
let formatter = CNPostalAddressFormatter()
let address = CNMutablePostalAddress()
address.street = street
address.postalCode = postalCode
address.city = city
address.country = country
return formatter.string(from: address)
}
}
当然,如果我们处理的是使用Contacts 框架本身检索的地址,那么这些地址已经用CNPostalAddress 实例表示,所以上述类型转换只有在使用自定义的地址存储方式时才有必要。
相对时间
尽管标准的DateFormatter 类型是相当知名的,并且常用于各种应用程序中,它还有一个不太知名的 "表亲",叫做RelativeDateTimeFormatter ,可以用来生成描述两个日期之间相对时间间隔的本地化字符串。
例如,这里是我们如何使用这个专门的格式化器来计算一个字符串,告诉用户距离一个给定的Event 开始还有多少时间。
struct Event {
var title: String
var startDate: Date
...
}
extension Event {
func relativeTimeString() -> String {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.formattingContext = .beginningOfSentence
return formatter.localizedString(for: startDate, relativeTo: Date())
}
}
真正酷的是,通过指定我们要使用named 日期时间样式(就像我们上面做的),像 "明天 "和 "昨天 "这样的词将被用来描述具有特定名称的时间间隔。
列表
最后,让我们快速浏览一下ListFormatter ,它也许不像我们迄今为止探索的其他一些格式化工具那样复杂,但它在某些情况下仍然有用。
从本质上讲,ListFormatter ,使我们能够将一个字符串数组串联成一个单一的、本地化的列表式字符串。使用它就像把我们希望连接的字符串数组传递给它的静态localizedString 方法一样简单,然后它就会根据用户的区域设置和语言设置返回一个格式化的字符串。
// In English:
let ingredientsList = ListFormatter.localizedString(
byJoining: ["Apples", "Sugar", "Flour", "Butter"]
)
print(ingredientsList) // "Apples, Sugar, Flour, and Butter"
// In Swedish:
let ingredientsList = ListFormatter.localizedString(
byJoining: ["Äpplen", "Socker", "Mjöl", "Smör"]
)
print(ingredientsList) // "Äpplen, Socker, Mjöl och Smör"
请注意,英语和瑞典语版本不仅在语言方面有差异,而且在逗号的使用方面也有差异。这些细节可能看起来微不足道,但确实可以使一个应用程序感觉到更彻底的本地化--再一次,我们不必编写任何自定义代码来说明这些差异--这一切都由系统为我们处理。
总结
很明显,苹果对高质量本地化的奉献不仅仅限于他们自己的应用程序和系统功能--他们的各种平台SDK也有许多API、工具和功能,我们也可以用来提升我们自己应用程序的本地化。我们所要做的就是在处理与当地有关的数值时,使用正确的API,例如姓名、数字、地址和日期。
谢谢你的阅读!