How do plural categories work across languages, and what does a stringsdict or catalog entry have to contain?
Predict first
How do English, Russian, Arabic and Japanese pick a form for 0, 1, 2, 5, 21 and 100?
import Foundation
let fm = FileManager.default
let root = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? fm.removeItem(at: root) }
func dict(_ cats: [(String, String)]) -> String {
let body = cats.map { "<key>\($0.0)</key><string>\($0.1)</string>" }.joined()
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><plist version=\"1.0\"><dict><key>files</key><dict><key>NSStringLocalizedFormatKey</key><string>%#@n@</string><key>n</key><dict><key>NSStringFormatSpecTypeKey</key><string>NSStringPluralRuleType</string><key>NSStringFormatValueTypeKey</key><string>d</string>\(body)</dict></dict></dict></plist>"
}
let langs: [(String, [(String, String)])] = [
("en", [("one", "%d file"), ("other", "%d files")]),
("ru", [("one", "%d файл"), ("few", "%d файла"), ("many", "%d файлов"), ("other", "%d файла")]),
("ar", [("zero", "لا ملفات"), ("one", "ملف واحد"), ("two", "ملفان"), ("few", "%d ملفات"), ("many", "%d ملفًا"), ("other", "%d ملف")]),
("ja", [("other", "%d 個")]),
]
for (code, cats) in langs {
let d = root.appendingPathComponent("\(code).lproj")
try fm.createDirectory(at: d, withIntermediateDirectories: true)
try dict(cats).write(to: d.appendingPathComponent("Localizable.stringsdict"), atomically: true, encoding: .utf8)
}
for (code, _) in langs {
let b = Bundle(path: root.appendingPathComponent("\(code).lproj").path)!
let fmt = b.localizedString(forKey: "files", value: nil, table: nil)
print(code, [0, 1, 2, 5, 21, 100].map { String(format: fmt, locale: Locale(identifier: code), $0) }.joined(separator: " | "))
}
- en 0 files | 1 file | 2 files | 5 files | 21 files | 100 files ; ru … ; ar … ; ja …
- Every language prints N files
- ru 21 файлов
Mental model. A plural rule is a sorting machine with up to six chutes. The language decides which chute each number drops into; you only decide what is printed on the chute.
Plural rules come from CLDR and assign each number to one of six categories: zero, one, two, few, many and other. Each language uses a subset. English uses one and other; Japanese only other; Russian uses one (1, 21, 101), few (2 to 4, 22 to 24) and many (0, 5 to 20, 25 to 30 and so on); Arabic uses all six. The check builds a real .stringsdict in a temporary bundle per language and formats 0, 1, 2, 5, 21 and 100: Russian gives “0 файлов, 1 файл, 2 файла, 5 файлов, 21 файл, 100 файлов”, and Arabic uses the special words for 0, 1 and 2, “ملفًا” for 21 and the plain form for 100. The number-to-category mapping is decided by the system from the language, not by your code, so you never write if n == 1; you supply the text for each category your target language needs, and other is mandatory because it is the fallback.
The practical rules: the placeholder format (%d, %lld) belongs in the variant string, a category is allowed to omit the number (the Arabic zero and one forms above do), and the string with the plural lookup key stays the same in code, such as String(localized: "\(n) files"). String catalogs (next question) write these variants for you through “Vary by plural”, and Xcode shows exactly which categories each language needs, so a translator cannot forget few. Fractions follow other rules (in English 1.5 uses other), so a formatted decimal needs the decimal variant. Do not put a count into one sentence with several plurals without separate placeholders, and keep sentences whole, since the plural form of a noun can change the adjective and verb around it (Q110).
The system maps each number to a category per language; the stringsdict only holds the text per category.
Common traps
- Adding only
oneandotherfor every language. Russian, Polish and Arabic need more. - Hard-coding “1 item / n items”. It is wrong in most languages.
They might follow up with
- What if a category is missing? The lookup falls back to
other. - Does the plural rule use the app language or the region? The language of the string bundle being read.
Related questions
- 2.6.4 How do you handle plurals and word order in localised strings?
- 2.6.3 How do you localise strings: Localizable.strings, string catalogs, String(localized:)?
- 24.8.3 How does a string catalog (.xcstrings) work, and which runtime files does the build compile it into?
- 24.8.1 How are locale identifiers structured, and how does the system match a user’s language to the localisations…
← How are locale identifiers structured, and how does the system match… How does a string catalog (.xcstrings) work, and which runtime files… →