Every time I ship a new feature, someone asks the same question: “Are you planning to localize the app into more languages?”

I write my apps primarily in English and Simplified Chinese. Over time, I’ve received requests for French, German, Spanish, and more. A few generous users even offered to help translate.

I always thank them for their generosity—but always chose to decline and for a very practical reason: my apps ship frequently. I can’t afford to wait for localization before releasing features. If localization becomes a blocking step, it stops being a quality improvement and turns into technical debt.

Shortly before the holiday season, I shipped Woolly, a free gift card tracker app that I built in 8 days. I used Woolly as my genie pig on the AI–powered localization process, and I learned a lot.

I realized the problem wasn’t translation at all, but system design.

The false problem: “We need better translations”

Most localization conversations start at the wrong place. We talk about string quality, translation accuracy, and native-sounding phrasing. Those things matter—but they’re downstream concerns.

If your app embeds numbers, dates, currencies, measurements, and formatting rules directly into strings, you’ve already lost. No amount of translation polish can save a system that fundamentally doesn’t understand locale.

That’s because language is only one layer of localization.

My favourite meme by far.
My favourite meme by far.

There are many factors that make an app feel localized. The app should obviously speak the language. It should also display numbers and date-time values in the widely accepted format. On top of all that, it should respect any preferences the user may have.

A few examples:

None of these are translation problems; they are convention problems, and more importantly, problems that the operating systems already know how to solve!

It’s the hard truth: most localization bugs are not caused by translators or LLMs; they are caused by developers overriding the system.

So surprisingly, my first lesson about the LLM–powered localization process? It’s not about LLM at all! It’s that you should design your app and delegate as much to the system as possible.

In the Apple ecosystem, that package is the Foundation framework.

The Foundation framework

The Foundation, from Blue Prince
The Foundation, from Blue Prince

The Foundation framework dates back decades. Many of the implementations existed way before Swift, or iPhone for that matter, was a thing. You should delegate date formatting to the DateFormatter class, and delegate number and currency formatting to NumberFormatter.

Formatting dates

Date formatter makes it easy to format dates based on the components you want. For example, this method below may give you “Sep 21, 2025”:

func displayDate(for date: Date) -> String {
	let formatter = DateFormatter()
    formatter.setLocalizedDateFormatFromTemplate("MMMdyyyy")
    return formatter.string(from: date)
}

The template symbols used all follow ISO 8601 standards. You can find the list of symbols here.

Some subtle traps even experienced devs hit:

Formatting numbers

It’s easy enough to format a decimal for the current locale:

func formatNumber(_ decimal: Decimal) -> String? {
	let formatter = NumberFormatter()
    // formatter.locale = .current // This is implied
	return formatter.string(from: decimal as NSDecimalNumber)
}

You can also specify the locale property of the number formatter. The formatter uses Locale.current by default.

It’s similarly easy to format a currency:

func formatNumber(_ decimal: Decimal) -> String? {
	let formatter = NumberFormatter()
    formatter.style = .currency // Add this line
	formatter.currencyCode = "USD" // And this line
	return formatter.string(from: decimal as NSDecimalNumber)
}

The number formatter considers the locale (both the language and the device region) and the currency. These are some of the results I’m able to get for 5 US dollars:

Locale 5 US Dollars
US English $5.00
Canadian English US$5.00
French 5,00 $ US (the decimal point, and space around the dollar sign)
German US$ 5.00 (space after the dollar sign)
Spain (Region = Canada) USD 5.00 (spelled out “USD”)
Spain (Region = US) $ 5.00 (only a dollar sign and a space; US is implied and omitted)

Imagine nailing all the details yourself! Whew.

Finally, there are currencies where there aren’t fractions, such as Japanese Yen or Korean Won. Number formatter automatically rounds up or down any fraction values for you, so you end up with, for example, JP¥100 but never JP¥100.00.

Side note — in SwiftUI, you can also use a convenience with Text(:, format:):

Text(5.00, format: .currency(code: "USD"))

This initializer doesn’t do all the things a Number Formatter can do, but it’s an option.

Other formatters

In recent years (relatively speaking), Apple added more formatters including MeasurementFormatter and DateIntervalFormatter. The former helps you do unit conversion (e.g. kilometres vs miles) and may choose a more natural scale (e.g. 10 centimetres instead of 0.0001 kilometres). The latter can omit shared date components when describing a date range so the result reads more naturally.

If those interest you, you can find out more yourself with Apple’s documentations and WWDC videos.

When “almost perfect” can be worse than “obviously machine-made”

I enlisted a friend to help check the Japanese localization of the app, and he really gave me something to think about on perfection. In his words, I shouldn’t be telling the user to stop doing something, as this can come across too harsh. Then he paused, and said, “hmm, I think it’s fine because you are a foreigner,” and that my users would be forgiving to the tone because they’d know it’s written by a non-native speaker (or rather, by a machine).

This is the uncanny valley in localization: right now, LLM-generated localization feels about 75–80% as good as professional native copywriting. That might be a sweet spot. When users know something is machine-generated, they’re forgiving. Slight stiffness reads as neutral, not wrong. Simple wording feels intentional.

But what happens when we reach 90–95%? These cultural errors stand out more than obvious automation ever did. And what is perfection then? It points back to systems: predictable, boring, consistent output beats cleverness.

  1. A personal anecdote: I added “rockets” to my bagel for 1 pound in London, UK, just to see what they are. They turned out to be arugula!