iOS开发基础78-iOS 国际化

iOS 国际化完全指南:从文本到 App 名称、手动切换语言与 RTL 适配

国际化(Internationalization,简称 i18n)是让 App 支持多种语言和地区的技术。本地化(Localization,简称 L10n)是把 App 翻译成具体语言的过程。本文从最基础的 Localizable.strings 讲起,系统覆盖文本、App 名称、Storyboard、图片、日期数字格式化、手动切换语言、RTL 适配等完整流程,并给出可直接使用的工具类封装。


一、国际化概述

1. 国际化 vs 本地化

概念 简称 说明
国际化 i18n(Internationalization,i 和 n 之间 18 个字母) 让 App 具备支持多语言的能力和架构,是技术层面的准备
本地化 L10n(Localization,L 和 n 之间 10 个字母) 把 App 翻译成具体某种语言,是内容层面的翻译

简单说:国际化是搭好框架,本地化是往框架里填具体语言的内容。

2. 国际化包含哪些内容

一个完整的国际化 App,需要处理以下内容:

内容 说明
文本 按钮文字、标签、提示语、错误信息等(Localizable.strings)
App 名称 桌面图标下的显示名称(InfoPlist.strings)
Storyboard/XIB 界面上的静态文本
图片/资源 包含文字的图片、启动图等
日期/时间 不同地区的日期格式(如 2026/09/01 vs 01/09/2026)
数字/货币 千分位、小数点、货币符号(如 $ vs ¥ vs €)
RTL 适配 阿拉伯语、希伯来语等从右到左的语言,界面需要翻转
权限描述 相机、定位、相册等系统权限的弹窗文案

二、项目配置与语言支持

1. 添加语言支持

在 Xcode 中添加多语言支持:

  1. 选中项目(PROJECT,不是 TARGET)→ Info 标签页。
  2. 找到 Localizations 区域,点击 + 号。
  3. 选择要添加的语言(如 Chinese (Simplified)Chinese (Traditional)English)。
  4. 弹出对话框选择需要本地化的文件(Storyboard、LaunchScreen 等),点击 Finish

添加后,Localizations 区域会显示已支持的语言列表,Base 是基础国际化(默认用 Base 的资源)。

2. 常用语言代码表

语言 代码 说明
简体中文 zh-Hans 中国大陆、新加坡
繁体中文 zh-Hant 台湾、香港、澳门(注意:不是 zh-TW 或 zh-HK,那是地区代码)
英语 en 美国、英国、澳大利亚等
日语 ja 日本
韩语 ko 韩国
法语 fr 法国、加拿大
德语 de 德国、奥地利、瑞士
西班牙语 es 西班牙、拉丁美洲
俄语 ru 俄罗斯
葡萄牙语 pt 葡萄牙、巴西
意大利语 it 意大利
阿拉伯语 ar RTL 语言,需要界面翻转
希伯来语 he RTL 语言
泰语 th 泰国
越南语 vi 越南
印尼语 id 印度尼西亚

注意:繁体中文的语言代码是 zh-Hant,不是 zh-TW(台湾)或 zh-HK(香港)。zh-Hant 覆盖所有繁体中文地区,是最常用的代码。


三、文本国际化(Localizable.strings)

文本国际化是最基础、最常用的国际化方式。

1. 创建 Localizable.strings

  1. Xcode 菜单:FileNewFile → 选择 Strings File
  2. 文件名必须是 Localizable.strings(这是 NSLocalizedString 默认查找的文件名,也可以用其他名字,但需要指定 table)。
  3. 保存到项目目录。

2. Localize 并添加语言

  1. 选中 Localizable.strings 文件。
  2. 右侧 File Inspector 中点击 Localize... 按钮。
  3. 选择 Base(或直接选择一种语言),点击 Localize
  4. 再次在 File Inspector 中勾选需要支持的语言(简体中文、繁体中文、英文等)。
  5. Xcode 会为每种语言生成一个对应的 Localizable.strings 文件,点击文件左侧的展开箭头可以看到所有语言版本。

3. 编写键值对

每个语言的 .strings 文件中,格式为 "键" = "值";,注意末尾必须有分号:

Localizable.strings (English)

"home_title" = "Home";
"login_button" = "Login";
"welcome_message" = "Hello, %@!";
"item_count" = "You have %d items";

Localizable.strings (Chinese (Simplified))

"home_title" = "首页";
"login_button" = "登录";
"welcome_message" = "你好,%@!";
"item_count" = "你有 %d 件商品";

Localizable.strings (Chinese (Traditional))

"home_title" = "首頁";
"login_button" = "登入";
"welcome_message" = "你好,%@!";
"item_count" = "你有 %d 件商品";

格式说明

  • 键名建议用 模块_含义 的命名方式(如 home_titlelogin_button),避免冲突。
  • 值中可以用 %@(对象)、%d(整数)、%f(浮点数)等占位符,和 NSString stringWithFormat: 用法一致。
  • 值中包含双引号时需要转义:"quote_example" = "他说:\"你好\"";
  • 值中包含换行时用 \n
  • 可以用 /* 注释 */ 添加注释说明这个键的用途,方便翻译人员理解。

4. NSLocalizedString 宏的使用

// 基本用法:从 Localizable.strings 读取
NSString *title = NSLocalizedString(@"home_title", nil);
self.titleLabel.text = title;

// 带注释(第二个参数是注释,给翻译人员看的,不影响运行)
NSString *loginText = NSLocalizedString(@"login_button", @"登录按钮的文字");

// 带占位符
NSString *welcome = [NSString stringWithFormat:NSLocalizedString(@"welcome_message", nil), @"张三"];
// 简体中文:你好,张三!

NSString *countText = [NSString stringWithFormat:NSLocalizedString(@"item_count", nil), 5];
// 简体中文:你有 5 件商品

5. 其他本地化宏

说明
NSLocalizedString(key, comment) 从默认的 Localizable.strings 读取
NSLocalizedStringFromTable(key, tbl, comment) 从指定的 .strings 文件(table)读取
NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) 从指定 bundle 的指定 table 读取
NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) 找不到时返回默认值 val
// 从 MyStrings.strings 文件读取(table 是文件名,不含 .strings 后缀)
NSString *text = NSLocalizedStringFromTable(@"my_key", @"MyStrings", nil);

四、App 名称国际化(InfoPlist.strings)

App 在桌面图标下显示的名称,也可以根据语言不同而不同。

1. 创建 InfoPlist.strings

  1. FileNewFileStrings File
  2. 文件名必须是 InfoPlist.strings(固定名称,系统会自动识别)。
  3. 和 Localizable.strings 一样,点击 Localize... 并勾选所有语言。

2. 配置 App 显示名称

在每种语言的 InfoPlist.strings 中添加:

InfoPlist.strings (English)

CFBundleDisplayName = "My App";

InfoPlist.strings (Chinese (Simplified))

CFBundleDisplayName = "我的应用";

InfoPlist.strings (Chinese (Traditional))

CFBundleDisplayName = "我的應用";

CFBundleDisplayName 是 App 显示名称的 key。注意 Info.plist 中需要有 CFBundleDisplayName 键(值可以留空或填默认名称),InfoPlist.strings 的国际化才会生效。

3. 其他 Info.plist 键的国际化

所有用户可见的 Info.plist 字符串都可以在 InfoPlist.strings 中国际化,最常见的是权限描述:

NSCameraUsageDescription = "需要访问相机来拍摄照片";
NSPhotoLibraryUsageDescription = "需要访问相册来选择图片";
NSLocationWhenInUseUsageDescription = "需要获取位置来显示附近的内容";
NSMicrophoneUsageDescription = "需要访问麦克风来录制语音";
NSContactsUsageDescription = "需要访问通讯录来添加好友";

五、Storyboard/XIB 国际化

Storyboard 和 XIB 中的静态文本(如按钮标题、标签文字)也需要国际化,有两种方式。

方式一:Base 国际化(自动生成 .strings)

这是 Xcode 提供的自动方式:

  1. 选中 Storyboard/XIB 文件。
  2. 右侧 File Inspector 中点击 Localize...,选择 Base
  3. 勾选需要支持的语言。
  4. Xcode 会为每种语言生成一个对应的 .strings 文件(如 Main.strings),里面包含 Storyboard 中所有 UI 元素的文本。

生成的 Main.strings 大致如下:

/* Class = "UILabel"; text = "Title"; ObjectID = "abc-123"; */
"abc-123.text" = "标题";

/* Class = "UIButton"; normalTitle = "Login"; ObjectID = "def-456"; */
"def-456.normalTitle" = "登录";

缺点:Storyboard 修改后(如新增 UI 元素),对应的 .strings 文件不会自动更新,需要手动添加或用工具导出,维护成本较高。

方式二:代码设置(推荐)

用 IBOutlet 引用 UI 元素,在 viewDidLoad 中用 NSLocalizedString 设置文本:

@interface HomeViewController ()
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *loginButton;
@end

@implementation HomeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.titleLabel.text = NSLocalizedString(@"home_title", nil);
    [self.loginButton setTitle:NSLocalizedString(@"login_button", nil) forState:UIControlStateNormal];
    self.navigationItem.title = NSLocalizedString(@"home_title", nil);
}

@end

优点

  • 灵活,Storyboard 修改不影响国际化文本。
  • 可以统一管理所有文本键。
  • 手动切换语言时可以动态更新。

实际开发中推荐用方式二(代码设置),维护成本低,且支持手动切换语言。


六、图片/资源国际化

包含文字的图片(如启动图、引导页图片、带文字的按钮图)也需要国际化。

方式一:Assets.xcassets 语言变体(Xcode 13+ 推荐)

Xcode 13 及以后,Assets.xcassets 支持为图片添加语言变体:

  1. 选中 Assets.xcassets 中的图片。
  2. 右侧 Attributes Inspector 中,找到 Localization 区域,勾选需要的语言。
  3. 图片会展开为多个语言槽位,分别拖入对应语言的图片。

方式二:不同语言的图片文件

旧版本 Xcode 或不使用 Assets 时,可以把不同语言的图片放在对应的 .lproj 目录中:

MyApp/
  en.lproj/
    welcome_image.png
  zh-Hans.lproj/
    welcome_image.png
  zh-Hant.lproj/
    welcome_image.png

[UIImage imageNamed:@"welcome_image"] 加载时,系统会自动根据当前语言选择对应目录的图片。

注意事项

  • 尽量避免在图片中包含文字,用纯图片 + 代码叠加文字的方式,减少国际化成本。
  • 启动图(LaunchScreen)建议用纯代码或 Auto Layout 布局,不要用包含文字的图片。
  • 图标(App Icon)一般不需要国际化,保持统一。

七、手动切换语言(重点)

默认情况下,App 跟随系统语言变化。但很多 App 希望在应用内提供语言切换功能(如"我的 → 设置 → 语言"),不需要用户去系统设置里切换。

1. 手动切换的原理

NSLocalizedString 底层是从 [NSBundle mainBundle] 中读取对应语言的 .strings 文件。手动切换语言的核心是:

  1. 把用户选择的语言保存到 NSUserDefaults(key 为 AppleLanguages)。
  2. 创建一个指向对应语言 .lproj 目录的自定义 NSBundle
  3. 用自定义 bundle 的 localizedStringForKey:value:table: 方法读取字符串,替代 NSLocalizedString
  4. 切换后发通知,所有页面重新设置文本,或直接重建根控制器。

2. 完整工具类实现

LanguageManager.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// 语言切换完成的通知
extern NSNotificationName const kLanguageDidChangeNotification;

@interface LanguageManager : NSObject

/// 当前语言代码(如 zh-Hans、en、ja)
@property (nonatomic, copy, readonly) NSString *currentLanguage;

/// 当前 bundle(用于读取本地化字符串)
@property (nonatomic, strong, readonly) NSBundle *currentBundle;

/// 单例
+ (instancetype)sharedManager;

/// 获取本地化字符串
/// @param key 键
/// @param table .strings 文件名(nil 则用默认 Localizable.strings)
- (NSString *)localizedStringForKey:(NSString *)key table:(nullable NSString *)table;

/// 快捷方法(从默认 Localizable.strings 读取)
- (NSString *)localizedStringForKey:(NSString *)key;

/// 切换语言
/// @param language 语言代码(如 zh-Hans、en)
- (void)setLanguage:(NSString *)language;

/// 获取系统当前语言
- (NSString *)systemLanguage;

/// 支持的语言列表
- (NSArray<NSString *> *)supportedLanguages;

@end

NS_ASSUME_NONNULL_END

LanguageManager.m

#import "LanguageManager.h"

NSNotificationName const kLanguageDidChangeNotification = @"kLanguageDidChangeNotification";

static NSString *const kUserLanguageKey = @"AppleLanguages";
static NSString *const kDefaultLanguage = @"en";

@interface LanguageManager ()
@property (nonatomic, copy, readwrite) NSString *currentLanguage;
@property (nonatomic, strong, readwrite) NSBundle *currentBundle;
@end

@implementation LanguageManager

+ (instancetype)sharedManager {
    static LanguageManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[LanguageManager alloc] init];
    });
    return instance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        [self loadCurrentLanguage];
    }
    return self;
}

#pragma mark - 初始化

- (void)loadCurrentLanguage {
    // 优先读取用户手动选择的语言
    NSArray *languages = [[NSUserDefaults standardUserDefaults] objectForKey:kUserLanguageKey];
    NSString *language = languages.firstObject;
    
    // 如果没有手动选择,用系统语言
    if (!language) {
        language = [self systemLanguage];
    }
    
    // 确保语言在支持列表中
    if (![[self supportedLanguages] containsObject:language]) {
        language = kDefaultLanguage;
    }
    
    [self setupLanguage:language];
}

- (void)setupLanguage:(NSString *)language {
    self.currentLanguage = language;
    
    // 找到对应语言的 .lproj 路径,创建自定义 bundle
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    if (path) {
        self.currentBundle = [NSBundle bundleWithPath:path];
    } else {
        self.currentBundle = [NSBundle mainBundle];
    }
}

#pragma mark - 公共方法

- (NSString *)localizedStringForKey:(NSString *)key table:(NSString *)table {
    if (!key) {
        return @"";
    }
    // 从自定义 bundle 读取,找不到则返回 key 本身
    NSString *value = [self.currentBundle localizedStringForKey:key value:key table:table];
    return value;
}

- (NSString *)localizedStringForKey:(NSString *)key {
    return [self localizedStringForKey:key table:nil];
}

- (void)setLanguage:(NSString *)language {
    if (!language || [language isEqualToString:self.currentLanguage]) {
        return;
    }
    
    // 保存到 NSUserDefaults(AppleLanguages 是系统识别的 key)
    [[NSUserDefaults standardUserDefaults] setObject:@[language] forKey:kUserLanguageKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    // 更新当前语言和 bundle
    [self setupLanguage:language];
    
    // 发送通知,让所有页面刷新 UI
    [[NSNotificationCenter defaultCenter] postNotificationName:kLanguageDidChangeNotification object:nil];
}

- (NSString *)systemLanguage {
    NSString *language = [NSLocale preferredLanguages].firstObject;
    // 处理地区代码,如 zh-Hans-CN → zh-Hans,en-US → en
    if ([language hasPrefix:@"zh-Hans"]) {
        return @"zh-Hans";
    } else if ([language hasPrefix:@"zh-Hant"] || [language hasPrefix:@"zh-TW"] || [language hasPrefix:@"zh-HK"]) {
        return @"zh-Hant";
    } else if ([language hasPrefix:@"en"]) {
        return @"en";
    } else if ([language hasPrefix:@"ja"]) {
        return @"ja";
    } else if ([language hasPrefix:@"ko"]) {
        return @"ko";
    }
    return language;
}

- (NSArray<NSString *> *)supportedLanguages {
    return @[@"en", @"zh-Hans", @"zh-Hant", @"ja", @"ko"];
}

@end

3. 使用工具类

#import "LanguageManager.h"

// 获取本地化字符串(替代 NSLocalizedString)
NSString *title = [[LanguageManager sharedManager] localizedStringForKey:@"home_title"];

// 切换语言(如在设置页面点击"简体中文")
[[LanguageManager sharedManager] setLanguage:@"zh-Hans"];

4. 切换后刷新 UI

切换语言后,已经显示的页面不会自动更新文本,有两种刷新方式:

方式一:监听通知,重新设置文本(推荐)

在基类控制器中监听语言切换通知,子类重写刷新方法:

// BaseViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(languageDidChange:)
                                                 name:kLanguageDidChangeNotification
                                               object:nil];
    [self refreshLocalizedText];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)languageDidChange:(NSNotification *)notification {
    [self refreshLocalizedText];
}

// 子类重写此方法,重新设置所有文本
- (void)refreshLocalizedText {
    // 子类实现
}

子类中:

- (void)refreshLocalizedText {
    self.titleLabel.text = [[LanguageManager sharedManager] localizedStringForKey:@"home_title"];
    [self.loginButton setTitle:[[LanguageManager sharedManager] localizedStringForKey:@"login_button"]
                      forState:UIControlStateNormal];
    self.navigationItem.title = [[LanguageManager sharedManager] localizedStringForKey:@"home_title"];
}

方式二:重建根控制器(简单粗暴)

切换语言后,直接重新设置 UIWindowrootViewController,所有页面重新创建:

- (void)setLanguage:(NSString *)language {
    // ... 保存语言、更新 bundle ...
    
    // 重建根控制器
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *rootVC = [storyboard instantiateInitialViewController];
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    keyWindow.rootViewController = rootVC;
    [keyWindow makeKeyAndVisible];
}

方式二实现简单,但会丢失当前页面状态(如用户输入的内容、滚动位置),适合页面层级简单的 App。方式二更灵活,推荐用方式一。

5. 注意事项

  • App 启动时设置:在 AppDelegatedidFinishLaunchingWithOptions 中,LanguageManager 单例初始化时就会加载用户选择的语言,确保启动时就是正确的语言。
  • 系统语言变化:如果用户没有手动选择语言,App 跟随系统语言。可以在 App 进入前台时检查系统语言是否变化。
  • 重启生效:如果用 NSLocalizedString(而不是自定义 bundle),切换 AppleLanguages 后需要重启 App 才生效。用自定义 bundle 的方式可以即时生效。
  • TabBar 标题UITabBarItem 的标题在控制器创建时设置,切换语言后需要重新设置或重建 TabBar。

八、日期与数字格式化本地化

不同地区的日期、数字、货币格式不同,不能硬编码格式。

1. 日期格式化(NSDateFormatter)

NSDate *date = [NSDate date];

// ❌ 错误:硬编码格式,所有语言都一样
NSDateFormatter *wrongFormatter = [[NSDateFormatter alloc] init];
wrongFormatter.dateFormat = @"yyyy-MM-dd HH:mm";
NSString *wrongString = [wrongFormatter stringFromDate:date];
// 所有地区都是 2026-09-01 14:30

// ✅ 正确:用 dateStyle 和 timeStyle,系统自动根据 locale 格式化
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateStyle = NSDateFormatterMediumStyle; // 中等长度日期
formatter.timeStyle = NSDateFormatterShortStyle;  // 短时间
NSString *dateString = [formatter stringFromDate:date];
// 英文:Sep 1, 2026 at 2:30 PM
// 中文:2026年9月1日 下午2:30
// 德文:01.09.2026, 14:30

dateStyle / timeStyle 可选值:

  • NSDateFormatterNoStyle:不显示
  • NSDateFormatterShortStyle:短(如 09/01/26、2:30 PM)
  • NSDateFormatterMediumStyle:中(如 Sep 1, 2026、2:30:00 PM)
  • NSDateFormatterLongStyle:长(如 September 1, 2026、2:30:00 PM GMT+8)
  • NSDateFormatterFullStyle:完整(如 Monday, September 1, 2026、2:30:00 PM China Standard Time)

指定 locale

// 强制用某个地区的格式
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [NSLocale localeWithLocaleIdentifier:@"zh_Hans_CN"]; // 简体中文
formatter.dateStyle = NSDateFormatterLongStyle;

2. 相对日期格式化(iOS 13+)

// iOS 13+ 提供了相对日期格式化
NSRelativeDateTimeFormatter *formatter = [[NSRelativeDateTimeFormatter alloc] init];
NSString *string = [formatter localizedStringForTimeInterval:3600]; // 3600 秒 = 1 小时
// 中文:1小时后
// 英文:in 1 hour

3. 数字格式化(NSNumberFormatter)

NSNumber *number = @1234567.89;

// 十进制数字(自动千分位)
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
NSString *numberString = [numberFormatter stringFromNumber:number];
// 中文/英文:1,234,567.89
// 德文:1.234.567,89(小数点和千分位相反)

// 货币
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle;
currencyFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"];
NSString *usdString = [currencyFormatter stringFromNumber:number]; // $1,234,567.89

currencyFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"zh_Hans_CN"];
NSString *cnyString = [currencyFormatter stringFromNumber:number]; // ¥1,234,567.89

// 百分比
NSNumberFormatter *percentFormatter = [[NSNumberFormatter alloc] init];
percentFormatter.numberStyle = NSNumberFormatterPercentStyle;
NSString *percentString = [percentFormatter stringFromNumber:@0.85]; // 85%

4. 注意事项

  • NSDateFormatterNSNumberFormatter 创建开销大,应该重用(如静态变量或单例),不要每次都创建。
  • 不要硬编码 dateFormat = @"yyyy-MM-dd",除非是和服务器交互的固定格式(服务器格式不受 locale 影响)。
  • 显示给用户看的日期/数字,一定要用 dateStyle/numberStyle + 系统 locale,让系统自动适配。

九、RTL(从右到左)语言适配

阿拉伯语(ar)、希伯来语(he)、波斯语(fa)等语言是从右到左(Right-to-Left,RTL)书写的,界面需要水平翻转。

1. Auto Layout:用 leading/trailing,不要用 left/right

这是 RTL 适配最核心的一点:

// ❌ 错误:用 left/right,RTL 语言下不会翻转
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:16];

// ✅ 正确:用 leading/trailing,RTL 语言下自动翻转
[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:16];

在 Xcode 的 Interface Builder 中,约束的 "Leading" 和 "Trailing" 就是对应前导/后导,LTR 语言下 leading=左、trailing=右,RTL 语言下自动翻转。

2. 图片翻转

包含方向指示的图片(如箭头、返回按钮),在 RTL 下需要水平翻转:

// iOS 10+ 提供了系统方法
UIImage *arrowImage = [UIImage imageNamed:@"arrow_right"];
if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) {
    arrowImage = [arrowImage imageFlippedForRightToLeftLayoutDirection];
}

或者在 Assets.xcassets 中设置图片的 Direction 属性为 Both Left-to-Right and Right-to-Left,并提供 RTL 版本的图片。

3. 文字对齐

// ❌ 错误:硬编码左对齐
label.textAlignment = NSTextAlignmentLeft;

// ✅ 正确:用自然对齐,LTR 左对齐,RTL 右对齐
label.textAlignment = NSTextAlignmentNatural;

4. 测试 RTL

方式一:Edit Scheme 切换语言

  1. Xcode 顶部菜单:ProductSchemeEdit Scheme
  2. 选择 RunOptions 标签。
  3. App Language 选择 ArabicRight-to-Left Pseudolanguage(伪 RTL 语言,用翻转的英文测试,不需要翻译)。
  4. 运行 App,界面会自动翻转。

方式二:代码强制 RTL(测试用)

// 强制 RTL(仅用于测试,不要在发布版本中硬编码)
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft];

十、本地化测试

1. 模拟器/真机切换系统语言

  1. 打开 设置通用语言与地区iPhone 语言
  2. 选择要测试的语言,重启 App 查看效果。

2. Edit Scheme 快速切换(推荐开发时用)

不需要修改系统语言,直接在 Xcode 中设置:

  1. ProductSchemeEdit SchemeRunOptions
  2. App Language 选择要测试的语言。
  3. Application Region 选择地区(影响日期、数字格式)。
  4. 运行即可,不影响系统设置。

3. 伪语言(Pseudolanguage)测试

Xcode 提供了几种伪语言,用于测试国际化是否完整:

伪语言 说明
Double-Length Pseudolanguage 所有字符串长度翻倍(模拟德文等长文本语言),测试 UI 是否被截断
Right-to-Left Pseudolanguage 界面翻转,文本用翻转的英文,测试 RTL 适配
Accented Pseudolanguage 所有字符加上重音符号(如 Hélló),测试特殊字符显示
Bounded String Pseudolanguage 字符串用方括号包裹,方便识别字符串边界

强烈建议用 Double-Length Pseudolanguage 测试:很多语言(如德文、俄文)的文本比英文长很多,如果 UI 是按英文长度设计的,切换到长文本语言后文字会被截断。伪语言可以提前发现这些问题。

4. 检查未本地化的字符串

运行 App 后,在 Xcode 控制台中可以查看未找到本地化的键(会输出警告)。也可以用 genstrings 工具扫描代码中所有 NSLocalizedString,生成完整的键列表,和 .strings 文件对比找出缺失的键:

# 扫描所有 .m 文件,生成 Localizable.strings
genstrings -o en.lproj *.m

十一、常见问题与坑

坑 1:切换语言后部分 UI 不更新

原因:手动切换语言后,已经创建的页面不会自动重新设置文本。

解决

  • LanguageManager + 通知的方式,每个页面监听 kLanguageDidChangeNotification,在回调中重新设置所有文本。
  • 或切换语言后重建根控制器。
  • 特别注意:UITabBarItem 标题、UINavigationItem 标题、UIAlertController 等,都需要手动更新。

坑 2:.strings 文件格式错误导致所有键不生效

常见错误

  • 末尾忘记分号:"key" = "value"(少了分号)
  • 值中包含未转义的双引号:"key" = "他说"你好"";(应该转义为 \"
  • 用了中文分号或中文引号。

排查:Xcode 编译时如果 .strings 文件格式错误,会有编译警告。可以用 plutil 命令检查:

plutil -lint Localizable.strings

坑 3:繁体中文用了错误的语言代码

错误:用 zh-TW(台湾)或 zh-HK(香港)作为繁体中文的代码。

正确:用 zh-Hant,覆盖所有繁体中文地区(台湾、香港、澳门)。zh-TWzh-HK 是地区代码,不是语言代码,系统匹配时可能找不到对应的 .lproj

坑 4:App 名称不更新

原因

  • Info.plist 中没有 CFBundleDisplayName 键。
  • InfoPlist.strings 文件名错误(必须是这个名字,不能改)。
  • 没有勾选 InfoPlist.strings 的语言本地化。

解决:确保 Info.plist 中有 CFBundleDisplayName,InfoPlist.strings 正确 Localize 并包含所有语言。

坑 5:Storyboard 国际化不生效

原因:用 Base 国际化方式时,Storyboard 修改后 .strings 文件没有更新,或 ObjectID 变化导致键不匹配。

解决:推荐用代码方式(IBOutlet + NSLocalizedString)设置 Storyboard 中的文本,避免维护 .strings 文件的麻烦。

坑 6:日期/数字格式在所有语言下都一样

原因:硬编码了 dateFormat = @"yyyy-MM-dd"numberStyle 没设置。

解决:显示给用户的日期用 dateStyle/timeStyle,数字用 numberStyle,让系统根据 locale 自动格式化。只有和服务器交互的固定格式才硬编码 dateFormat

坑 7:长文本语言下 UI 被截断

原因:UI 按英文(短文本)设计,切换到德文、俄文等长文本语言后文字超出范围。

解决

  • 用 Auto Layout,不要固定 label 的宽度,让它自适应。
  • label 设置 numberOfLines = 0,允许换行。
  • 用 Double-Length Pseudolanguage 测试,提前发现问题。

坑 8:手动切换语言后系统控件语言不变

原因:系统控件(如 UIAlertController 的"取消"按钮、UIImagePickerController 的界面文字)由系统控制,跟随系统语言,不跟随 App 内手动切换的语言。

解决

  • 这是系统限制,无法完全解决。
  • 可以自定义系统控件(如自定义 Alert),但成本较高。
  • 大多数 App 接受这个限制,手动切换语言后系统控件仍显示系统语言。

十二、Swift 版本对照

1. 本地化字符串

// 基本用法
let title = NSLocalizedString("home_title", comment: "首页标题")

// 带占位符
let welcome = String(format: NSLocalizedString("welcome_message", comment: ""), "张三")

2. LanguageManager(Swift 版)

import Foundation

extension Notification.Name {
    static let languageDidChange = Notification.Name("kLanguageDidChangeNotification")
}

class LanguageManager {
    static let shared = LanguageManager()
    
    private(set) var currentLanguage: String
    private(set) var currentBundle: Bundle
    
    private init() {
        let saved = UserDefaults.standard.array(forKey: "AppleLanguages")?.first as? String
        let language = saved ?? LanguageManager.systemLanguage()
        self.currentLanguage = language
        self.currentBundle = LanguageManager.bundle(for: language)
    }
    
    func localizedString(_ key: String, table: String? = nil) -> String {
        return currentBundle.localizedString(forKey: key, value: key, table: table)
    }
    
    func setLanguage(_ language: String) {
        guard language != currentLanguage else { return }
        UserDefaults.standard.set([language], forKey: "AppleLanguages")
        UserDefaults.standard.synchronize()
        currentLanguage = language
        currentBundle = LanguageManager.bundle(for: language)
        NotificationCenter.default.post(name: .languageDidChange, object: nil)
    }
    
    static func systemLanguage() -> String {
        let lang = Locale.preferredLanguages.first ?? "en"
        if lang.hasPrefix("zh-Hans") { return "zh-Hans" }
        if lang.hasPrefix("zh-Hant") || lang.hasPrefix("zh-TW") || lang.hasPrefix("zh-HK") { return "zh-Hant" }
        if lang.hasPrefix("en") { return "en" }
        return lang
    }
    
    static func bundle(for language: String) -> Bundle {
        if let path = Bundle.main.path(forResource: language, ofType: "lproj"),
           let bundle = Bundle(path: path) {
            return bundle
        }
        return .main
    }
}

// 使用
let title = LanguageManager.shared.localizedString("home_title")
LanguageManager.shared.setLanguage("zh-Hans")

3. 日期/数字格式化

let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
let dateString = formatter.string(from: Date())

let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let numberString = numberFormatter.string(from: 1234567.89)

4. 监听语言切换

class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(languageDidChange), name: .languageDidChange, object: nil)
        refreshLocalizedText()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func languageDidChange() {
        refreshLocalizedText()
    }
    
    func refreshLocalizedText() {
        // 子类重写
    }
}

十三、总结

  • 国际化(i18n) 是搭好多语言框架,本地化(L10n) 是翻译成具体语言。
  • 完整国际化流程:项目添加语言 → Localizable.strings(文本)→ InfoPlist.strings(App 名称、权限描述)→ Storyboard/XIB 国际化(推荐代码方式)→ 图片资源国际化 → 日期数字格式化本地化 → RTL 适配。
  • Localizable.strings:文件名固定,格式 "key" = "value";,用 NSLocalizedString 读取。键名建议 模块_含义 命名。
  • InfoPlist.strings:文件名固定,用于国际化 App 显示名称(CFBundleDisplayName)和权限描述。
  • 手动切换语言:核心是自定义 NSBundle 指向对应语言的 .lproj,用自定义 bundle 读取字符串,切换后发通知刷新 UI 或重建根控制器。本文提供了完整的 LanguageManager 工具类。
  • 日期数字格式化:显示给用户的用 dateStyle/numberStyle + 系统 locale,不要硬编码格式;和服务器交互的固定格式才硬编码 dateFormat。
  • RTL 适配:Auto Layout 用 leading/trailing 不用 left/right,文字用 NSTextAlignmentNatural,方向图片用 imageFlippedForRightToLeftLayoutDirection。
  • 测试:用 Edit Scheme 切换语言,用 Double-Length Pseudolanguage 测长文本,用 Right-to-Left Pseudolanguage 测 RTL。
  • 常见坑:切换语言 UI 不更新(监听通知刷新)、.strings 格式错误(缺分号、未转义引号)、繁体中文用 zh-Hant 不是 zh-TW、App 名称不更新(Info.plist 要有 CFBundleDisplayName)、长文本 UI 截断(Auto Layout + 多行 label)。

posted @ 2016-03-26 14:54  Mr.陳  阅读(2537)  评论(0)    收藏  举报