UI strings come from platform resources (or RN i18n), not from literals in views. System locale + in-app override if the product has one. Web: i18n-frontend. API: i18n-backend. Visual: better-mobile-interface.
Non-negotiable rules
- No new hardcoded UI string (
Text("Salvar"),"Welcome \(name)"withoutLocalizedStringResource). - Native plurals (
quantity,stringsdict/ String Catalog, ICU in RN). Notif count == 1. - Dates, times, numbers, currency: platform formatters (
DateFormatter,NumberFormatter,DateTimeFormatter,Intlin RN) with the active locale. - RTL:
layoutDirection/ trailing-leading. Testareven if the MVP is LTR. - Locale fallback (e.g.,
pt-BR→pt→ default). No raw key in the production UI. - Do not translate: IDs, tokens, logs, API names.
Android (Kotlin / Compose)
res/
values/strings.xml
values-pt-rBR/strings.xml
values-ar/strings.xml
<plurals name="item_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
- Compose:
stringResource(R.string.x),pluralStringResource. NotgetStringinViewModelif you can passUiText/ resource id. LocaleList/ per-app language (AppCompatDelegate.setApplicationLocales) if the product allows a language ≠ the system one.tools:localein previews. Pseudolocale (en-XA) in debug.- Layout:
start/end, notleft/right.autoMirroredin directional drawables.
App: android-development. UI: jetpack-compose.
iOS (Swift / SwiftUI)
- String Catalog (
.xcstrings) in new code.LocalizedStringResource/String(localized:). Text("key")only if the key is in the catalog — preferText(.saveAction)withLocalizedStringKeydocumented.- Plurals in the catalog (variations).
Inflect/ format withlocale: .autoupdatingCurrent. Locale.current/environment(\.locale). In-app override:environment(\.locale, …)+ persistence.- Format:
Text(date, format: Date.FormatStyle().locale(...))— notDateFormatterwithyyyyfixed for UI. - RTL:
layoutDirection; Trailing/Leading. RTL screenshots in preview.
Language/UI: swift / swiftui-development. Widgets: strings in the extension (widgetkit) with the same catalog/App Group if shared.
React Native / Expo
expo-localizationfor device locale/RTL. Catalog:i18n-js/react-i18next/i18next— the one in the repo.I18nManager.forceRTLaligned to the locale; reload if the platform requires it.- Do not mix native
strings.xmland JS JSON for the same key without a bridge. Prefer one source (JS) for RN UI; native only in native modules. - Deep links and store listing are another pipeline (ASO) — do not forget
InfoPlist/AndroidManifesttranslatable labels.
App: react-native-development.
Share locale with the backend
- Send
Accept-Languageand/orX-Locale/ field in the profile. The server does not guess from the IP. - Errors: prefer
codestable and translate on the client (errors.not_found) — seei18n-backend. - Push/email: locale saved in the profile, not the device's at the time of the job.
Anti-patterns
- Concatenating sentences (
"Olá, " + name + "!") String(format:)with order of%@that breaks in another language- One
if language == .portuguesein the View DateFormatterwith localeen_US_POSIXin UI (POSIX only for ISO parsing)- Forget RTL / quantity
zero/few/manywhen the language requires it - Translate in the ViewModel for logging
Integration with other skills
- Android:
android-development/jetpack-compose - iOS:
swiftui-development/widgetkit - RN:
react-native-development - Web:
i18n-frontend - API:
i18n-backend - IAP copy:
storekit/play-billing(Store texts stay in the Console; in-app paywall in the catalog) - Copy:
website-copywriting
Completion criteria
- New strings in resources/catalog; native plurals
- Formatters with locale; no fixed “foreign” date in the UI
- RTL verified or logical layout
- User locale sent to the API when there is a server
- Fallback without raw keys