iOS中应用程序的本地化
第1部分。我们有什么?
本地化字符串资源指南
引言
几年前,我进入了iOS开发的神奇世界,从本质上讲,它为我带来了IT领域的美好未来。 但是,在深入研究平台和开发环境的功能时,在解决看似微不足道的任务时,我遇到了许多困难和不便之处:苹果公司的“创新的保守主义”有时使开发人员变得非常老练,从而满足了无限的“ WANT”客户。
这些问题之一是本地化应用程序的字符串资源的问题。 我想将我在哈勃(Habr)的广阔著作中的第一批出版物投入到这个问题上。
最初,我希望将自己的想法写在一篇文章中,但是我想介绍的信息量很大。 在本文中,我将尝试揭示使用本地化资源的标准机制的本质,重点是大多数指南和教程所忽略的某些方面。 该材料主要针对初学者(或尚未经历过此类任务的开发人员)。 对于有经验的开发人员,此信息可能不是特别有价值。 但是关于实践中可能会遇到的不便和不利之处,我会在未来告诉大家...
开箱即用。 如何在iOS应用程序中组织字符串资源的存储
首先,我们注意到平台中的本地化机制已经是一个巨大的优势,因为 使程序员免于进行额外的开发,并设置了用于处理数据的单一格式。 通常,基本机制足以实施相对较小的项目。
那么,Xcode为我们提供了“开箱即用”的机会吗? 首先,让我们看一下在项目中存储字符串资源的标准。
在具有静态内容的项目中,字符串数据可以直接存储在界面中( .storyboard
和.storyboard
标记文件,它们依次是使用Interface Builder工具呈现的XML文件)或代码中。 第一种方法使我们能够简化并加快标记屏幕和单个显示器的过程,因为 开发人员无需构建应用程序即可观察到大部分更改。 但是,在这种情况下,不难实现数据冗余(如果多个元素使用相同的文本,则显示)。 第二种方法只是消除了数据冗余的问题,但是导致需要手动填充屏幕(通过设置其他IBOutlet
并为其分配相应的文本值),这反过来又导致了代码冗余(当然,除非文本应由应用程序代码直接安装)。
此外,Apple提供了标准文件扩展名.strings
。 该标准规范了以关联数组( "-"
)的形式存储字符串数据的格式:
"key" = "value";
该键区分大小写,允许使用空格,下划线,标点符号和特殊字符。
重要的是要注意,尽管语法简单明了,但字符串文件是应用程序的编译,汇编或操作期间的常规错误源。 这有几个原因。
首先,语法错误。 缺少分号,等号,多余或未转义的引号将不可避免地导致编译器错误。 此外,Xcode将指向带有错误的文件,但不会突出显示出现错误的行。 查找此类错字可能需要花费大量时间,尤其是在文件中包含大量数据的情况下。
其次,重复密钥。 该应用程序当然不会因此而崩溃,但是可能会向用户显示不正确的数据。 问题是,当按键访问一行时,对应于文件中最后一次出现的键的值将被拉出。
结果,简单的设计要求程序员在用数据填充文件时要非常透彻和专心。
知识渊博 开发人员可以立即惊呼: “但是JSON和PLIST呢?他们不满意呢?” 好吧,首先, JSON
和PLIST
(实际上是普通的XML
)是通用标准,允许同时存储字符串和数字,逻辑( BOOL
),二进制数据,时间和日期以及索引集( Array
)和关联性( Dictionary
)数组。 因此,这些标准的语法更加饱和,因此更容易被它们所蚕食。 其次,再次由于复杂的语法,此类文件的处理速度略低于Strings文件。 更不用说要与他们合作,您需要在代码中进行许多操作。
本地化,本地化但不本地化。 用户界面本地化
因此,在找出标准之后,现在让我们找出如何使用所有标准。
让我们去吧。 首先,创建一个简单的Single View Application并将一些文本组件添加到ViewController的Main.storyboard上。

在这种情况下,内容直接存储在界面中。 要对其进行本地化,必须执行以下操作:
1)转到项目设置

2)然后-从目标到项目

3)打开信息标签

在“ 本地化”部分中 ,我们立即看到我们已经有了条目“英语-开发语言” 。 这意味着将英语设置为开发语言(或默认语言)。
现在添加另一种语言。 为此,请单击“ + ”并选择所需的语言(例如,我选择了俄语)。 关心Xcode立即为我们提供了选择要添加的语言本地化的文件。

点击完成 ,看看发生了什么。 在项目导航器中,用于显示嵌套的按钮出现在所选文件的附近。 通过单击它们,我们看到先前选择的文件包含创建的本地化文件。

例如, Main.storyboard (Base)
是基本开发语言中的默认接口标记文件,并且在对其进行本地化时,成对创建了关联的Main.strings (Russian)
-俄罗斯本地化的字符串文件。 打开它,您将看到以下内容:
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label"; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = "TextField"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = "Button";
通常,这里一切都很简单,但是为了清楚起见,我们将更详细地考虑,注意由有爱心的Xcode生成的注释:
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = "Label";
这是UILabel
类的实例,其text
参数值为"Label"
。 ObjectID
标记文件中对象的标识符-这是在将其放置在Storyboard/Xib
上时分配给任何组件的唯一行。 密钥是由对象ObjectID
和对象的参数名称(在这种情况下为text
)组成的,记录本身可以按如下方式正式解释:
将tQe-tG-eeo对象的text参数设置为Label。
在此记录中,仅“ 值 ”可以更改。 将“ 标签 ”替换为“ 标签 ”。 我们将对其他对象执行相同的操作。
/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */ "tQe-tG-eeo.text" = ""; /* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */ "cpp-y2-Z0N.placeholder" = " "; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */ "EKl-Rz-Dc2.normalTitle" = "";
我们启动我们的应用程序。

但是我们看到了什么? 该应用程序使用基本本地化。 如何检查我们是否正确转帐?
在这里值得做一些小小的改动,并在iOS平台的功能和应用程序结构的方向上进行一些挖掘。
首先,请考虑在添加本地化的过程中更改项目的结构。 这是添加俄语本地化之前项目目录的外观:

之后:

如我们所见,Xcode创建了一个新目录ru.lproj
,在其中放置了创建的本地化字符串。

完成的iOS应用程序的Xcode项目的结构在哪里? 尽管事实上这有助于更好地了解平台的功能,以及直接在最终应用程序中分配和存储资源的原理。 最重要的是,在组装Xcode项目时,除了生成可执行文件之外,环境还将资源( 情节提要/ Xib界面布局文件,图像,行文件等)传输到完成的应用程序中,从而保留了在开发阶段指定的层次结构。
为了使用此层次结构,Apple提供了Bundle(NSBundle)
类Bundle(NSBundle)
( 免费翻译 ):
Apple使用Bundle
提供对应用程序,框架,插件和许多其他类型内容的访问。 捆绑包将资源组织到明确定义的子目录中,并且捆绑包结构因平台和类型而异。 使用bundle
,可以访问包的资源而无需了解其结构。 Bundle
是一个用于搜索元素的界面,其中考虑了包的结构,用户需求,可用的本地化以及其他相关因素。
搜索和发现资源
在开始使用资源之前,必须指定其bundle
。 Bundle
类具有许多构造函数,但main是最常用的。 Bundle.main
提供了包含当前可执行代码的目录的路径。 这样, Bundle.main
提供对当前应用程序使用的资源的访问。
考虑使用FileManager
类的Bundle.main
结构:

基于上述内容,我们可以得出以下结论:在加载应用程序时, Bundle.main
形成了Bundle.main
,并分析了当前设备的本地化(系统语言),应用程序本地化和本地化资源。 然后,应用程序从所有可用的本地化中选择一种与系统当前语言相对应的本地化,并提取相应的本地化资源。 如果没有匹配项,则使用默认目录中的资源(在我们的示例中,英语为本地化,因为英语被定义为开发语言,因此可以忽略对资源的其他本地化的需求)。 如果将设备语言更改为俄语并重新启动应用程序,则界面将已经对应于俄语本地化。

但是在关闭通过Interface Builder本地化用户界面的主题之前,值得注意另一种值得注意的方式。 创建本地化文件时(通过向项目或本地化文件检查器中添加新语言),很容易注意到Xcode提供了选择要创建的文件类型的能力:

您可以轻松地创建一个本地化的Storyboard/Xib
,而不是一个线文件,该文件将保存基本文件的所有标记。 这种方法的一大优点是,开发人员可以立即查看如何以特定语言显示内容,并立即更正屏幕的布局,尤其是在文本量不同或使用了另一个方向(例如阿拉伯语,希伯来语)的情况下,更是如此。 。 但是同时,创建其他Storyboard / Xib文件会大大增加应用程序本身的大小(同样,字符串文件占用的空间要少得多)。
因此,在选择一种或另一种接口本地化方法时,值得考虑哪种方法在特定情况下会更合适和实用。
自己动手。 在代码中使用本地化的字符串资源
希望有了静态内容,一切都差不多了。 但是直接在代码中设置的文本呢?
iOS操作系统的开发人员负责此工作。
为了使用本地化的文本资源,Foundation框架在Swift中提供了NSLocalizedStrings
系列方法。
NSLocalizedString(_ key: String, comment: String) NSLocalizedString(_ key: String, tableName: String?, bundle: Bundle, value: String, comment: String)
和Objective-C中的宏
NSLocalizedString(key, comment) NSLocalizedStringFromTable(key, tbl, comment) NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)
让我们从显而易见的地方开始。 key
参数是Strings文件中的字符串键; val
(默认值)-如果指定的密钥不在文件中,则使用默认值; comment
-(不太明显)对本地化字符串的简短描述(实际上,它不具有有用的功能,并且旨在解释使用特定字符串的目的)。
至于参数tableName
( tbl
)和bunble
,则应更详细地考虑它们。
tableName
( tbl
)是String文件的名称(说实话,我不知道Apple为什么将其称为表),该文件包含指定键所需的行; 传输时,未指定.string
扩展名。 在表之间导航的功能使您不必将字符串资源存储在一个文件中,而是可以自行决定分配它们。 这使您可以消除文件拥塞,简化编辑,并最大程度地减少错误发生的机会。
bundle
参数进一步扩展了资源导航。 如前所述, 捆绑是一种访问应用程序资源的机制,也就是说,我们可以独立确定资源的来源。
多一点。 我们将直接进入基金会,并考虑方法的声明(宏),以获得更清晰的画面,因为 绝大多数教程只是忽略了这一点。 Swift框架不是非常有用:
/// Returns a localized string, using the main bundle if one is not specified. public func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String
“主捆绑包返回本地化的字符串” -我们所拥有的。 Objective-C有点不同。
#define NSLocalizedString(key, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil] #define NSLocalizedStringFromTable(key, tbl, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
在这里,您已经可以清楚地看到,除了bundle
(在前两种情况下, mainBundle
)对字符串资源文件起作用之外,其他一切都与接口本地化情况相同。 当然,考虑到上一段中的Bundle
( NSBundle
)类,我可以立即说出来,但是那时该信息没有特别的实用价值。 但是在使用代码行的情况下,这不能说。 实际上, 基金会提供的全局功能只是标准捆绑方法的包装,其主要任务是使代码更加简洁和安全。 没有人禁止手动初始化bundle
并直接代表bundle
访问资源,但是以这种方式出现(尽管非常小)形成循环链接和内存泄漏的可能性。
下面的示例将描述如何使用全局函数和宏。
让我们看看它是如何工作的。
首先,创建一个字符串文件,其中将包含我们的字符串资源。 将其命名为Localizable.strings *并将其添加到其中。
"testKey" = "testValue";
( 字符串文件的本地化与情节提要/ Xib 完全相同 ,因此,我将不描述此过程。我们将俄语本地化文件 中的 “ testValue ” 替换 为“ test value *”。)
重要! 在iOS中,具有此名称的文件是默认的字符串资源文件,即 如果您未指定tableName
( tbl
)表名,则应用程序将自动关闭Localizable.strings
。
将以下代码添加到我们的项目中
//Swift print("String for 'testKey': " + NSLocalizedString("testKey", comment: ""))
并运行该项目。 执行代码后,控制台中将出现一行
String for 'testKey': testValue
一切正常!
与接口本地化的示例类似,更改本地化并运行应用程序。 代码执行的结果将是
String for 'testKey':
现在,让我们尝试通过键获取值,该键不在Localizable.strings
文件中:
//Swift print("String for 'unknownKey': " + NSLocalizedString("unknownKey", comment: ""))
这样的代码执行的结果将是
String for 'unknownKey': unknownKey
由于文件中没有密钥,因此该方法将返回密钥本身。 如果这样的结果是不可接受的,那么最好使用该方法
//Swift print("String for 'testKey': " + NSLocalizedString("unknownKey", tableName: nil, bundle: Bundle.main, value: "noValue", comment: ""))
其中有一个value
参数( 默认值 )。 但是在这种情况下,您必须指定资源的来源bundle
。
本地化字符串支持插值机制,类似于标准iOS字符串。 为此,请使用字符串文字( %@
, %li
, %f
等)将记录添加到字符串文件中,例如:
"stringWithArgs" = "String with %@: %li, %f";
要输出这样的行,必须添加以下形式的代码
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", 123, 123.098 ))
但是在使用此类设计时,您需要非常小心! 事实是,iOS严格监视参数的数量,参数顺序,类型与指定文字的对应关系。 因此,例如,如果您将字符串替换为第二个参数而不是整数值
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", "123", 123.098 ))
那么应用程序将用字符串“ 123”的整数代码代替不匹配的位置
"String with some: 4307341664, 123.089000"
如果您跳过它,我们得到
"String with some: 0, 123.089000"
但是,如果您跳过参数列表中与%@
对应的对象
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "123", 123.098 ))
那么应用程序只会在代码执行时崩溃。
推我,宝贝! 通知本地化
我想简单地谈一谈使用本地化的字符串资源的另一项重要任务是本地化通知。 最重要的是,大多数教程(有关Push Notifications
和Localizable Strings
)通常都忽略了这个问题,而且这样的任务并不罕见。 因此,当开发人员第一次遇到这个问题时,可能会遇到一个合理的问题: 原则上这可能吗? 我不会在这里考虑Apple Push Notification Service
操作机制,尤其是因为从iOS 10.0开始,Push和本地通知是通过相同的框架UserNotifications
。
开发多语言客户端-服务器应用程序时,您必须面对类似的问题。 当这样的任务第一次遇到我时,我想到的第一件事就是摆脱服务器端消息本地化的问题。 这个想法非常简单:应用程序在启动时将当前本地化发送到后端 ,并且服务器在发送push时选择适当的消息。 但是问题很快就解决了:如果设备的本地化发生了变化,并且应用程序没有重新启动(没有更新数据库中的数据),则服务器将发送与上一次“注册”本地化相对应的文本。 而且,如果将应用程序一次安装在具有不同系统语言的多台设备上,则整个实现将像地狱一样工作。 由于这样的解决方案对我而言似乎是最疯狂的拐杖,因此我立即开始寻找适当的解决方案(很有趣,但是在许多论坛中,“开发人员”建议完全将后端定位在本地)。
正确的决定非常简单,尽管并不完全显而易见。 代替服务器在APNS上发送的标准JSON
"aps" : { "alert" : { "body" : "some message"; }; };
需要发送形式的JSON
"aps" : { "alert" : { "loc-key" : "message localized key"; }; };
其中loc-key
用于传递Localizable.strings
文件中的本地化字符串关键字。 因此,根据设备的当前位置显示推送消息。
在推式通知中插值本地化字符串的机制以类似的方式工作:
"aps" : { "alert" : { "loc-key" : "message localized key"; "loc-args" : [ "First argument", "Second argument" ]; }; };
loc-args
键传递一个参数数组,该参数必须嵌入本地化的通知文本中。
总结一下...
因此,我们到底有什么?
- 使用简单且可访问的语法将字符串数据存储在专用
.string
文件中的标准; - 无需在代码中进行其他操作即可对接口进行本地化的能力;
- 通过代码快速访问本地化资源;
- 使用Xcode工具自动生成本地化文件并构建项目目录(应用程序)的资源;
- 本地化通知文本的能力。
, Xcode , .
.