为什么我的App翻译成西班牙语后文字被截断且德国版数字格式全乱套软件本地化踩坑全指南
一、 先聊聊那些年我踩过的本地化大坑
说实话,本地化这事儿吧,一开始我觉得不就是把字翻了吗?谁能想到,这背后藏着的坑,比撒哈拉沙漠里的沙子还多。
记得第一次做本地化,我信心满满地把App翻译成西班牙语和德语,结果一上线,用户吐槽刷屏:
- “你们这App是残次品吧?按钮文字被切了一半!”
- “我在德国买东西,价格显示错得离谱,差点亏死”
- “这日期格式是在跟我开玩笑吗?”
那一刻我才意识到,本地化 ≠ 翻译。这玩意儿涉及字体、布局、数字格式、日期格式、货币、从左到右/从右到左、日历系统、文本扩展率……每一个点没处理好,都能让用户抓狂。
今天我就把踩过的坑、掉过的泪、烧掉的头发,全都梳理出来。希望能帮到你。
二、 西班牙语文字截断——你以为只是翻译问题?
2.1 为什么西班牙语总是”超长”?
英语说”Settings”,西班牙语是”Configuración”——9个字符对14个字符,长度差了55%。
再看看这组对比:
| 英文 | 西语 | 长度差 |
|---|---|---|
| Home | Inicio | +33% |
| Settings | Configuración | +56% |
| My Account | Mi Cuenta | +44% |
| Search | Buscar | -50% |
| Login | Iniciar sesión | +91% |
| Logout | Cerrar sesión | +90% |
看到没?西班牙语平均比英语长20%-30%,有些词甚至长一倍以上。如果你在设计的时候只按英文布局来,西语版本必然炸。
2.2 真实案例:我的”Save”按钮遭遇了什么
我有个按钮叫”Save Changes”,UI设计的时候用了固定宽度120px,英文显示:
┌─────────────────────┐
│ Save Changes │
└─────────────────────┘
翻译成西班牙语”Guardar Cambios”之后:
┌─────────────────────┐
│ Guardar Camb... │
└─────────────────────┘
被截断了,而且”…“这个省略号本身也不是所有语言都通用的。法语用户看到可能还以为是正常省略,但西班牙语用户完全不知道这是个被截断的按钮。
更惨的是,有些地方”Save”单独一个词,翻译成西语是”Guardar”,按钮宽度可能不够,文字直接溢出容器,把旁边的图标给挤飞了。
2.3 数字格式错乱——德国的”1.234,56”是怎么把我坑的
德国人(以及很多欧洲国家)的数字格式和英语国家完全相反:
- 千位分隔符:用点号
. - 小数点:用逗号
,
所以1234.56美元,在德国就是 1.234,56 €。
问题来了:
2.3.1 代码里硬编码了数字格式化
很多开发者(包括曾经的我)会写出这样的代码:
// iOS Swift - 错误的硬编码方式
let price = 1234.56
let formatted = String(format: "%.2f", price) // "1234.56"
// 然后拼到UI里
label.text = "Price: \(formatted)"
这段代码在美国没问题,在德国用户眼里看到的是”1234.56”——他们会以为这是1234欧56分,但实际上他们期望看到的是”1.234,56 €”。
更可怕的是,有些代码还反过来做字符串解析:
// 从输入框读取金额
let input = "1.234,56" // 用户输入的德国格式
let value = Double(input) // ❌ 返回 nil!因为Swift默认用点号当小数点
直接崩了,或者更糟——静默失败,你拿到一个nil,然后UI显示0或者空白,用户一脸懵逼。
2.3.2 货币符号的位置
英语国家写 $1,234.56,但法语国家写 1 234,56 €,德国写 1.234,56 €。
如果你直接拼字符串:
// ❌ 完全错误的做法
let text = "$" + String(price) // 法国用户看到"$1234.56"是什么心情?
法国用户会想:这啥?美元?我用的是欧元!而且数字格式也不对!
2.4 日期格式——你以为”01/02/2024”是常识?
再来看日期。这个坑我以前踩过三次,才真正记住。
| 地区 | 格式 | 示例 |
|---|---|---|
| 美国 | MM/DD/YYYY | 01/15/2024 |
| 欧洲大部分 | DD/MM/YYYY | 15/01/2024 |
| 德国 | DD.MM.YYYY | 15.01.2024 |
| 中国 | YYYY年MM月DD日 | 2024年01月15日 |
你猜怎么着?01/02/2024 这个日期,在美国是1月2日,在欧洲是2月1日。
如果你代码里这么写:
// ❌ 灾难性写法
let dateStr = "01/02/2024"
let parts = dateStr.components(separatedBy: "/")
let month = Int(parts[0])! // 1
let day = Int(parts[1])! // 2
let year = Int(parts[2])! // 2024
// 在欧洲用户看来,你把他2月1日理解成了1月2日
更严重的是,有些国家的日历根本不是公历。比如:
- 泰国:佛历(比公历多543年),2024年 = 佛历2567年
- 伊斯兰历:完全不同的月份系统
- 希伯来历:同样是独立系统
如果你只做DD/MM/YYYY的假设,泰国用户看到”2024年”就会很困惑——他们习惯看到的是”2567年”。
三、 本地化的正确姿势——从理论到实践
3.1 文本扩展——设计阶段的预防
这是最重要的一点:在设计界面之前,就要考虑文本扩展。
方案A:用锚点约束(Auto Layout / Constraint)
不是固定宽度!不是固定宽度!不是固定宽度!
重要的事说三遍。用自动布局,让控件根据内容自适应。
// iOS - 正确的Auto Layout做法
import UIKit
class LocalizableButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
// 设置最大行数,避免文字无限展开
self.titleLabel?.numberOfLines = 2
// 设置最小宽高约束,避免文字过短时被压缩
self.setContentHuggingPriority(.defaultLow, for: .horizontal)
self.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
}
}
在Storyboard或者XIB里,确保你的按钮不是固定宽度(比如width = 120),而是用leading + trailing或者width >= 最小值来约束。
方案B:文本扩展率的经验值
每种语言相对于英文的扩展率,有一个大致的经验值:
| 语言 | 相对英文扩展率 | 备注 |
|---|---|---|
| 德语 | +25%~35% | 名词首字母大写,还可以连词 |
| 法语 | +15%~20% | 需要空格在标点前 |
| 西班牙语 | +20%~30% | 形容词往往比名词长 |
| 意大利语 | +20%~25% | |
| 日语 | -30%~-50% | 短!但要注意字体大小 |
| 韩语 | -20%~-30% | 也是短的 |
| 中文 | -10%~-20% | 单个字符信息密度高 |
| 阿拉伯语 | +20%~40% | 从右到左,还有复杂的连字 |
在设计UI时,至少预留30%的额外空间给文本区域。别抠那几像素,用户骂你是真的。
3.2 正确的数字格式化
iOS (Swift)
import Foundation
func formatPrice(_ amount: Double, locale: Locale) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = locale
formatter.currencyCode = "EUR" // 或者根据地区动态设置
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
}
// 使用示例
let germanLocale = Locale(identifier: "de_DE")
let frenchLocale = Locale(identifier: "fr_FR")
let usLocale = Locale(identifier: "en_US")
formatPrice(1234.56, locale: germanLocale) // "1.234,56 €"
formatPrice(1234.56, locale: frenchLocale) // "1 234,56 €"
formatPrice(1234.56, locale: usLocale) // "$1,234.56"
Android (Kotlin)
import java.text.NumberFormat
import java.util.Locale
fun formatPrice(amount: Double, locale: Locale): String {
val formatter = NumberFormat.getCurrencyInstance(locale)
return formatter.format(amount)
}
// 使用示例
formatPrice(1234.56, Locale("de", "DE")) // "1.234,56 €"
formatPrice(1234.56, Locale.FRANCE) // "1 234,56 €"
formatPrice(1234.56, Locale.US) // "$1.234,56"
Web (JavaScript)
// 使用Intl.NumberFormat
const formatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
});
formatter.format(1234.56); // "1.234,56 €"
const usFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
usFormatter.format(1234.56); // "$1,234.56"
关键原则:永远不要手动拼接数字和货币符号! 让系统API来做这件事。
3.3 日期格式化——同样用API
iOS
func formatDate(_ date: Date, locale: Locale) -> String {
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
// 使用
let date = Date()
formatDate(date, locale: Locale(identifier: "de_DE"))
// "15. Jan. 2024, 14:30"
formatDate(date, locale: Locale(identifier: "ja_JP"))
// "2024年1月15日 14:30"
Android
fun formatDate(date: Date, locale: Locale): String {
val formatter = android.text.format.DateFormat.getBestDateTimePattern(locale, "yyyyMMddhhmmss")
val dateFormat = java.text.SimpleDateFormat(formatter, locale)
return dateFormat.format(date)
}
3.4 文字方向——RTL(从右到左)的语言
阿拉伯语和希伯来语是从右到左书写的。这不只是文字方向的问题,整个UI布局都要镜像翻转。
// iOS - 检测当前语言的布局方向
func isRTL() -> Bool {
return UIView.userInterfaceLayoutDirection(
for: UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?.windows.first ?? UIWindow()
) == .rightToLeft
}
// 关键:iOS 15+ 会自动处理大部分RTL布局,但你要检查
// 特别是自定义布局、阴影、图标方向等
在RTL布局中,以下UI元素需要特别注意:
- 箭头图标(
>要变成<) - 进度条方向
- 时间线/流程图的顺序
- 图片中的文字方向
四、 翻译文本的管理——别再用字符串硬编码了
4.1 iOS: Localizable.strings / String Catalog
传统的做法是用 .strings 文件:
// Localizable.strings (English)
"welcome_message" = "Welcome to our app!";
"save_changes" = "Save Changes";
"error_network" = "Network error. Please try again.";
// Localizable.strings (Spanish - es.lproj)
"welcome_message" = "¡Bienvenido a nuestra aplicación!";
"save_changes" = "Guardar cambios";
"error_network" = "Error de red. Por favor, inténtelo de nuevo.";
Swift中使用:
// 正确:使用本地化字符串
let message = NSLocalizedString("welcome_message", comment: "欢迎消息")
label.text = message
注意:从iOS 16开始,苹果推荐使用 String Catalog(.stringscat),功能更强,支持字符串变体、复数形式等。
// .stringscat 文件中的复数形式
"items_count" = "{count, plural, =0{No items} =1{1 item} other{# items}}";
// Swift中使用
let count = 5
let text = String(localized: .init("items_count", comment: ""), count)
// 输出:"5 items"
4.2 Android: strings.xml
<!-- res/values/strings.xml (默认/英文) -->
<resources>
<string name="welcome_message">Welcome to our app!</string>
<plurals name="items_count">
<item quantity="zero">No items</item>
<item quantity="one">1 item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>
<!-- res/values-es/strings.xml (西班牙语) -->
<resources>
<string name="welcome_message">¡Bienvenido a nuestra aplicación!</string>
<plurals name="items_count">
<item quantity="zero">No hay elementos</item>
<item quantity="one">1 elemento</item>
<item quantity="other">%d elementos</item>
</plurals>
</resources>
使用:
// 单数
val message = getString(R.string.welcome_message)
// 复数
val count = 5
val itemsText = resources.getQuantityString(R.plurals.items_count, count, count)
// "5 elementos"
4.3 复数规则的陷阱——你以为所有语言的复数都一样?
这是个超级大坑。
英语只有两个复数形式:单数和复数。
1 item, 5 items
但有些语言的复数规则复杂得多:
| 语言 | 复数形式数量 | 规则简述 |
|---|---|---|
| 英语 | 2 | 1 vs 其他 |
| 法语 | 2 | 单数和复数(大部分情况) |
| 俄语 | 3 | 1, 2-4, 5+(且规则复杂) |
| 阿拉伯语 | 6 | 0, 1, 2, 3-10, 11-99, 100+ |
| 波兰语 | 3 | 1, 2-4, 5+(且和格变化相关) |
| 爱尔兰语 | 3 | 单数, 双数, 复数 |
| 斯洛文尼亚 | 4 | 1, 2, 3-4, 5+ |
| 塞尔维亚语 | 3 | 相同规则(西里尔字母/拉丁字母) |
举个例子,俄语中:
- 1 книга(一本书)
- 2 книги(两本书)
- 5 книг(五本书)
- 21 книга(21本书)
- 22 книги(22本书)
- 25 книг(25本书)
看明白没?1、2-4、5+ 的规则完全不同,而且还要看最后一个数字。21和22虽然尾数是1和2,但复数形式却和5-20一样。
如果你在App里展示数量,必须使用ICU消息格式或者平台提供的复数API,别自己写if-else判断。
// iOS - 使用String Catalog的复数支持
String(localized: .init("items_count", comment: ""), count)
// Android - 使用plurals标签
resources.getQuantityString(R.plurals.items_count, count, count)
// Web - 使用Intl.PluralRules
const rule = new Intl.PluralRules('ru', { type: 'cardinal' });
rule.select(1); // "one"
rule.select(2); // "few"
rule.select(5); // "many"
五、 字体和显示问题——不只是”能显示就行”
5.1 字体覆盖——你的App可能”看不见”某些文字
简体中文字符有2万多个,如果你没有嵌入中文字体,用户看到的可能是方框(tofu)或者乱码。
解决方案:
- iOS:系统字体(如PingFang SC)自带中文支持,但要注意字体回退
- Android:使用Google Noto字体,覆盖所有Unicode字符
- Web:使用
@font-face引入字体,或者依赖系统字体栈
/* Web - 正确的字体回退栈 */
font-family:
"PingFang SC", "Microsoft YaHei", "Heiti SC",
"Noto Sans CJK SC",
system-ui, -apple-system, sans-serif;
<!-- Android - 在manifest中声明支持的字体 -->
<application
android:fontFamily="@font/noto_sans"
...>
5.2 特殊字符——重音符号、连字、变音符号
德语有变音符号:ä, ö, ü, ß 法语有:é, è, ê, à, ç, ù, â 越南语有:â, ă, ê, ô, ơ, ư(带各种声调符号)
如果你用截图来展示文字,或者用硬编码的字体大小,这些变音符号可能会被截断。
解决方案:
- 永远不要用固定行高,用
lineHeight的相对值 - 测试时加入”测试字符串”来验证所有字符都能正常显示
"The quick brown fox jumps over the lazy dog" // 英文全部字母
"Ñoño" // 西班牙语特有字符
"straße" // 德语ß
"café résumé naïve" // 法语重音
"가나다라마바사" // 韩文
"あいうえお" // 日文
"العربية" // 阿拉伯语
"日本語テスト" // 日文测试
"Тест на русском" // 俄语
把这些字符串加到你的本地化测试流程中,确保每种语言都有覆盖。
六、 本地化的完整检查清单
为了避免以后再踩坑,我整理了一份完整的本地化检查清单。每次发布新版本前,对照这个过一遍:
6.1 文本和布局
- [ ] 所有UI文本是否都走本地化文件,没有硬编码字符串?
- [ ] 是否有固定宽度的按钮/标签在西班牙语/德语下被截断?
- [ ] 是否测试了文本扩展率最大的语言(德语、阿拉伯语)?
- [ ] 是否测试了文本最短的语言(日语、中文)在固定宽度容器中是否过于宽松?
- [ ] 复数形式是否正确处理?
- [ ] RTL语言(阿拉伯语、希伯来语)的布局是否镜像?
- [ ] 图标和箭头方向是否跟随语言方向?
6.2 数字和货币
- [ ] 所有数字是否使用
NumberFormatter/NumberFormat格式化? - [ ] 货币符号位置是否正确(前/后,带不带空格)?
- [ ] 小数位数是否符合当地习惯(有些国家用2位,有些用0位)?
- [ ] 是否有数字解析逻辑需要考虑不同格式?
6.3 日期和时间
- [ ] 所有日期是否使用
DateFormatter/DateFormat格式化? - [ ] 时间格式是12小时制还是24小时制(取决于地区)?
- [ ] 日历系统是否需要特殊处理(佛历、伊斯兰历等)?
6.4 文化和内容
- [ ] 颜色、图标、图片是否在某些文化中有特殊含义?
- [ ] 是否有地区限制的内容需要屏蔽?
- [ ] 表单输入是否符合当地格式(电话、地址、姓名)?
- [ ] 是否有地区特定的日期格式输入?
6.5 技术实现
- [ ] 是否使用了平台提供的本地化API?
- [ ] 是否有单元测试覆盖本地化逻辑?
- [ ] 是否有自动化测试验证关键UI元素?
- [ ] 是否有CI/CD流程中的本地化检查?
七、 自动化测试——让本地化问题在上线前就暴露
手动测试每种语言太累了,而且容易遗漏。自动化测试是必须的。
7.1 iOS - UI测试本地化
import XCTest
class LocalizationTests: XCTestCase {
func testGermanButtonDoesNotTruncate() {
let app = XCUIApplication()
app.launchArguments = ["--locale", "de_DE"]
app.launch()
// 检查关键按钮文字是否完整显示
let saveButton = app.buttons["save_changes"]
XCTAssertTrue(saveButton.label.contains("Änderungen speichern"),
"德语按钮文字被截断: \(saveButton.label)")
}
func testSpanishTextFitsInContainer() {
let app = XCUIApplication()
app.launchArguments = ["--locale", "es_ES"]
app.launch()
let label = app.staticTexts["welcome_message"]
// 检查文字是否溢出容器
let container = label.superior
XCTAssertTrue(label.frame.width <= container.frame.width,
"西班牙语文字溢出容器")
}
}
7.2 Android - 字符串检查
// 使用String资源检查工具
// 可以集成到CI中,检查未翻译的字符串
// gradle配置示例
android {
defaultConfig {
resourceConfigurations += listOf(
"en", "es", "de", "fr", "ja", "zh", "ar", "ru"
)
}
}
// 在CI中添加检查
// ./gradlew checkStringResources --strict
7.3 Web - Playwright测试
// playwright测试 - 验证不同语言的布局
import { test, expect } from '@playwright/test';
test.describe('Localization', () => {
test('German locale should not truncate save button', async ({ page }) => {
await page.route('**/api/settings', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ locale: 'de-DE' })
});
});
await page.goto('/');
await page.reload(); // 强制重新加载本地化
const saveButton = page.locator('[data-testid="save-button"]');
const buttonText = await saveButton.textContent();
expect(buttonText).toContain('Änderungen speichern');
// 检查文字没有溢出
const buttonBox = await saveButton.boundingBox();
const containerBox = await saveButton.locator('..').boundingBox();
expect(buttonBox.width).toBeLessThanOrEqual(containerBox.width);
});
test('RTL layout for Arabic locale', async ({ page }) => {
await page.goto('/', { locale: 'ar' });
const container = page.locator('[data-testid="main-container"]');
const containerAlignment = await container.evaluate(el =>
getComputedStyle(el).direction
);
expect(containerAlignment).toBe('rtl');
});
});
八、 一个完整的本地化架构示例
如果你正在从零开始构建一个支持多语言的App,下面是一个推荐的架构:
8.1 iOS项目结构
MyApp/
├── Resources/
│ ├── en.lproj/
│ │ └── Localizable.strings
│ ├── es.lproj/
│ │ └── Localizable.strings
│ ├── de.lproj/
│ │ └── Localizable.strings
│ ├── fr.lproj/
│ │ └── Localizable.strings
│ ├── ja.lproj/
│ │ └── Localizable.strings
│ ├── zh-Hans.lproj/
│ │ └── Localizable.strings
│ ├── ar.lproj/
│ │ └── Localizable.strings
│ └── ru.lproj/
│ └── Localizable.strings
├── Localization/
│ ├── LocaleManager.swift // 管理当前语言设置
│ ├── NumberFormatterManager.swift // 统一数字格式化
│ ├── DateFormatterManager.swift // 统一日期格式化
│ └── RTLHelper.swift // RTL布局辅助
├── Extensions/
│ ├── String+Localized.swift // 字符串扩展
│ ├── Date+Localized.swift // 日期扩展
│ └── CGFloat+Localized.swift // 度量单位扩展
└── Tests/
└── LocalizationTests.swift
8.2 LocaleManager(iOS)
import Foundation
class LocaleManager {
static let shared = LocaleManager()
private var currentLocale: Locale {
// 优先使用用户选择的语言
if let languageCode = UserDefaults.standard.string(forKey: "preferred_language") {
return Locale(identifier: languageCode)
}
// 否则使用系统语言
return Locale.current
}
// 数字格式化
func formatNumber(_ number: Double, style: NumberFormatter.Style = .decimal) -> String {
let formatter = NumberFormatter()
formatter.locale = currentLocale
formatter.numberStyle = style
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
}
// 货币格式化
func formatCurrency(_ amount: Double, currencyCode: String = "USD") -> String {
let formatter = NumberFormatter()
formatter.locale = currentLocale
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount) \(currencyCode)"
}
// 日期格式化
func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.locale = currentLocale
formatter.dateStyle = style
formatter.timeStyle = .none
return formatter.string(from: date)
}
// 日期时间格式化
func formatDateTime(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = currentLocale
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
// 是否RTL语言
var isRTL: Bool {
return currentLocale.languageCode.flatMap {
Locale.isoLanguageCodes.contains($0)
} ?? false &&
(currentLocale.languageCode == "ar" || currentLocale.languageCode == "he"
|| currentLocale.languageCode == "fa" || currentLocale.languageCode == "ur")
}
}
8.3 字符串扩展(iOS)
import Foundation
extension String {
/// 本地化字符串
var localized: String {
return NSLocalizedString(self, comment: "")
}
/// 带参数的本地化字符串
func localized(_ args: CVarArg...) -> String {
return String(format: NSLocalizedString(self, comment: ""), arguments: args)
}
/// 日期格式化
func toDate(from format: String = "yyyy-MM-dd") -> Date? {
let formatter = DateFormatter()
formatter.locale = LocaleManager.shared.currentLocale
formatter.dateFormat = format
return formatter.date(from: self)
}
}
extension Date {
/// 格式化为本地化日期字符串
func toString(format: DateFormatter.Style = .medium) -> String {
return LocaleManager.shared.formatDate(self, style: format)
}
}
extension Double {
/// 格式化为本地化货币
func toCurrency(currencyCode: String = "USD") -> String {
return LocaleManager.shared.formatCurrency(self, currencyCode: currencyCode)
}
/// 格式化为本地化数字
func toNumber() -> String {
return LocaleManager.shared.formatNumber(self)
}
}
8.4 使用示例
// 在ViewController中
class SettingsViewController: UIViewController {
@IBOutlet weak var welcomeLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var saveButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// 使用本地化字符串
welcomeLabel.text = "welcome_message".localized
saveButton.setTitle("save_changes".localized, for: .normal)
// 使用数字格式化
let price = 1234.56
priceLabel.text = price.toCurrency(currencyCode: "EUR")
// 使用日期格式化
dateLabel.text = Date().toString()
}
}
九、 一些容易被忽视的细节
9.1 搜索和过滤
如果你的App有搜索功能,不同语言的搜索规则完全不同:
- 德语:ß和ss是等价的
- 土耳其语:I和i的处理方式不同(有.dotless i)
- 法语:带重音和不带重音的字符应该匹配(é和e)
- 日语:片假名和平假名需要规范化
// iOS - 正确的搜索字符串比较
let searchQuery = "straße"
let searchText = "strasse"
// 使用compare方式处理语言特定的比较规则
if searchQuery.localizedCaseInsensitiveCompare(searchText) == .orderedSame {
// 匹配
}
9.2 排序规则
不同语言的字母排序顺序不同:
- 德语:ä < e < ö < o < ü < u < ß
- 西班牙语:ch和ll曾经是独立的字母,现在已合并
- 法语:accented characters are sorted after their base letter
- 日语:按五十音图顺序
使用平台提供的排序API,不要自己实现:
// iOS - 使用locale-aware排序
let sortedNames = names.sorted {
$0.localizedCaseInsensitiveCompare($1) == .orderedAscending
}
9.3 测量单位
不同国家使用不同的度量衡:
- 温度:摄氏度(大部分国家)vs 华氏度(美国)
- 距离:公里(大部分国家)vs 英里(美国、英国)
- 重量:千克(大部分国家)vs 磅(美国)
不要假设用户所在地区使用什么单位,让用户选择:
// 让用户选择单位系统
enum MeasurementSystem: String, Codable {
case metric = "metric"
case imperial = "imperial"
}
// 根据选择显示单位
func displayTemperature(_ celsius: Double, system: MeasurementSystem) -> String {
switch system {
case .metric:
return "\(celsius)°C"
case .imperial:
let fahrenheit = celsius * 9.0 / 5.0 + 32.0
return String(format: "%.1f°F", fahrenheit)
}
}
十、 总结——本地化是一个系统性工程
写到这里,我想说的是:本地化从来不只是一个”翻译”问题。
它是一个涉及UI设计、代码架构、测试流程、文化理解的全方位工程。每一个坑都可能是你未来用户流失的原因。
我的建议是:
- 从一开始就考虑本地化,不要等到做了翻译再回头改
- 使用平台提供的API,不要自己实现格式化逻辑
- 测试所有支持的语言,至少测试扩展率最大和最小的语言
- 建立自动化测试,每次提交都验证本地化完整性
- 关注文化差异,有些内容在某些文化里可能是冒犯的
希望这篇文章能帮你避开我踩过的所有坑。如果你还有什么具体问题,随时来问。