Localization rtl
Skill Emasoft/emasoft-complete-ios-app-authoring/skills/localization-rtl
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).From its SKILL.md
npx -y skills add Emasoft/emasoft-complete-ios-app-authoring --skill localization-rtlAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
17.3 KB, as published. Nobody here has run it
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(neverleft/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.
Contents
- Decision: which layer is the problem
- Strings: String Catalogs (.xcstrings)
- Never concatenate — interpolate
- Pluralization (ICU)
- Locale-aware formatting
- Runtime language switching
- RTL layout
- Bidirectional text (the deep part)
- The visual sentinel method (RTL UI fixes)
- Testing localization & RTL
- Export / import for translators
- Auto-filling translations
- Common mistakes
- Visual RTL QA checklist
- References
Decision: which layer is the problem
| Symptom | Layer | Where to look |
|---|---|---|
| Text shows in English in a non-English locale | i18n — string not in catalog, or hardcoded | String Catalogs |
| "1 items" / wrong plural | i18n — plural not varied | Pluralization |
| Wrong number/date/currency format for region | i18n — manual formatting | Formatting |
| Arabic text is left-aligned | RTL — hardcoded .left, or root not RTL | RTL layout / Sentinel |
| Back arrow / chevron points wrong way in Arabic | RTL — image not mirrored | RTL layout |
| Phone number / URL re-orders inside Arabic text | bidi — missing isolate/mark | Bidirectional text |
| Cursor "jumps" in mixed Arabic+Latin editing | bidi — visual vs logical order | Bidirectional text |
| Rows/accessories bunch to the middle in Arabic | RTL — no flexible text zone / Spacer | Sentinel |
Strings: String Catalogs (.xcstrings)
Use String Catalogs (.xcstrings, Xcode 15+) for everything — they replace Localizable.strings + .stringsdict, handle ICU plurals natively, and auto-extract Text(...) / String(localized:) keys on build. Prefer semantic keys (cart.checkout.cta) and always add a translator-facing comment:.
- Full quick-start (creation, key naming, comments,
.stringsmigration): localization-patterns.md
Never concatenate — interpolate
Word order differs across languages. Build complete sentences with placeholders; never glue translated fragments.
// WRONG — untranslatable: word order is fixed by code
Text("Hello, " + name + "!")
Text(String(localized: "items_count") + " \(count)")
// RIGHT — one catalog entry per sentence, placeholders re-orderable by locale
Text("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).
- Full plural guide (per-language category table, examples): localization-patterns.md
Locale-aware formatting
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.
- Full FormatStyle catalog with examples: localization-patterns.md
Runtime language switching
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.
- Full guide +
LocalizationManager/LanguagePickerViewcode: localization-patterns.md and localization-templates.swift.md
RTL layout
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).
- Full guide (code, preview recipe, UIKit anchor semantics): rtl-bidi-deep-dive.md
Bidirectional text (the deep part)
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.
- Full 7-step procedure with code: rtl-bidi-deep-dive.md
Testing localization & RTL
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.
- Full testing guide (scheme options, CLI flip recipe, coverage tests, previews): localization-patterns.md
Export / import for translators
xcodebuild -exportLocalizations / -importLocalizations (or Xcode Product → Export/Import Localizations…) round-trip per-language .xcloc bundles wrapping standard XLIFF for CAT tools.
- Exact commands + workflow: localization-patterns.md
Auto-filling translations
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.
- Full usage, flags, env vars, verification steps: xcstrings-translator.md
Common mistakes
- Hardcoded
.left/.rightalignment. Absolute alignments never flip. Body text →.natural; layout → leading/trailing. Symptom: looks right in English, stays left-aligned in Arabic. - String concatenation.
"Hello, " + nameis 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). UseFormatStyleso 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
UITextInputview, not the input. selectedRange(single) in bidi content. Visually disjoint across a boundary; adoptselectedRangeson 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
layoutManagerflips the view to TK1 and disables TK2-only behavior. UsetextLayoutManager. - SwiftUI
Text+.writingDirectionattribute. Silently ignored; SwiftUI direction flows through\.layoutDirection(orAttributedString.writingDirectionon 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/.forwardSF Symbol). Conversely, non-directional icons must NOT mirror — battery, signal/Wi-Fi, clocks, logos, universal marks keep their look; customUIImage/asset icons don't auto-flip even when they should. (Per-icon HIG rules inrtl-bidi-deep-dive.md.) - In-app language switch that updates strings but not direction or derived content — set
\.layoutDirectionalongside\.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 inrtl-bidi-deep-dive.mdpitfall 12). - No translator context. Missing
comment:produces wrong translations for ambiguous words ("Open" verb vs adjective). Always addcomment:. - Forcing Arabic /
.rightToLeftat 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.localefollows the device locale, not the app's active language — set.localefor display, forceen_US_POSIXfor parsing server dates, andenfor any formatter feeding a server/storage/arithmetic. (Details + theyyyy-vs-YYYYtrap inlocalization-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
.stringsfor 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).
- Phone numbers / URLs / codes inside Arabic prose don't reorder (isolated).
- No local RTL override fighting an already-inherited RTL root.
- Double-Length and RTL pseudolanguage runs pass (no truncation, full mirror).
- Direction-aware animations/gestures still feel correct (checked, not assumed).
References
- references/localization-patterns.md — extended i18n reference: catalog JSON shape, manual-symbol-key extraction, type-safe
L10n, plural/gender/device tables (+ Slavicone≠one), everyFormatStyle, date parsing vs display (en_US_POSIX,yyyy-vs-YYYY),DateFormatterdevice-vs-app locale, SwiftUI gotchas (verbatim/LocalizedStringKey/bundle:.module), per-language expansion budget, runtime manager, accessibility strings, pseudo-localization, CLI simulator locale-flip, coverage + deterministic-audit tests,.stringsmigration, XLIFF. - references/rtl-bidi-deep-dive.md — full bidi reference: writing direction by every layer, visual-vs-logical model, all Unicode bidi controls, iOS 26 Natural Selection, alignment-in-RTL (+ HIG paragraph alignment, +2 pt visual balance, sequence-vs-digit-order, per-component mirroring), and 14 platform pitfalls (force-Arabic-at-root, variable-at-start flip, stale derived content, non-directional-icon mirroring, macOS
NSTextView, TextKit-1 geometry). - references/builtin-script-fonts.md — on-device PostScript names for script-appropriate fallback faces (CJK PingFang/Hiragino/Apple SD Gothic, Arabic GeezaPro, Indic Kohinoor/Sangam), the
.width(.condensed)no-asset fit-long-strings API, and theUIFont.familyNamesdiscovery loop. - references/localization-templates.swift.md — drop-in Swift:
L10ntype-safe strings,@MainActor @Observable LocalizationManager(+LanguagePickerView,.withLocalization()), andLocalizedPreview/RTLPreview/DynamicTypeLocalePreview/LocalePresets. - references/xcstrings-translator.md +
references/scripts/translate.py— auto-fill missing.xcstringstranslations (placeholder-safe, brand-safe, dry-run). references/scripts/xcstrings_validate.py— deterministic, zero-network.xcstringsauditor (validate / audit-plurals / status / add-key / fix-plurals); CI gate counterpart totranslate.py.- Apple: String Catalogs · Localization · FormatStyle · UITextInput · NSParagraphStyle · AttributedString.writingDirection · EnvironmentValues.layoutDirection