Localize/internationalize apps (iOS 26, Swift 6, Xcode 26) and make them correct in RTL locales (Arabic, Hebrew, Persian). String Catalogs (.xcstrings), ICU plurals, locale-aware FormatStyle, runtime language switching, XLIFF, bidi text (FSI/PDI isolates, iOS 26 Natural Selection), RTL layout (leading/trailing, icon mirroring, sentinel method), RTL QA checklist. Use when localizing, adding a language, i18n/l10n, migrating Localizable.strings, currency/date formatting, supporting RTL, Arabic screens left-aligned, phone numbers reorder, bidi cursor jumps, translating xcstrings. Not for general SwiftUI layout (swiftui-ui).
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Localize/internationalize apps (iOS 26, Swift 6, Xcode 26) and make them correct in RTL locales (Arabic, Hebrew, Persian). String Catalogs (.xcstrings), ICU plurals, locale-aware FormatStyle, runtime language switching, XLIFF, bidi text (FSI/PDI isolates, iOS 26 Natural Selection), RTL layout (leading/trailing, icon mirroring, sentinel method), RTL QA checklist. Use when localizing, adding a language, i18n/l10n, migrating Localizable.strings, currency/date formatting, supporting RTL, Arabic screens left-aligned, phone numbers reorder, bidi cursor jumps, translating xcstrings. Not for general SwiftUI layout (swiftui-ui).
license
MIT
Localization & RTL for Apple Platforms
Authored against iOS 26.x / iPadOS 26.x / macOS 26.x / Swift 6.x / Xcode 26.x.
Two jobs hide inside "localize my app", and conflating them is the #1 mistake:
Internationalization (i18n) — making strings & formats translatable. Pull every user-facing string into a String Catalog, never concatenate sentences, format numbers/dates/currency/lists through FormatStyle, and let plurals/gender/device variants live in the catalog.
Right-to-left (RTL) correctness — making the layout mirror. SwiftUI mirrors automatically when the locale is RTL only if you wrote layout with leading/trailing (never left/right), mirrored directional icons, and handled bidirectional text at the right layer. "RTL is enabled" does not mean the screen is correct — verify visually.
Do both. A perfectly translated app with hardcoded .left alignment is broken in Arabic; a perfectly mirrored layout with "Hello, " + name is broken for every translator.
Before quoting an exact API signature (especially anything around cursor movement, UITextInput, or TextKit), fetch the current Apple docs (sosumi.ai/documentation/...) and verify against real code. The patterns below are starting points, not a substitute for the current reference.
Word order differs across languages. Build complete sentences with placeholders; never glue translated fragments.
// WRONG — untranslatable: word order is fixed by codeText("Hello, "+ name +"!")
Text(String(localized: "items_count") +" \(count)")
// RIGHT — one catalog entry per sentence, placeholders re-orderable by localeText("greeting \(name)") // catalog: "greeting %@" → "Hello, %@!"Text("inbox.unread \(count)") // catalog varies by plural (see below)
The implicit key of Text("greeting \(name)") is greeting %@; the translator sees the whole sentence and can move %@ wherever the grammar needs it.
Pluralization (ICU)
Use Vary by Plural catalog entries (Text("items.count \(count)")); never hand-write count == 1 ? "item" : "items" — plural category sets differ per language, and Arabic alone uses six (zero/one/two/few/many/other).
Numbers, currency, dates, percentages, lists, and measurements are not strings to translate — format through FormatStyle (.currency(code:), .percent, .number, .dateTime, .relative, .list, .measurement) so the locale's separators, digits, and order apply. Currency codes / ISO units never translate; force a specific currency only when the price is genuinely in it.
Per-scene (no restart, SwiftUI): inject .environment(\.locale, …). App-wide: persist the language code into AppleLanguages in UserDefaults (takes effect on next launch). When switching in-app, also set \.layoutDirection — a locale change alone does not reliably re-flip existing views.
SwiftUI mirrors automatically in RTL locales only if layout uses logical directions: .leading/.trailing and .natural flip; .left/.right never do (the classic "Arabic stays left-aligned" bug). UIKit: leadingAnchor/trailingAnchor, never leftAnchor. Directional icons need .flipsForRightToLeftLayoutDirection(true) or the .backward/.forward SF Symbol names. Preview with .environment(\.layoutDirection, .rightToLeft).
Stock text views handle bidi with .natural; most "bidi bugs" are a hardcoded .left alignment or a weak run (number/URL/phone) missing an isolate — wrap with FSI/PDI (U+2068/U+2069, preferred over LRM/RLM). Writing direction is controlled per layer (SwiftUI \.layoutDirection — Text ignores the .writingDirection attribute; NSParagraphStyle.baseWritingDirection; UITextInput; iOS 26 AttributedString.writingDirection). One logical caret position maps to two visual positions at a direction boundary, so cursor "jumps" are correct behavior. iOS 26 Natural Selection (selectedRanges) requires TextKit 2.
Full bidi guide (layer table, all control characters, cursor model, Natural Selection, macOS quirks): rtl-bidi-deep-dive.md
The visual sentinel method (RTL UI fixes)
When fixing an existing Arabic screen, mirror the visual result, not the code: verify the root direction first with a full-width Arabic sentinel Text (it must land on the visual right — if not, fix the root before any row fixes), then fix text width/alignment, row composition (text right, accessory left, Spacer between), directional icons, and compound-control internal order — then sweep every affected screen visually.
Scheme App Language / App Region overrides; Double-Length and Right-to-Left pseudolanguages (catch truncation and non-mirroring layout with no real translations); previews across Western/Asian/RTL/long-word locales; CLI simulator locale flip (xcrun simctl spawn … AppleLanguages) for cross-locale screenshots; per-locale snapshot tests; CI gate via references/scripts/xcstrings_validate.py validate.
references/scripts/translate.py drafts missing/new/needs_review values in a .xcstrings catalog via an OpenAI-compatible endpoint — placeholder-preserving (%@, %lld, %1$@), brand/currency/URL-safe, with --dry-run, --locales, --force, --keep. A bootstrap for first-draft coverage, never a substitute for professional translation — run it, then export XLIFF for review.
Hardcoded .left / .right alignment. Absolute alignments never flip. Body text → .natural; layout → leading/trailing. Symptom: looks right in English, stays left-aligned in Arabic.
String concatenation."Hello, " + name is untranslatable. One catalog entry per full sentence with placeholders.
Hand-rolled plurals (count == 1 ? "item" : "items"). Breaks for Arabic/Russian/Polish. Use plural-varied catalog entries.
Manual number/date formatting ("\(count) items", String(format:) for currency). Use FormatStyle so separators/digits/order localize.
Phone numbers / URLs reorder in RTL. Weak runs inherit surrounding direction — wrap with FSI/PDI (or LRM/RLM).
Expecting linear cursor movement in bidi text. One logical position maps to two visual positions at boundaries; right-arrow need not move right. The bug is usually a custom UITextInput view, not the input.
selectedRange (single) in bidi content. Visually disjoint across a boundary; adopt selectedRanges on iOS 26+, accept the artifact (or build a custom selection layer) below 26.
TextKit 1 where Natural Selection / TK2 RTL geometry is needed. Any code path asking for layoutManager flips the view to TK1 and disables TK2-only behavior. Use textLayoutManager.
SwiftUI Text + .writingDirection attribute. Silently ignored; SwiftUI direction flows through \.layoutDirection (or AttributedString.writingDirection on iOS 26+).
A second local RTL override on a screen that already inherits RTL — the two fight; the screen looks half-mirrored. Determine the root first.
Directional icons left LTR in an Arabic flow — back/forward arrows and chevrons need .flipsForRightToLeftLayoutDirection(true) (or the .backward/.forward SF Symbol). Conversely, non-directional icons must NOT mirror — battery, signal/Wi-Fi, clocks, logos, universal marks keep their look; custom UIImage/asset icons don't auto-flip even when they should. (Per-icon HIG rules in rtl-bidi-deep-dive.md.)
In-app language switch that updates strings but not direction or derived content — set \.layoutDirection alongside \.locale, and refresh everything computed before the switch (startup-built tab/chart labels, scheduled notification copy, reminder strings in reducers, cached summaries, widget timelines). Appears only after a switch, never on fresh launch (full list in rtl-bidi-deep-dive.md pitfall 12).
No translator context. Missing comment: produces wrong translations for ambiguous words ("Open" verb vs adjective). Always add comment:.
Forcing Arabic / .rightToLeft at the app root to "fix Arabic" silently breaks English (Eastern-Arabic digits, Hijri dates, lost LTR). Derive locale/direction from the user's active-language choice; never hard-code either at root.
Legacy DateFormatter()/NumberFormatter() with no explicit .locale follows the device locale, not the app's active language — set .locale for display, force en_US_POSIX for parsing server dates, and en for any formatter feeding a server/storage/arithmetic. (Details + the yyyy-vs-YYYY trap in localization-patterns.md.)
Visual RTL QA checklist
After changing shared components, check every affected screen visually — not just the component:
All user-visible strings come from Text / String(localized:); no hardcoded literals slipped through.
String Catalog is the single source of truth; no loose .strings for new modules.
Plurals use catalog variants; no string-built plurals.
Numbers, currency, dates, lists, measurements use FormatStyle.
Every key has a translator-facing comment:.
A full-width Arabic sentinel lands on the visual right.
Page title, subtitle/helper copy, card titles, card descriptions, row titles, row subtitles, metric labels/values — all read anchored to the Arabic side.
In rows with text + accessory: text is visually right, accessory visually left, with real empty space between.
Back arrows point right; disclosure chevrons sit opposite the main text; directional icons match the action. Non-directional icons (battery, signal/Wi-Fi, clock, logos) are not mirrored.
Compound controls (±/segmented/playback) are on the correct side and keep correct internal order (local LTR if needed).