写点什么

iOS 文本的多语言适配和实践

2021 年 9 月 13 日

iOS文本的多语言适配和实践

背景

产品被多个国家使用,产品方希望产品拥有更好的多语言使用体验,所以设计师提供多种字体来适配指定的语言。基于以上背景,客户端需要快速给出解决方案并且上线。

字体包的多语言适配和实践

需求分析

首先,在了解产品需求和设计方案之后,结合业务研发人员的痛点,整理出以下需求。

  1. 产品和设计的需求

  • 不同语言,对应字体包不相同。

  • 全局字体默认使用设计师指定的字体包。

  • 某些语言的字体包缺少某些字重版本,要求降级使用下一个字重版本。

  • 存在某些特殊文案不使用全局字体包(例如:中文,它有专属的字体包,和语言环境无关)。

  • 产品迭代需要快速支持扩展,尽量减少研发投入成本。

设计师要求的字体包资源



  1. 研发的痛点和需求

  • 存在公用组件(其他业务线都在使用,伴鱼公共业务组件目前有 50+),不能修改通用组件。

  • 仅壳工程支持且依赖字体包。

  • 字体包资源来源方式要灵活。

总结一下,产品和设计的需求强调字体适配的全局性、多样性、可扩展性,研发关心的是解耦、职责单一、灵活性。

技术设计

分析过后,先确定技术框架的分层。


垂直分层和水平模块



如图所示分 3 层,1.基础组件提供核心实现,并支持需求扩展 2.业务组件(无相关修改)3.壳工程提供资源包和代理者。


FontPackage 组件要负责什么?

  • FontPackageManager,负责绑定代理来获取资源包,控制流程逻辑。

  • FontPackageExtension,负责 AOP,增加文本属性来满足特殊场景的多样性。

  • FontPackageModel,映射字体包资源的配置信息,明确了使用协议。上层业务可以增加和调整参数来配置字体包资源。


壳工程的资源包配置

  • env:国际编码, default 表示设计师指定的默认字体。注意有些国际编码代表一种语言,例如英语存在 en-US、en-GB 等多种编码,需要统一为 en。

  • font:字重类型,0:light、1:medium、2:bold。斜体默认替换为 medium

  • name:字体源文件的名称。例如:GothamRndSSm-Medium

备注:因为设计师只要求 3 种字重,默认 light 字重,这个和系统提供的 UIFontWeight 不太一致。

//壳工程中的配置文件,反序列化传回FontPackage层//appfont.json{    "list": [{        "env" : "vi",        "note" : "越南语,按照国际编码:vi、vi-VN。FontPackageManager 判断国际编码来对应",        "data" : [{                "font" : 0,                "name" : "genjyuu_light(越南细)"            },{                "font" : 1,                "name" : "genjyuu_medium(越南中)"            },{                "font" : 2,                "name" : "genjyuu_bold(越南粗)"            }        ]    }, {        "env" : "default",        "note" : "其他语种默认使用字体,但优先判断设备的国际编码来匹配字体包",        "data" : [{                "font" : 0,                "name" : "GothamRndSSm-Light"            },{                "font" : 1,                "name" : "GothamRndSSm-Medium"            },{                "font" : 2,                "name" : "GothamRndSSm-Bold"            }        ]    }]}
复制代码


添加字体包和配置文件,还有冷启动流程:

冷启动流程图



技术开发

FontPackage 功能组件共 3 个 Class,200+行代码。首先,在冷启动时候 FontPackage 根据 json 配置缓存语言编码匹配到的字体包资源 Model。然后使用 runtime hook UIFont 类的几个构造函数,更换构造函数的 fontName 参数。目前确定 5 个构造函数:

//已处理+ (UIFont *)systemFontOfSize:(CGFloat)fontSize;+ (UIFont *)systemFontOfSize:(CGFloat)fontSize weight:(UIFontWeight)weight;+ (UIFont *)boldSystemFontOfSize:(CGFloat)fontSize;+ (UIFont *)italicSystemFontOfSize:(CGFloat)fontSize;+ (UIFont *)fontWithName:(NSString *)fontName size:(CGFloat)fontSize;
复制代码


最后统一使用 +fontWithName:size: 函数初始化,fontName 为自定义字体包。函数 -fontpackage_name: 根据原 fontName 更换为对应的自定义字体包。


//FontPackageExtension.m //UIFont+FontPackage.m


+ (UIFont *)xxxFontPackage_systemFontOfSize:(CGFloat)fontSize weight:(UIFontWeight)weight { NSString *fontName = @""; if (weight == UIFontWeightMedium) { fontName = @"medium"; } else if (weight > UIFontWeightMedium) { fontName = @"bold"; } return [self fontWithName:fontName size:fontSize];}
+ (UIFont *)xxxFontPackage_italicSystemFontOfSize:(CGFloat)fontSize { //斜体默认是medium return [self fontWithName:@"medium" size:fontSize];}
+ (UIFont *)xxxFontPackage_boldSystemFontOfSize:(CGFloat)fontSize { return [self fontWithName:@"bold" size:fontSize];}
+ (UIFont *)xxxFontPackage_systemFontOfSize:(CGFloat)fontSize { return [self fontWithName:@"" size:fontSize];}
+ (UIFont *)xxxFontPackage_fontWithName:(NSString *)fontName size:(CGFloat)fontSize { fontName = [self fontpackage_name:fontName]; return [self xxxFontPackage_fontWithName:fontName size:fontSize];}


+ (NSString *)fontpackage_name:(NSString *)fontName { fontName = [fontName lowercaseString]; FontPackageFont replaceFont = FontPackageFontLight; //默认light if ([fontName containsString:@"medium"]) { replaceFont = FontPackageFontMedium; } else if ([fontName containsString:@"bold"]) { replaceFont = FontPackageFontBold; } //匹配替换的字体 NSString *replaceFontName = [[FontPackageManager shareInstance].fontPackageInfo.dataMap objectForKey:@(replaceFont)]; return replaceFontName;}
复制代码

文本信息的多语言适配和实践

针对海外用户做语言本地化也是一项重要的产品功能,但很多组件在开发之初并未预留本地化拓展的接口,客户端需要提供一套优雅的解决方案来应对此问题。

需求分析

1、产品和设计的需求

  • 语言本地化

  • 未提供本地化的语言,默认使用产品指定的语言

  • 快速支持新语言本地化

2、技术要求

  • 接入成本低,不需要对成熟组件做改动

  • 解耦,其他组件无需依赖本功能

技术设计

垂直分层和水平模块



如图所示分 3 层:1、基础组件提供需求扩展 2、业务组件(基本不需要修改,如有特殊属性需求可以依赖基础组件)3、壳工程提供资源包和以及资源包的更新

LocalizedString 组件要负责什么?

  • LocalizedString,负责文字本地化适配。

  • LocalizedTool,负责语言包的配置、读取、更换功能。

  • LocalizedExtension,负责 AOP,补充某些属性。

语言包目录如下:

语言包目录



可以看到,语言包是按照语言码进行命名的,方便在使用中及时定位到对应文件并读取(存在多种编码的语言,统一使用其基础类)。同时,在壳工程中会对本地语言包进行刷新,App 启动后会检查是否有新的语言包可用,如果有会保证数据同步。

配置好语言包后,接下来需要冷启动时初始化 LocalizedString 组件。启动时组件任务流程图如下:

冷启动流程图



技术开发

考虑到字符串最终都会依托于 UILabel 进行展示,[UILabel setText:]会作为设置展示文本的唯一收口。所以我们对[UILabel setText:]进行了 hook 和拓展,其内部操作流程图如下:

AOP 流程图



LocalizedString 组件有 NSString、UILabel 分类分别做了属性拓展。具体代码如下:

@interface UILabel (Localized)@property (nonatomic, assign) BOOL isAutoLocalized; ///< 设置的文字是否要自动转换成本地化的语言,默认YES@end
@interface NSString (Localized)@property (nonatomic, copy) NSString *oriStr; ///< 上次本地化的字符串原始值@property (nonatomic, copy) NSString *localizedStr; ///< oriStr 本地化后的字符串@end
复制代码

对 UILabel 的分类拓展可以判断 Label 是否需要被本地化;对 NSString 的分类拓展会对本地化后的结果进行缓存,当同一个 string 对象再次本地化时,可以快速从缓存拿到结果减少在 map 中的检索次数、提高效率。类拓展的方式也保证了本组件的侵入性极低。整个工程使用了 pod 进行集成,基础组件无需声明依赖,对本组件有依赖要求的只在特定业务中出现。hook + pod 的方式保证了本组件的灵活使用和充分解耦。


与 NSLocalizedString 的兼容


从上面的流程介绍可以看到,本地化替换发生在对 Label 设置文本的时候,不同于 NSLocalizedString 需要先显式本地化再设置文本的方式。所以,当使用方提前对文本进行了本地化,本组件的自动本地化不生效。考虑到本组件主要应用于新语言地区,NSLocalizedString 尚未配置对应的结果,故目前仍然可以使用本组件兜底。我们也会后续优化本组件,完成与 NSLocalizedString 的兼容,更加方便本组的使用。

拓展

由于上述方法只适用于[UILabel setText:]这种形式的无侵入调整,对于字符串拼接的情况,仍需要开发人员使用 LocalizedString 类对子串进行逐一本地化。同时,为了支持以后可能的应用内变更语言,LocalizedString 也提供了动态变更语言包功能。LocalizedString 主要 API 如下:


/** @brief 直接返回指定 key 对应的 本地化文字 @param key 转译文件表中的key */+ (NSString *)forKey:(NSString *)key;
/** @brief 根据指定的 language code,返回key 对应的 本地化文字 @param key 转译文件表中的key @param langCode语言编码 */+ (NSString *)forKey:(NSString *)key langCode:(NSString *)langCode;
/** @brief 设置当前默认的语言编码 @param langCode语言编码 */+ (void)setCurrentLangCode:(NSString *)langCode;
复制代码


总结

在多个产品同时的迭代情况下,使用组件化已经变得非常普遍,不断地重构优化组件来保证低耦合。当面对国际化场景时,需要沉淀打磨国际化适配框架来支撑业务高效迭代,并且不能给其他业务造成负担。目前以上功能都已上线,满足了产品的需求,解决了研发的痛点。

作者介绍

  • 吕洪阳,伴鱼 iOS 工程师,伴鱼绘本 iOS 端负责人

  • 赵杰,伴鱼 iOS 工程师,负责伴鱼绘本客户端研发,功能降级框架等工作

参考

https://developer.apple.com/documentation/uikit/uifonthttp://www.lingoes.cn/zh/translator/langcode.htm


作者:吕洪阳、赵杰

原文:https://tech.ipalfish.com/blog/2021/08/29/reading_ios_internationlization/

原文:iOS 文本的多语言适配和实践

来源:伴鱼技术博客

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 9 月 13 日 07:391

评论

发布
暂无评论
发现更多内容

java 通过 SmbFile 类操作共享文件夹,膜拜大牛

JVM调优资料

Java 程序员 面试 后端

Java内联函数,SpringBoot集成Redis集群

程序猿一枚

Java 程序员 面试 后端

Apache APISIX 2.8 正式发布,带来更多新功能!

Apache APISIX 中国社区

开源 网关 APISIX

J2EE基础集合框架之Map集合,公司初级程序员Java面试题

JVM调优资料

Java 程序员 面试 后端

思想的懒惰

箭上有毒

8月日更

Dubbo如何支持本地调用?InJvm方式解析,阿里官方推荐

Geek_f90455

Java 程序员 面试 后端

C#索引器的实现,黑马程序员Java基础入门

Geek_f90455

Java 程序员 面试 后端

Vue深入学习3—数据响应式原理

魁首

GitHub-标星过万!腾讯技术官发布的,腾讯Java社招面试经验

Geek_f90455

Java 程序员 面试 后端

Java反射机制的那些事儿,Java基础必背知识点

程序猿一枚

Java 程序员 面试 后端

常见的静态负载均衡算法

HQ数字卡

算法 8月日更

Java个人学习之旅(第十天),黑马程序员Java全套视频

JVM调优资料

Java 程序员 面试 后端

Java中高级核心知识全面解析(4),Java开发中遇到最难的问题

JVM调优资料

Java 程序员 面试 后端

C#位运算,面试要掌握这几个关键点

Geek_f90455

Java 程序员 面试 后端

ELK性能优化实战总结:我强任我强,美的Java面试题

Geek_f90455

Java 程序员 面试 后端

HashMap源码解析,Java黑马程序员资源

Geek_f90455

Java 程序员 面试 后端

JavaScript是解释型语言--V8、JIT,Java重点知识大全

JVM调优资料

Java 程序员 面试 后端

apk瘦身;如何缩小体积呢,Java面试笔试题及答案

欢喜学安卓

Java 程序员 面试 后端

Hystrix-服务容错处理:什么是Hystrix,鲁班学院二期

Geek_f90455

Java 程序员 面试 后端

iOS开发:Block传值的运用

三掌柜

8月日更 8 月日更 8月

IDEA的中文注释引起的程序报错!,那些被大厂优化的程序员们

JVM调优资料

Java 程序员 面试 后端

Java 反射 getClass(),最新Java大厂高频面试题

JVM调优资料

Java 程序员 面试 后端

Java中高级核心知识全面解析,张孝祥Java基础视频教程

程序猿一枚

Java 程序员 面试 后端

Java多态实现原理,分布式系统中ACID和CAP有什么区别

程序猿一枚

Java 程序员 面试 后端

Git-如何优雅地回退代码,字节跳动内部学习资料泄露

Geek_f90455

Java 程序员 面试 后端

保持信心

Nydia

Dagger2源码分析(二,深入理解Java虚拟集百度云

Geek_f90455

Java 程序员 面试 后端

java springboot,大专生面试阿里P7居然过了

JVM调优资料

Java 程序员 面试 后端

Java 集合容器篇面试题(上,Java中高级程序员进阶

JVM调优资料

Java 程序员 面试 后端

Java-进阶:集合框架2,熬夜整理华为最新Java笔试题

JVM调优资料

Java 程序员 面试 后端

ArrayList(Java8,阿里大牛把「服务雪崩」玩到了极致

Geek_f90455

Java 程序员 面试 后端

iOS文本的多语言适配和实践-InfoQ