How I Added Internationalization to My Mac AI Coding Assistant
When I released NeoCode, my Mac-native AI coding assistant, I immediately got feedback from non-English speakers. One comment in Portuguese read: “Isso parece me lindo…gostaria de testar” (This looks beautiful…I would like to test it). Another user reported in Spanish: “hay un error y no lista todos los modelos” (there’s an error and it doesn’t list all the models).
My app was English-only. I needed to add internationalization, fast.
The Problem: English-Only AI Apps Exclude Users
I built my Mac AI coding assistant for myself—an English speaker. But when I shared it publicly, I realized developers worldwide wanted to use it. The problem was:
- UI text hardcoded in English
- Error messages in English only
- AI prompts optimized for English responses
- No way to switch languages at runtime
Adding i18n to a SwiftUI Mac app turned out to be straightforward, but AI-specific content required special handling.
Step 1: Enable Localization in Xcode
First, I needed to enable localization in my Xcode project:
- Select the project in the navigator
- Select the target
- Go to Info tab
- Under Localizations, add the languages you want to support
Localizations: - English (Base) - Spanish (es) - Portuguese (Brazil) (pt-BR) - Chinese (Simplified) (zh-Hans)Step 2: Create a String Catalog
Xcode 15 introduced String Catalogs (.xcstrings files)—a centralized way to manage all localized strings.
I created Localizable.xcstrings in my project:
{ "sourceLanguage" : "en", "strings" : { "Welcome to NeoCode" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Welcome to NeoCode" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Bienvenido a NeoCode" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Bem-vindo ao NeoCode" } } } } }, "version" : "1.0"}The JSON structure stores each string’s translations across languages. Xcode provides a visual editor for this file—you don’t need to edit JSON directly.
Step 3: Replace Hardcoded Strings
Here’s where SwiftUI shines. Text views automatically use LocalizedStringKey, not plain String. This means:
Text("Welcome to NeoCode") // Already localized!Wait, that’s it? Yes. The string “Welcome to NeoCode” becomes a key in the String Catalog. Xcode extracts it automatically.
But I had many strings in other places:
Button("Save") { ... } // Also auto-localizedTextField("Enter API key", text: $apiKey) // Placeholder localizedFor strings outside SwiftUI views, I used String(localized:):
let errorMessage = String(localized: "API key is missing. Please add your API key in Settings.")
// For alertsAlert( title: Text("Connection Error"), message: Text("Could not connect to the server."))Step 4: Handle String Interpolation
Dynamic strings were trickier. I had code like this:
Text("Found \(fileCount) files") // WRONG: key is "Found %lld files"The problem: SwiftUI creates a single key with format specifiers. I needed to handle this properly:
// Option 1: Use the autolocalized string with parameterText("Found \(fileCount) files") // SwiftUI handles this correctly
// Option 2: For grammar-aware pluralization (macOS 14+)Text("^[\(fileCount) files](inflect: true)")In the String Catalog, I defined the plural forms:
{ "Found %lld files" : { "localizations" : { "en" : { "variations" : { "plural" : { "one" : { "stringUnit" : { "value" : "Found 1 file" } }, "other" : { "stringUnit" : { "value" : "Found %lld files" } } } } }, "es" : { "variations" : { "plural" : { "one" : { "stringUnit" : { "value" : "Se encontro 1 archivo" } }, "other" : { "stringUnit" : { "value" : "Se encontraron %lld archivos" } } } } } } }}Step 5: Localize Error Messages for AI Operations
AI apps have unique challenges. Error messages from API calls need localization. I created a LocalizedError enum:
enum AIError: LocalizedError { case apiKeyMissing case modelNotAvailable(String) case rateLimitExceeded case networkError(Error)
var errorDescription: String? { switch self { case .apiKeyMissing: return String(localized: "API key is missing. Please add your API key in Settings.") case .modelNotAvailable(let model): return String(localized: "The model '\(model)' is not available. Please select a different model.") case .rateLimitExceeded: return String(localized: "Rate limit exceeded. Please wait a moment and try again.") case .networkError(let error): return String(localized: "Network error: \(error.localizedDescription)") } }
var recoverySuggestion: String? { switch self { case .apiKeyMissing: return String(localized: "Go to Settings > API Keys to add your key.") case .rateLimitExceeded: return String(localized: "Wait 60 seconds before retrying.") default: return nil } }}Now error alerts show localized messages automatically.
Step 6: Handle AI-Specific Localization
The trickiest part: AI prompts. I wanted my AI assistant to respond in the user’s preferred language. I created localized prompt templates:
struct LocalizedPrompts { static func systemPrompt(for locale: Locale) -> String { switch locale.language.languageCode?.identifier { case "es": return """ Eres un asistente de programacion util. Proporciona respuestas claras y concisas en espanol. El codigo debe mantenerse en ingles, pero las explicaciones deben estar en espanol. """ case "pt": return """ Voce e um assistente de programacao util. Forneca respostas claras e concisas em portugues. O codigo deve permanecer em ingles, mas as explicacoes devem ser em portugues. """ case "zh": return """ You are a helpful coding assistant. Provide clear and concise responses. Code should remain in English, but explanations should be in Chinese. """ default: return """ You are a helpful coding assistant. Provide clear and concise responses. """ } }}Key insight: I kept programming keywords in English but localized explanations. Developers expect code to be in English regardless of their native language.
Step 7: Format Dates and Numbers Locally
AI responses often include timestamps and token counts. SwiftUI’s formatted() method handles this automatically:
struct AIResponseView: View { let timestamp: Date let tokenCount: Int
var body: some View { VStack { // Locale-aware date formatting Text(timestamp.formatted(date: .long, time: .short))
// Locale-aware number formatting Text("\(tokenCount.formatted()) tokens")
// Percentage formatting if let progress = progress { Text(progress.formatted(.percent)) } } }}Spanish users see “19 de marzo de 2026, 10:50” while English users see “March 19, 2026 at 10:50 AM”.
Step 8: Test with Different Locales
I didn’t want to change my system language for testing. SwiftUI previews support locale overrides:
#Preview("English") { ContentView() .environment(\.locale, Locale(identifier: "en"))}
#Preview("Spanish") { ContentView() .environment(\.locale, Locale(identifier: "es"))}
#Preview("Portuguese (Brazil)") { ContentView() .environment(\.locale, Locale(identifier: "pt-BR"))}
#Preview("Right-to-Left (Arabic)") { ContentView() .environment(\.locale, Locale(identifier: "ar")) .environment(\.layoutDirection, .rightToLeft)}This let me verify all strings display correctly without switching system settings.
The Runtime Language Switch
Users wanted to change languages without restarting the app. I created a LocalizationManager:
import SwiftUI
class LocalizationManager: ObservableObject { static let shared = LocalizationManager()
@Published var currentLocale: Locale { didSet { UserDefaults.standard.set(currentLocale.identifier, forKey: "app_locale") } }
private init() { if let savedLocale = UserDefaults.standard.string(forKey: "app_locale") { currentLocale = Locale(identifier: savedLocale) } else { currentLocale = Locale.current } }
func switchLanguage(to identifier: String) { currentLocale = Locale(identifier: identifier) objectWillChange.send() }}In my Settings view:
struct SettingsView: View { @StateObject private var localization = LocalizationManager.shared
var body: some View { Form { Section("Language") { Picker("App Language", selection: Binding( get: { localization.currentLocale.identifier }, set: { localization.switchLanguage(to: $0) } )) { Text("English").tag("en") Text("Espanol").tag("es") Text("Portugues (Brasil)").tag("pt-BR") Text("简体中文").tag("zh-Hans") } } } }}What I Learned
- String Catalogs are powerful—Xcode 15 made localization much easier than the old
.stringsfiles - Test with previews—No need to change system language
- AI prompts need special handling—Keep code in English, localize explanations
- Start with high-priority languages—Spanish and Portuguese had immediate demand from my users
The whole process took about a day. The String Catalog editor in Xcode made adding translations straightforward. The AI-specific parts (prompt localization, response formatting) required more thought but weren’t complicated.
After releasing the localized version, I saw engagement from Spanish and Portuguese speakers increase significantly. The user who reported the model listing bug in Spanish? They submitted a pull request with additional translations.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments