Fixing the Czech plurals turned up a trap worth recording: String(localized: locale:) reads the language from the bundle, while locale: only drives number and date formatting. A test that passes a Czech locale and nothing else quietly compares English strings — ours failed with "1 coupon" == "1 kupón", which reads as broken plurals when the plurals were fine and the test was looking in the wrong place. Also that a plural variation must reference the number, so a key extracted as "%@" cannot carry one and the build says so outright. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.1 KiB
Localization
Plurals: inflection is not enough for Czech
What it is. SwiftUI parses an inflection attribute inside a Text
literal and applies Foundation's grammar agreement, so the noun follows
the number without any manual branching:
Text("You read ^[\(bookCount) book](inflect: true) this year!")
Language support is the catch. It shipped for English and Spanish in iOS 15, and iOS 18 added German, French, Italian, Portuguese, Hindi and Korean. Czech is not on that list, and Czech is exactly the language that needs it: 1 kupón, 2–4 kupóny, 5+ kupónů — three forms where English has two.
What it found in our code. A real bug, not a nicety:
Text("\(activeCoupons.count) coupons") // CardDetailView, checkout screen
The Czech translation in the catalogue is a single string, %lld kupónů,
so the app renders "1 kupónů" and "3 kupónů" — wrong for every count
except five and up. It sits on the checkout screen, which is the screen
people actually look at in a shop.
The fix for us is plural variations in the String Catalog rather than
inflect:. Xcode's "Vary by Plural" writes a variations.plural block
with one / few / many / other, and Czech uses all of them:
| category | counts | Czech |
|---|---|---|
| one | 1 | 1 kupón |
| few | 2–4 | 3 kupóny |
| many | fractions | 1,5 kupónu |
| other | 0, 5+ | 5 kupónů |
Worth auditing every interpolated count in the UI at the same time; a count wrapped in parentheses — "Send unsent receipts (3)" — dodges the problem entirely and is a reasonable fallback where a plural form would be awkward.
Testing a translation needs the right bundle, not the right locale
String(localized:locale:) looks like it renders a given language. It does
not: locale: drives number and date formatting, while the language comes
from the bundle. A test that passes locale: Locale(identifier: "cs")
and nothing else reads the English strings and reports them as if they
were Czech.
let path = Bundle.main.path(forResource: "cs", ofType: "lproj")!
let czech = Bundle(path: path)!
String(localized: "\(1) coupons", bundle: czech, locale: Locale(identifier: "cs"))
Our first attempt at the plural test got this wrong and failed with
"1 coupon" == "1 kupón" — which at a glance reads as "the plurals are
broken" when in fact the English plurals were working and the test was
looking in the wrong place.
Also: a plural variation must reference the number. A key extracted as
%@ coupons cannot carry one, and the build fails outright with "Plural
variation requires referencing the number in the string" — usable
feedback, and a reason to keep counts typed as Int so the key comes out
as %lld.
Step forward? Yes, and overdue. This is the kind of defect that never shows up in a test and that a Czech speaker notices immediately.
Source. Natalia Panferova, "Handle plurals in SwiftUI Text views with
inflection", nilcoalescing.com, 13 Jan 2025. Read 18 Aug 2026. The article
is about inflect:; the Czech conclusion is ours, from checking which
languages that API actually covers.