Files
claude-plugins/pdat-swift-practice/skills/apple-findings/references/localization.md
T
TomUviandClaude Opus 5 a7f5d352ef Note that testing a translation needs the bundle, not the locale
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>
2026-08-18 15:42:28 +02:00

3.1 KiB
Raw Blame History

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.