V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
pjhubs
V2EX  ›  iOS

More-iOS 国际化一站式解决方案

  •  
  •   pjhubs · 2018-04-20 14:29:49 +08:00 · 1629 次点击
    这是一个创建于 2191 天前的主题,其中的信息可能已经有所发展或是发生改变。

    关于 iOS 开发中的国际化(也可称为多语言)在网上的文章多如牛毛,不过总结起来就那么一回事,不是说他们写的不好我写的多好,而是说过于零散。

    现在,我将结合实际场景需求进行国际化做法详解。可以肯定的是,Android 的国际化做法大同小异,无非也就是各个语言版本的文件替换,我们先来分析下真实的需求是怎么一回事。

    国际化需求:

    1. 只提供 English 和 Chinese Simplified 两种语言;
    2. App 名称跟随系统语言变化;
    3. 用户首次打开 app 时,app 的语言与系统语言保持一致(系统语言为非简体中文,默认 app 都是英文)用户手动更改语言之后,之后都记忆用户选择的语言;
    4. 用户在 App 内切换语言后,App 本身所有文本信息全部替换成对应语言。

    根据需求,我比较纠结的地方是,App 的静态文本数据可以存两份在本地,也就是 English 一份 Chinese Simplified 一份,但请求的 API 是同时返回两份中英文数据 or 分中英文两个接口?如果是要一个接口同时返回了中英文两份数据,显然会加大数据包的大小,其次用户很有可能从安装 App 的那天开始就不再切换 App 语言,甚至平均几个星期才换一次,同时返回两份数据是否多余,但是这么做几乎可以达到“无感知”数据源切换,相当于是说,一旦用户选择好了要切换语言,“啪嗒”点了完成,立马 pop 掉当前页面,然后整个 App 的数据源中英文切换可以几乎用“瞬间完成”来形容。

    如果是分中英文两个接口,实际上就会出现微信在进行语言切换时的 loading 菊花,因为要重新拉取英文版数据,不过好处是可以减少上一种做法的数据包整体大小。这两种做法我都有实践过,如果你的 App 是非常固定,不会频繁出现语言切换的需求,那么可以使用第二种;如果 App 有一天之内可能会频繁切换多次语言的情况,第一种无疑。

    经过一番探讨,虽然要供给拉美、北美和欧洲的同学使用,但是不会出现频繁切换语言的情况,所以,最终我们选择了第二种解决方案。先来看一张最终成果 gif 图,

    从上图中可以看到其实并没有对数据源进行切换,因为。。。。后台没写完😓。

    不过也不影响我们的讲解,首先,明确一个概念,我们能够做的国际化语言支持 iOS 系统中自带的所有语言,只要你能在系统设置中找到的语言,就能够对你的 App 做对应版本的国际化适配;其次,每对 App 适配一种语言,就要单创建出一个语言文件(要不然会引起冲突)。OK,我们正式进入讲解。

    首先创建一个工程

    我起名为languageTest

    工程初始化

    // AppDelegate.m
    
    #import "navOneViewController.h"
    #import "navTwoViewController.h"
    
    @interface AppDelegate ()
    
    @property (nonatomic, strong) UITabBarController *rootTabBar;
    
    @end
    
    @implementation AppDelegate
    
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
        self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
        self.rootTabBar = [[UITabBarController alloc]init];
        self.rootTabBar.delegate = (id)self;
        self.window.rootViewController = self.rootTabBar;
        [[UITabBar appearance] setBarTintColor:[UIColor whiteColor]];
    
        navOneViewController *navOneController = [navOneViewController new];
        UINavigationController *nav1 = [[UINavigationController alloc] initWithRootViewController:navOneController];
        nav1.title = @"首页";
    
        navTwoViewController *navTwoController = [navTwoViewController new];
        UINavigationController *nav2 = [[UINavigationController alloc] initWithRootViewController:navTwoController];
        nav2.title = @"发现";
    
        self.rootTabBar.viewControllers = @[nav1, nav2];
    
        [self.window makeKeyAndVisible];
    
        return YES;
    }
    
    

    Appdelegate中,我们创建一个具备基本展示功能的 tabBar 及挂载在其之上的 VC,

    // navOneViewController.m
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor blueColor];
    
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
        [self.view addSubview:label];
        label.font = [UIFont systemFontOfSize:25];
        label.textColor = [UIColor whiteColor];
        label.text = @"这是首页";
    
    }
    
    
    // navTwoViewController.m
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor orangeColor];
    
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
        [self.view addSubview:label];
        label.font = [UIFont systemFontOfSize:25];
        label.textColor = [UIColor whiteColor];
        label.text = @"这是发现";
    
        UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
        [self.view addSubview:button];
        [button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        button.backgroundColor = [UIColor blueColor];
        [button setTitle:@"改变语言" forState:UIControlStateNormal];
    
    }
    
    - (void)buttonClick {
    
    }
    
    

    在各自对应的 VC 中写下相关 UI,并预留相关 Button 点击事件即可。

    创建语言文件

    创建路径:file -> new -> file... -> String File,文件名严格命名为——“ Localizable ”,创建好该文件后,点击该文件,并打开 Xcode 的右边功能区(不知道应该叫啥),在 Localization 功能区勾选语言版本,如果此时你并未看到或者只有 English 可选,我们需要到 PROJECT -> info -> Localization,添加需要的语言。

    添加完成后,会在之前创建的 Localization.string 文件下看到多出来的语言文件,我选择了 English 和 Chinese Simplified。现在,我们已经可以在对应生成的语言文件中进行需要多语言替换的字段编写了。

    // Localizable.string/English 文件
    
    "home" = "home";
    "homeString" = "I'm home";
    
    "discover" = "discover";
    "discoverString" = "I'm discover";
    
    "change" = "change";
    
    // Localizable.string/Chinese(Simplified)文件
    
    "home" = "首页";
    "homeString" = "我是首页";
    
    "discover" = "发现";
    "discoverString" = "我是发现";
    
    "change" = "改变语言";
    

    并新建一个 pch 文件,pch 文件同样也是头文件,不过这是一个特殊头文件,是一个预编译文件,位于该文件中的所有内容,能够被其他所有源文件共享和访问,相信你也看出来了,如果在 pch 文件中写了大量的不是必须文件,则会延长编译期时间,我们可以在.pch 文件中放:

    1. 全局宏;
    2. 整个工程中都能用上的头文件;
    3. 动态更加当前 App 运行的环境切换相关宏( debug or release )。

    因此,我们需要创建一个 pch 文件去存放接下来要在整个工程中都要用到的判断语言环境的中英文宏。创建一个 pch 文件的方式为,file -> new -> file... -> 搜“ pch ”关键字,创建它。

    进入工程配置 -> TARGET -> Build Settings -> 搜 pch 关键词 -> 在“ Apple LLVM 9.0 - Language ”下的 Prefix Header 中,双击输入你的.pch 文件路径,我写的是$(SRCROOT)/PrefixHeader.pch,填写完毕,回车,会看到生成的绝对路径,确定 pch 文件路径是否正确。一切都没问题后,编译通过即可。

    在 pch 文件中,写入以下宏定义,
    ```objc
    #define AppLanguage @"appLanguage"
    #define PJLocalString(key) \
    [[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@",[[NSUserDefaults standardUserDefaults] objectForKey:@"appLanguage"]] ofType:@"lproj"]] localizedStringForKey:(key) value:@"" table:nil]
    

    首先定义了一个AppLanguage宏,推荐大家的命名更加多样化一些,因为 OC 并没有 namespace,如果我们的命名过于简单,就会导致和 Apple 本身自定义的 NSUserDefaults 默认值产生冲突。

    PJLocalString(key)这个宏“定义”了一个更长的方法,我们也都明确了一个概念,在 iOS 中的每个国际化语言,就对应着一个文件,这个文件就保存在 App 沙盒的根目录中,我们要做的就是在某个时机替换系统所采用的语言文件即可,而PJLocalString(key)这个宏所做的事情,就是替换!先从 NSUserDefaults 中取出对应的语言 key ( en 还是 zh-Hans ),根据语言 key 去索引到对应的.lproj 文件,最后把要替换的关键词传入,抛出找到的对应值(我觉得找的这个过程用的结构应该不是 hashmap,真的很快。😨)

    多语言文件有了,宏也有了,那怎么用呢?举个例子!

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor blueColor];
    
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
        [self.view addSubview:label];
        label.font = [UIFont systemFontOfSize:25];
        label.textColor = [UIColor whiteColor];
        label.text = PJLocalString(@"homeString");
    
    }
    

    只需要在多语言文字的地方调用PJLocalString()宏,传入对应 key 即可。但是此时运行工程,会发现啥都没了,是因为我们并未对 NSUserDefaults 中做当前语言的设置,这就导致了取出的值为 nil。所以,还需要在AppDelegate文件中设置初始语言,

    if(![[NSUserDefaults standardUserDefaults] objectForKey:AppLanguage]){
            [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
    

    这样,我们即可完成第一次进入 App 时初始化基础语言,如果我们想要实时更改呢?这就需要用到了通知,使用通知机制去给监听语言设置改变的监听者进行相应的处理,

    // 给 Appdelegate.m 更新以下方法
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
        if(![[NSUserDefaults standardUserDefaults] objectForKey:AppLanguage]){
            [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
    
        self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
        self.rootTabBar = [[UITabBarController alloc]init];
        self.rootTabBar.delegate = (id)self;
        self.window.rootViewController = self.rootTabBar;
        [[UITabBar appearance] setBarTintColor:[UIColor whiteColor]];
    
        navOneViewController *navOneController = [navOneViewController new];
        UINavigationController *nav1 = [[UINavigationController alloc] initWithRootViewController:navOneController];
        nav1.title = PJLocalString(@"home");
    
        navTwoViewController *navTwoController = [navTwoViewController new];
        UINavigationController *nav2 = [[UINavigationController alloc] initWithRootViewController:navTwoController];
        nav2.title = PJLocalString(@"discover");
    
        self.rootTabBar.viewControllers = @[nav1, nav2];
    
        // 新增监听方法
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
    
        [self.window makeKeyAndVisible];
    
        return YES;
    }
    
    - (void)changeLanguage:(NSNotification *)notify {
        self.rootTabBar.viewControllers[0].tabBarItem.title = PJLocalString(@"home");
        self.rootTabBar.viewControllers[1].tabBarItem.title = PJLocalString(@"discover");
    }
    
    
    // navOneViewController.m 更新以下方法
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor blueColor];
    
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
        [self.view addSubview:label];
        label.font = [UIFont systemFontOfSize:25];
        label.textColor = [UIColor whiteColor];
        label.text = PJLocalString(@"homeString");
    
        // 新增监听方法
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
    }
    
    - (void)changeLanguage:(NSNotification *)notify {
        self.label.text = PJLocalString(@"discoverString");
    }
    
    
    // navTwoViewController.m 更新以下方法
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor orangeColor];
    
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 20)];
        [self.view addSubview:self.label];
        self.label.font = [UIFont systemFontOfSize:25];
        self.label.textColor = [UIColor whiteColor];
        self.label.text = PJLocalString(@"discoverString");
    
        self.button = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
        [self.view addSubview:self.button];
        [self.button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        self.button.backgroundColor = [UIColor blueColor];
        [self.button setTitle:PJLocalString(@"change") forState:UIControlStateNormal];
    
        // 新增监听方法
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeLanguage:) name:@"changeLanguage" object:nil];
    }
    
    - (void)changeLanguage:(NSNotification *)notify {
        self.label.text = PJLocalString(@"discoverString");
        [self.button setTitle:PJLocalString(@"change") forState:UIControlStateNormal];
    }
    
    - (void)buttonClick {
        [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
        [[NSUserDefaults standardUserDefaults] synchronize];
    
        // 同步完 NSUserDefault 后,发送语言更改通知
        [[NSNotificationCenter defaultCenter] postNotificationName:@"changeLanguage" object:nil];
    }
    

    编译运行吧,见证奇迹的时刻到了~点击“更改语言” button,怎么样,是不是瞬间全都改过了。。😝

    但是现在只完成了第一和第四个需求,我们接着来完成第三个需求,“用户首次打开 app 时,app 的语言与系统语言保持一致(系统语言为非简体中文,默认 app 都是英文)用户手动更改语言之后,之后都记忆用户选择的语言”。

    分析一下,该需求的重点在于用户第一次打开 App 时整体 App 语言设置跟随系统语言设置,非简体中文之外的语言,都设置成英文,因此,我们需要对AppDelegate.m文件进行改造,

    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
        // App 第一次启动跟随系统语言设置
        if(![[NSUserDefaults standardUserDefaults] boolForKey:@"firstLaunch"]){
            [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"firstLaunch"];
            NSArray *allLanguages = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
            NSString *preferredLanguage = allLanguages[0];
            if([preferredLanguage rangeOfString:@"zh-Hans"].location != NSNotFound) {
                [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hans" forKey:AppLanguage];
            } else {
                [[NSUserDefaults standardUserDefaults] setObject:@"en" forKey:AppLanguage];
            }
        }
    
        ........
    
    

    接下来完成最后一个需求,把 App 的名称也做国际化适配,如果你之前有过在info.plist文件中修改过 App 的名字,我们现在要做的事情同样也是改名字,而且是针对info.plist整个文件做国际化,同样新建一个string file文件,命名严格填写为infoPlist.strings,并且在 Xcode 的右边拓展栏中选择Localizable,点击生成 English 和 Chinese Simplified 多语言文件

    // 在 English 中写下
    CFBundleName = "your english name";
    CFBundleDisplayName = "your english name";
    
    
    // 在 Chinese Simplified 中写下
    CFBundleName = "你的中文名";
    CFBundleDisplayName = "你的中文名";
    
    

    OK,以上就是本篇文章所要表达的所有内容,当然这些都是 demo 级别的 code,如果此文对你有帮助,记得对其进行多多改造!

    demo 地址: https://github.com/windstormeye/iOSMorePractices/tree/master/languageTest

    原文链接:pjhubs.com

    最近自己在总结从大一到现在 iOS 开发经验内容,目前正在编写 UITableView、Objective-C 语言使用、iOS 与 OpenCV 结合、iOS 与 Cocos2D-X 结合等内容,因为目前只有我自己一个人在总结,后续会一块拉小伙伴们持续输出高质量总结文章哒~希望大家能够喜欢,能够帮助到大家!

    https://github.com/windstormeye/iOS-Course

    2 条回复    2018-04-20 15:12:39 +08:00
    so898
        1
    so898  
       2018-04-20 14:57:14 +08:00
    有点睿智,建议学习一下 OBJC Extension 的使用
    pjhubs
        2
    pjhubs  
    OP
       2018-04-20 15:12:39 +08:00
    @so898 是的,这个实现方法有考虑过,不过当时需求上来后,觉得国际化应该是往简单的方向走,我个人比较喜欢用已掌握的知识去解决问题。

    不过,非常的感谢你能够再次提醒我。(づ ̄ 3  ̄)づ╭❤~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2443 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:54 · PVG 23:54 · LAX 08:54 · JFK 11:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.