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 中添加多语言支持:
- 选中项目(PROJECT,不是 TARGET)→
Info标签页。 - 找到
Localizations区域,点击+号。 - 选择要添加的语言(如
Chinese (Simplified)、Chinese (Traditional)、English)。 - 弹出对话框选择需要本地化的文件(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
- Xcode 菜单:
File→New→File→ 选择Strings File。 - 文件名必须是
Localizable.strings(这是 NSLocalizedString 默认查找的文件名,也可以用其他名字,但需要指定 table)。 - 保存到项目目录。
2. Localize 并添加语言
- 选中
Localizable.strings文件。 - 右侧 File Inspector 中点击
Localize...按钮。 - 选择
Base(或直接选择一种语言),点击Localize。 - 再次在 File Inspector 中勾选需要支持的语言(简体中文、繁体中文、英文等)。
- 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_title、login_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
File→New→File→Strings File。- 文件名必须是
InfoPlist.strings(固定名称,系统会自动识别)。 - 和 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 提供的自动方式:
- 选中 Storyboard/XIB 文件。
- 右侧 File Inspector 中点击
Localize...,选择Base。 - 勾选需要支持的语言。
- 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 支持为图片添加语言变体:
- 选中 Assets.xcassets 中的图片。
- 右侧 Attributes Inspector 中,找到
Localization区域,勾选需要的语言。 - 图片会展开为多个语言槽位,分别拖入对应语言的图片。
方式二:不同语言的图片文件
旧版本 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 文件。手动切换语言的核心是:
- 把用户选择的语言保存到
NSUserDefaults(key 为AppleLanguages)。 - 创建一个指向对应语言
.lproj目录的自定义NSBundle。 - 用自定义 bundle 的
localizedStringForKey:value:table:方法读取字符串,替代NSLocalizedString。 - 切换后发通知,所有页面重新设置文本,或直接重建根控制器。
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"];
}
方式二:重建根控制器(简单粗暴)
切换语言后,直接重新设置 UIWindow 的 rootViewController,所有页面重新创建:
- (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 启动时设置:在
AppDelegate的didFinishLaunchingWithOptions中,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. 注意事项
NSDateFormatter和NSNumberFormatter创建开销大,应该重用(如静态变量或单例),不要每次都创建。- 不要硬编码
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 切换语言
- Xcode 顶部菜单:
Product→Scheme→Edit Scheme。 - 选择
Run→Options标签。 App Language选择Arabic或Right-to-Left Pseudolanguage(伪 RTL 语言,用翻转的英文测试,不需要翻译)。- 运行 App,界面会自动翻转。
方式二:代码强制 RTL(测试用)
// 强制 RTL(仅用于测试,不要在发布版本中硬编码)
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft];
十、本地化测试
1. 模拟器/真机切换系统语言
- 打开
设置→通用→语言与地区→iPhone 语言。 - 选择要测试的语言,重启 App 查看效果。
2. Edit Scheme 快速切换(推荐开发时用)
不需要修改系统语言,直接在 Xcode 中设置:
Product→Scheme→Edit Scheme→Run→Options。App Language选择要测试的语言。Application Region选择地区(影响日期、数字格式)。- 运行即可,不影响系统设置。
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-TW 和 zh-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)。

浙公网安备 33010602011771号