%@、%1$@、%lld:在翻译中出错的格式说明符

更新于 · LocalePilot

带变量的 iOS 字符串包含 %@ 或 %lld。译者必须保留它,并且位置和类型都要正确。本文介绍反复出现的错误(其中一个是法语特有的),以及如何发现它们。

赶时间?直接在浏览器中检查译文的占位符:免费,无需注册,不上传任何内容。

% 从哪里来

在 iOS 应用中,带变量的本地化字符串会变成格式字符串。在 SwiftUI 中,这是隐式发生的:

Text("\(count) items left")                  // key: "%lld items left"
Text("Welcome back, \(name)!")               // key: "Welcome back, %@!"
String(localized: "Total: \(price) €")       // price is a Double: "Total: %lf €"

Int 对应 %lld,String 对应 %@,Double 对应 %lf。使用旧 API 时,你需要自己写格式:String(format: NSLocalizedString("items_left", comment: ""), count),而 .strings 文件中写的是 %d 或 %ld。

无论哪种方式,译者拿到的都是中间夹着 % 符号的文本,问题就出在这里。

破坏译文的三种方式

丢掉说明符。写成 "Invitez à modifier cette liste",而不是 "Invitez %@ à modifier cette liste"。不会崩溃:参数只是被忽略了,对方的名字从界面上消失。这是最常见、也最不易察觉的错误。

改变类型。把 %lld 换成 %@,或者反过来。格式化时会用错误的类型读取参数:轻则显示无意义的数字,重则崩溃,因为 %@ 期望的是对象,拿到的却是整数。

悄悄改变顺序。在语序不同的语言中,"%@ shared %@" 会变成两个名字互换的句子:语法正确,意思却错了。

用位置编号调整顺序

要改变顺序,就给参数编号:%1$@ 是第一个参数,%2$@ 是第二个,无论它们在句子中的什么位置。

/* source */      "%@ shared %@ with you" = "%1$@ shared %2$@ with you";
/* French */      "%@ shared %@ with you" = "%1$@ vous a partagé %2$@";
/* other order */ "%@ shared %@ with you" = "%2$@ : partagé par %1$@";

记住一条规则:在同一个字符串中,要么全部编号,要么全部不编号。混用 %1$@ 和 %@ 属于未定义行为。

法语陷阱:“20 % de remise”

如果你的应用有法语版,需要注意这一点。法语排版规范要求在百分号前加一个空格:“20 % de remise”(打八折,即减 20%)。在格式字符串中这就成了问题,因为 % d(百分号、空格、d)是一个有效的说明符:空格是 printf 的标志位(“正数前补一个空格”),d 是整数转换。因此这句

"Profitez de %lld % de remise"

包含两个说明符 %lld 和 % d,却只有一个参数。第二个说明符会读取一个不存在的值。正确的写法是把百分号写两次:

"Profitez de %lld %% de remise"

这只影响经过格式化处理的字符串,也就是至少包含一个变量的字符串。不带参数的固定文本 “20 % de remise” 会按原样显示。但译文会不断变化:一旦有人在这句话中加入变量,那个孤零零的 % 就会变成 bug。

复数

对于 “1 item / 3 items”,不要写两个键再在代码中用 if 判断。在 String Catalog 中,%lld items left 这个键可以有复数变体(one、other,以及视语言而定的 few 或 many;中文只需要 other),每个变体都有自己的 %lld。当一句话中有两个独立变化的数字时,Xcode 会使用替换(%#@items@):这种情况较少见,需要在 Xcode 中编辑。

发布前的检查

基本检查是机械性的:对每条译文,说明符列表(类型和位置)必须与原文一致。这正是 LocalePilot 的质量检查对 .strings 和 .xcstrings 文件所做的:它能识别 %@、%1$@、%d、%ld、%lld、%.2f、%u、%i 和 %s,把 %% 视为普通文本,并在导出前标记缺失或多余的说明符。它会有意忽略 %e 或 %x 等少见的转换,以免把 “20%en” 误判为错误。上面的 “% d” 情况则需要人工审阅:在你的法语译文中搜索后面跟着空格的 %。

常见问题

%d、%ld 和 %lld 有什么区别?

所需整数的大小不同。SwiftUI 对插值的 Int 使用 %lld;在手写的 .strings 文件中,请保留原文的说明符。

在翻译后的字符串中如何写百分号?

在带变量的字符串中,把它写两次:%%。否则“% d”或“% s”可能会被当作说明符读取。

译者可以改变变量的顺序吗?

可以,使用位置编号:先写 %2$@ 再写 %1$@。但这样整个字符串都必须编号。