Skip to content

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:

  1. Select the project in the navigator
  2. Select the target
  3. Go to Info tab
  4. Under Localizations, add the languages you want to support
Project Settings
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:

Localizable.xcstrings
{
"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:

Before: Hardcoded
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 labels
Button("Save") { ... } // Also auto-localized
TextField("Enter API key", text: $apiKey) // Placeholder localized

For strings outside SwiftUI views, I used String(localized:):

Programmatic localization
let errorMessage = String(localized: "API key is missing. Please add your API key in Settings.")
// For alerts
Alert(
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:

Before: Broken interpolation
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:

After: Proper interpolation
// Option 1: Use the autolocalized string with parameter
Text("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:

Plural strings in Localizable.xcstrings
{
"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:

AIError.swift
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:

LocalizedPrompts.swift
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:

AIResponseView.swift
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:

ContentView.swift
#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:

LocalizationManager.swift
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:

SettingsView.swift
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

  1. String Catalogs are powerful—Xcode 15 made localization much easier than the old .strings files
  2. Test with previews—No need to change system language
  3. AI prompts need special handling—Keep code in English, localize explanations
  4. 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