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

78 lines
3.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```swift
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:
```swift
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.
```swift
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.