Saturday, March 13
When I started going to TED its talks weren’t on the web, and it was a very expensive, by-invite-only conference for about 1,200 people, so basically nobody I'd ever met had heard of TED, including me. Nowadays TED itself is still expensive and by-invite-only, but its
talks are all online, and that's made all the difference. TED’s now Kind of a Big Deal.
So when people see my TED laptop bag or my TED grocery bag or my TED luggage they will actually stop me and ask what TED is like. Before, I’m pretty sure everyone just thought my mom named me “Theodor” and I was
mighty proud.After attending for five years I have a lot of TED-branded loot, and, yes, I love the attention it brings me. I'm at the point where this year when I came home with my post-coital TED swag my assistant asked “What do you want to do with the free TED umbrella?” and I was all, “Well, it doesn’t actually have the TED logo on it, so… Goodwill.” To be fair, I hated myself a little as I said that.
Look, it's impossible to write about TED, ok? Because, if I were reading this blog and weren't able to attend TED, I'd hate me, too. I'm trying to share my random experiences at TED, but understand that, like 90% of people who get to go to TED,
I feel like a total imposter. I'm waiting for someone to notice that I don't belong among all these rich, smart, and/or accomplished people. I literally have nightmares about going to TED all year, where I'm naked and late and I can't find my notebook and I haven't done the required reading and everyone
knows.So, I write this blog entry to say, "Here's what it'd be like if you were here," not, "Nyah nyah I was here and you weren't."
And with that, here's what I can remember of TED 2010 after an entire
month with the killer man-bear-pig flu.
—

Nobel laureate Daniel Kahneman (and, honestly, shouldn't there should be some honorific like "sir" or "his worship" for winning a Nobel?) started off TED this year with an
absolutely fascinating talk about how are we experience our lives in two modes — the "experiencing" mode and the "remembering" mode. This may cause you to say "duh" but he actually uses hard data to draw some amazing conclusions about how we are screwing up our own happiness.
The brief summary (please watch the talk) is that the "remembering self" tends to have a very different set of criteria for whether an event in your life was good or bad, that isn't directly related to what the "experiencing self" actually went through at the time. Eg, you can have a miserable vacation but look at the pictures later and be nostalgic for it, and end up doing it again.
Besides being incredibly insightful, I liked Daniel's talk because he's the first TED speaker I've seen graphing how uncomfortable it is to have something stuck in your butt for different lengths of time, and different levels of clumsiness. I'm not even making this up.

This is my favorite kind of talk, and reminds me much of TED's unofficial Love Doctor, Helen Fischer, who has spoken twice about the
biochemical basis of love but still believes in romance. What these talks have in common is a scientist using hard data to affect what is usually a very soft-science field — human happiness. Daniel and Helen play against the type of scientists as unfeeling wonks — they both care a lot about the happiness of people, and use science to serve that. They are the models for all scientists.
And Daniel's talk was, of course, a great way to start TED, because it reminded all of us to try to actually enjoy the moments of TED2010, instead of spoiling them by trying to capture them for the exclusive later enjoyment by our remembering self. Also, it gave all of us a catch-phrase for the week; at night we'd say, "Well, my experiencing self thinks I want another gin & tonic…"
—

Sarah Silverman caused controversy by telling some pretty blue jokes to a crowd that still calls them “blue jokes.” In my opinion the biggest surprise was: Who did NOT expect that Sarah Silverman was going to say some risqué stuff? This woman is famous for “Fucking Matt Damon,” telling a version of The Aristocrats that’s so horrible she got sued over it, and cracking wise about getting raped as a child and kind of liking it.
I feel like hiring Sarah Silverman and thinking she’ll tone down her humor is like hiring Dora’s friend Swiper the Fox and then saying, “Ok, you’re going to perform at TED, but NO SWIPING!” Or, to switch metaphors: Sarah’s a scorpion, and you can’t be surprised when she stings you instead of helping you across the river.

Chris Anderson, the esteemed curator of TED and a man I immensely respect, simply didn’t find her funny. I can understand this; her humor is cringe-inducing, and if you haven’t been raised on David Letterman and then The Simpsons and then South Park, you won’t have developed that part of your humor palette. There’s no particular harm in that. I, for instance, don’t like the taste of tripe, but I don’t begrudge those who do.
It seems to me that the problem came because the people who didn’t think Sarah was funny either (a) took her seriously, and so thought she was the most idiotic, venal, and horrible woman who ever lived, and/or (b) thought
everyone else must be hating this as well, because so clearly WE ARE EATING A STOMACH OH GOD THIS IS DISGUSTING I FEEL LIKE IT IS EATING ME BACK THERE MUST BE SOME KIND OF MISTAKE.
At any rate, I thought Sarah was hilarious, and was one of the people who gave her a standing ovation. There were enough of us clapping loudly enough that she actually was called back for a curtain call, which is very rare at TED. So, it’s not like everyone hated her. But, man, the people who did… whoo-boy.
I certainly hope TED continues to invite people who only appeal to
some of us, though. It’d be a shame if our speakers became so bland that
none of us found them objectionable. (Eg, I didn’t particularly enjoy listening to the reverend tell me my life is pointless unless I have God backing all my good deeds, but I’m still glad he was asked for his point of view.)
—

Although I very much wanted to meet Sarah she didn't actually attend the conference itself, which was just as well because Sarah got a new boyfriend just a week before TED and also only my crush on / engagement to Jehane Noujaim had been cleared with my girlfriend, and so Sarah wasn't on my "Free List" anyway.

And, as it happens, my official TED-crush this year went to TED senior fellow Juliana Machado Ferreira, who's a Brazilian PhD who fights exotic-bird poachers. I can tell you with no shame that when a lovely young Brazilian doctor gets onstage and starts telling stories of tiny baby birds she's risked her life to save, well, it makes me temporarily forget my pretend-vows to another. She's also the first TED speaker in memory to show a little bit of tummy during her talk. Don't you judge me. I know I'm not the only one who noticed.
I also feel compelled to mention that despite claiming to not know how to salsa, Juliana was the best salsa partner I've ever had. At the risk of sounding inverse-racist, I believe there is
something in Brazilian blood.Also, I like Juliana because she reminds me of the time 'W' was told by an advisor that four Brazilian soldiers were just killed in a helicopter crash and he squinched up his face the way he did when he used to try to think and said, "Now, how'd we fit that many of our boys into one helicopter?"
—
Julia Sweeney did a little four-minute set on explaining the facts of life to her daughter in the age of the internet, and I’ve got to say she killed it. She has grown into one of my favorite performers; she could tell you a story about folding laundry and you’d laugh at every towel.

She was in the bar the night after she performed, and I think I stumbled upon her at about 1am… maybe 2. You know, honestly, I’m not sure: I was pretty lit up. “JULIA!” I yelled at her, from two feet away, “YOU KILLED IT!” I then threw my arms around her and gave her a huge hug. I’m sure she doesn’t know me from Adam, but, honestly, who doesn’t like hugs?
I told her how much I admired her timing, and asked how much of her story was embellished for the stage. She told me there was only one joke that she hadn’t actually said to her daughter, which didn’t get that great a laugh anyhow. (Julia says, “Well, women kind of lay their eggs in ponds like frogs, except the ponds are inside of them!” I told Julia: “I think with this crowd, that’s too close to the literal truth to get a laugh. The composition of our blood is almost exactly that of sea-water.” BECAUSE I AM A LAUGH RIOT WHEN DRUNK.)
I wish Julia would talk at TED every year; in part because she’s part of the community, and actually hangs out during the week and talks to people. There were a number of speakers this year who showed up right before their talk and left right after, and that tears apart the fabric of what TED is supposed to be. TED is supposed to be where we
all interact with each other; that’s the genius of it. It’s not a one-way flow.
I would go so far as to ban any speaker who won’t attend the rest of the conference — to me, it’s like playing Dungeons & Dragons (bear with me here)… D&D is a ridiculously fun game for adults if
everyone in the group is into it, but if even one person is sitting there with her arms crossed saying, "This is stupid," then everyone playing will have a miserable time.
When the TED speakers snub the rest of us, it has a chilling effect — it sets up a dichotomy between speakers and attendees that has the potential to make the conference
just a little worse by degrees.
—

Dr. Mark Roth spoke about
de-animating and re-animating humans this year. Dr. Roth looks and moves almost exactly like Christopher Lloyd's “Doc” from
Back to the Future, with a little of Gene Wilder's “Dr. Fronk-uhn-STEEN” flare from
Young Frankenstein. Almost every year there’s one guy who starts to speak and I think, “Oh boy, here comes the fun crazy-quack part of the program… hmm, I wonder if I can manage to get to the aisle and go nom on some free cottage cheese (with chives) without stepping on some billionaire’s toes?” (Actually, the cottage cheese was gone this year, replaced by not-as-tasty boring old yogurt. Such are the travails of my life at TED.)
But I was sitting beside my friends Blaise and his wife Adrienne, whose work collectively has been the source of more talks at TED than any other family (I believe). So, good company, plus, you know… Dr. Frankenstein Redux! So I sat tight.
Mark kind of lurches around the stage like he’s maybe not currently drunk, but gosh he’s spent a lot of time there and and it won’t be long before he visits again. And, you know, he comes out swinging with his thesis that we can de-animate and re-animate mammals that don’t naturally swing that way, such as humans. So, overall, it's like getting a talk from Jack Sparrow and the witch doctor combined. In fact, early on in his talk Blaise leaned over and whispered, "In the movie of this, he'll play himself."
But, gosh-damnit, he’s right. Honestly, I think he’s one of the smartest researchers I’ve ever met, and his science is very sound and incredibly practical. His talk was one of the most important I’ve ever seen [sorry, no link yet]. The basic premise is this: hydrogen sulfide is a deadly substance that used to occur in abundance on earth back when anaerobic bacteria were king and our atmosphere wasn't yet breathable for organisms like us, and it turns out if we're exposed to this substance in smallish amounts, we become inanimate and stop needing oxygen to keep our cells alive. We simply shut down, but without the other bad parts of death where we immediately decay and stuff.
The immediate advantage to this is for critical surgery — you can “freeze-dry” a patient who has had severe trauma until they can get to a hospital, or you can slow down a patient who is undergoing, say, heart surgery, so the heart is barely beating and thus much safer to work on, and (I am assuming) even if the brain is cut off from oxygen for a few minutes, well, meh, it doesn’t need it as much so she doesn’t suffer brain damage.
I know, this all sounds like Crazy Talk. But, no, that’s my brother who sells
fireworks. In fact, Mark's got the data — they've already done surgeries where the patients were given hydrogen sulfide in tiny doses (please remember, kids, do not try this at home, unless you are on the show
Jersey Shore, in which case, well, honestly, what's the cost to society?) and the overall success rate was much higher, as expected. (Sadly, I don't have numbers here, because the talk's not up yet.)
I spotted Dr. Roth in the lobby later, and cornered him to find out more. He's actually much more sane-sounding in person, and filled in some of the details he'd glossed over because he only had 18 minutes. For one, he clarified that with human beings he's only
slowed them down so far (but not stopped them), but with fish and some simpler life forms he's completely
stopped them and revived them with
no apparent harmful effects. (It's unknown if, you know, the fish come back without souls and hunger for the brains of humans because, like, it's hard to measure that with current instruments.)
I said he'd mentioned that he'd de-animated fish for six hours and brought them back just fine — was six hours a hard limit? Did the fish come back impaired after six, or not come back at all? Or come back hungering for brains?
And he replies – I swear to god – "Oh, I dunno, after six hours I had to go to dinner." I'm like, couldn't you just leave the fish in the lab overnight? I mean, he's de-animated. How much trouble's he going to get into?
I also asked Mark about the further-out uses for his research — for instance, sending people into deep hibernation for space exploration, or traveling forward through time great distances (lengths?). He acknowledged these were possibilities, but was quick to say that he intentionally distances himself from such speculation, as his research already seems kind of kooky (my words). So he's focussed on immediate, tangible, measurable benefits, like better surgery outcomes from partial de-animation.
Dr. Stephen Wolfram was also talking to Dr. Roth, and asked what, exactly, gets blocked so that de-animated creatures don't immediately start decaying, the way dead things do. Dr. Wolfram was quite familiar with the first few seconds of death, which I am not so I can't quote him exactly. But I
can quote Dr. Roth: "We don't know." He was quite candid about it. He had a hunch it'd work, he tried it out, it did. Nobody knows why. He'd love it if someone could explain it.
—

In what was possibly the worst bit of scheduling yet known to man, the next talk was an on-stage demo of the Google Nexus One. "Look! It has an… animated background!" Yes, but can it re-animate the dead? No? BO-RING!
The audience was completely a-squirm until the demo suddenly ended with the Google guy saying: we'd love each of you to try this phone, so we're giving them to you — they're waiting outside. Suddenly, ovation. I was frankly surprised how excited TEDsters were to get free swag — again I was reminded how "thrifty" many millionaires are.
When we went outside after the session, there was a line a (literal) block long to the booth handing out free phones. As I walked past it, I tried to do a mental calculation of the value of the time of all these millionaires and billionaires waiting in a long line vs. the value of the phones, but gave up. And, as it happened, they got the last laugh, because I forgot to pick up my phone later so I'm Nexus-less.
—

Dr. Stephen Wolfram has been called arrogant, I gather, which I found to be completely unfair. "Arrogant" implies someone treats you with no respect, and Dr. Wolfram is completely respectful, and interested in the people around him. What he is
not is humble, which is, in my view, completely fine. I don't think he should have to bow and scrape and apologize for having invented so many things or explored so many intellectual pursuits. He's done an incredible amount to advance science and research, and in my view deserves a Nobel prize for Mathematica alone. If he were a patent-troll I'd have no patience for him, sure, but he's a man of science in the truest sense of the word.
In the lobby again, I stood, as one does, and listened politely while he was talking at length with someone from the
Encyclopedia of Life about their data, and then asked him a couple questions. He'd mentioned that he'd invented an algorithm that, by simply tweaking the parameters, could iterate through all the possible shell designs one finds in nature… in fact, he'd found some bizarre shells from his algorithm and showed them to a naturalist, who'd in each case said, "Oh, yes, there's actually a rare animal that has this exact shell…" This was an incredible finding and says something amazing about evolution — that shells are determined by the same algorithm with slight tweaks.
I told Dr. Wolfram about my troubles focussing barcodes using the crappy far-sighted lens on Apple's new iMacs, and asked if Mathematica would have the tools I could use to mock up a process that I could then hard-code (answer: yes). I'd like to point out this is a total n00b question — I'd just admitted I didn't know crap about his incredibly famous invention and asked him, essentially, to sell me on it. But he was an incredible gentleman and told me exactly what chapter of his book would cover the operations I'd need to learn about as I explored Mathematica.
It's quite possible that Dr. Wolfram has something like Asperger's syndrome, but is obviously super-incredible-high-functioning — obviously I'm not a psychiatrist, so what do I know, but he strikes me as a fellow who follows a different set of social cues than "normal" people, and is obviously much better off for it (much like Temple Grandin, who also spoke at TED this year.)
As supporting evidence, I was up drinking later with another TEDster (as I am wont to do,
gasp) and my new buddy told me a story of Dr. Wolfram speaking to him for an hour at another conference, and the good Dr. was excited by the TEDster's work and offered to help. Dr. Wolfram then left the room, came back in a few minutes later, and turned to the TEDster with absolutely no recognition in his face and asked casually, "Do you know when the next speaker is?"
My drinking buddy was so surprised he answered, "Dr. Wolfram… it's me! We've been talking for the last hour!" Dr. Wolfram looks at the guy's badge and says, "Oh, yes… sorry — I don't index people by face, I do it by name, and I didn't see your badge."
Now, you may be tempted to say, "Well, he was just covering up for not remembering the dude!" But that's not the end of this story. A couple days later my (now-drunken) friend gets an e-mail from an assistant of Dr. Wolfram's, that says, essentially, "You spoke with Dr. Wolfram at such-and-such conference about so-and-so, and you have been assigned conversation tracking number #47572… please send me a list of what you need from Dr. Wolfram for your project."
I'm floored. Conversation tracking numbers? I have literally never met anyone so organized. Honestly, my drunken friend's story sounds a bit fishy.
Except, a few days after I get back from TED, I got this actual e-mail:
From: Wolfram Research <___@wolfram.com>
Subject: [SWCOR #_____] Follow up to discussion with Stephen Wolfram
at TED 2010
Date: February 22, 2010 11:03:51 AM PST
To: Wil Shipley <wjs@delicious-monster.com>
Dear Wil Shipley,
I am writing in regard to your recent conversation with Stephen
Wolfram at TED 2010. His notes mentioned that you had a question
about Mathematica and image processing. Please let me know if
your question did not get answered and I can find an appropriate
contact to follow up.
Now tell me that this is an arrogant man.
—
This was a great TED for me because I’ve finally hit the point where a bunch of people know me, so my social phobia is constantly reined in. It’s amazing how nice it is to walk around and have everyone nod and smile at you. TED makes me think it’d be great to live in a small town — we’ve lost so much, living in cities with millions of people: we simply give up on knowing anyone around us, and end up strangers in our own neighborhoods for our whole lives.
I also was more social because I was invited to be a TED host this year, which basically is a little badge that means, “you are supposed to be extra-social.” It’s amazing how much easier it is to do when
it’s your job. It’s almost like putting on an act. “Look, I’m pretending I’m a guy who is actually social!”

I was actually thinking about this wednesday night when three of us ended up doing a ton of “jump shots” in front of a huge banner of TED-prize-winner Jamie Oliver (pictured), which is where you jump up and have a photographer snap a picture while you’re in the air. Normally, you know, if you’re at a party with a ton of very rich, very smart people, you might not start jumping and yelling and having flash photography done of the whole scene. But, in context, it seemed ok.
Don’t worry, ma’am… I’m a host.I realized the next day (and the next and next) that I don’t actually ever jump in my normal life, and don’t know how to do it. There’s actually some skill involved in jumping, I believe, so you don’t tear up your remaining good knee, which I did, so I was limping for the rest of the conference. On the last night I couldn’t take it any more, and at the grand party I spotted an MD/researcher in a conversation with another TEDster.
I walked up and smiled, and when there was a break in the conversation I said casually, “Man, I bet people come up to you all the time and are all, ‘Hey, you’re a doctor, can you look at this…’” at parties. Isn’t that rude?”
The other guy actually responded, “No, I think that’s kind of OK, actually.”
Me: “Oh, sweet!” Turns toward the doctor, “I tore my knee out jumping, can you help me?”
Without a pause the doctor was all, “600 milligrams ibuprofen orally 3x daily with food.”
“Thanks doc!” The next day I was all better.
—
Every year there are some stories I don't tell — in general I don't mention the famous people I walked up who completely snubbed me, because it makes me sound petty and, really, there's no law saying you have to be nice to the people around you.
But, yah… it's scary as hell to approach a famous person, especially one you admire, and it hurts like hell when you get snubbed. There's a handful of people I've learned to completely avoid at TED, because every interaction with them has been an exercise in me learning that I have absolutely no value in their universe whatsoever.
I'm sure there's people in my life who've approached me and I seemed short with, and let me say now, if I've ever given you the brush-off, it's because of my own failings and frailties. It wasn't you. I honestly believe you have value.

I also don't tell stories about Matt Groening any more, because he and I have become pals and so the stories about him fall into the categories of too banal ("Then I ordered a gin and tonic, and Matt got one, too!"), too personal ("Then I tried the super-great weed from the super-rich guy, and Matt missed out!"), or too braggy ("Then Matt Matt Matt my good friend Matt Groening Matt did I mention Matt?"). I honestly think of him as just this extremely nice guy from whom I want to learn a ton about how to treat people, but every time I talk about TED to any friend I'm all, "So Matt said to this guy…" and the person is all, "Oh, you mean
your close personal friend Matt Groening," and makes a face and I realize I sound like a total dick.
So, no Matt stories. Well, ok… I will say that Matt's the Best Wingman Ever, because every time I want to meet someone at TED I go up to them and say, "Hey, have you met Matt Groening? He's awesome, and right over there…" Yes, shameless. I know. I'm a shameless whore. Fine. I accept that.
Matt also doesn't talk a lot about himself, so it's hard to tell stories about him. This year he was surrounded by a group at lunch and I was heaping praise on him for having a show that not only has been on for twenty years, but has actually influenced
so much of our culture. Matt listened and then responded in a stage voice, without any pauses, "Now watch as I skillfully deflect the attention so Wil what's new in your life?"
—
Also every year there are a couple talks that are just amazing but to which I have no personal anecdotes to add. Rather than skip them, I'll simply tell you without further comment that you must watch Dan Barber's
How I Fell in Love With a Fish story if you like any of the following: well-told stories or jokes or food or eating or continuing to live on this planet.
And
Sir Ken Robinson's hilarious and frankly perfect talk this year isn't posted at this time, but his previous TED talk is the most-watched talk EVAR, and
you can watch it and pretend it's current. Also, if life is sane, they'll link his new talk off that page in the months to come, so by the time you read this maybe you can watch both talks for the price of one (which price is, in fact, zero).
—

Only during two of the talks did strangers nudge me and audibly gasp and exclaim "I didn't think that was possible!" One was during Blaise's
incredible demo of Bing maps (I know, I never thought I'd use the words "Bing" and "incredible" together in a sentence, myself), and the other was during the LXD performance. (That handsome devil to the right is Blaise, not an LXD dancer.)
The League of Extraordinary Dancers performed at TED and frankly performed acrobatics that I (and most of the audience) would not have thought the human body could do. You may have seen LXD perform at the Oscars this year, and I hate to be the guy who says this, but unless you're actually really close to them you're missing most of the experience — so much of their motion in lost in video it's almost a crime.
One might have certain preconceived notions about hip-hop dancers, since I'd wager most of us have only experienced them as clichés in movies. I can state after chatting with several of the LXD dancers that they are in fact a group of incredibly soft-spoken and respectful and gracious young kids, who are wide-eyed and humbled at their sudden success after so much hard work.

At the final picnic, I s
poke with LXD dancer
Madd Chadd and his lovely wife, who I called “Mrs. Madd,” since “Chadd” is obviously the
first name in the “Madd Chadd” nom-de-pirouette. I walked up to his table because he’s young, super-tall, and super-handsome, which isn't a combination that's super-common at TED. Also, honestly, I was wondering if he was the guy from “Step Up” and “Step Up 2 The Streets,” which are two of my favorite movies.
Don’t you judge me! You try drinking a bunch of rum and watching them and see if you don’t enjoy it. (If not, I submit that you are dead inside. Dead.)
Anyways, the answer was no, but he’s in “Step Up 3D,” which will be awesome because it’s directed by the same guy who did “Step Up 2,” and also the leader of the LXD.
Madd Chadd, it turns out, loves video games and had in fact just finished the copy of Bioshock 2 that he'd gotten early (fame has perks!), and can speak intelligently on a large variety of subjects. Our little group spoke for hours after the party ended, until the workers had literally dismantled the entire picnic around us, and we were standing alone in a field. It felt like a planned metaphor for the end: to be left standing in a field, talking to fascinating people and trying to hold on to TED for a few more minutes as it was being torn down around us and packed away for another year.
—
The final night of TED, as I lay in bed in my hotel room with two or maybe three of the
very most attractive of the generally-lovely female TED fellows, idly daydreaming of the lies I'd tell in my blog about my final night at TED instead of admitting I was bored and alone in my room, I realized that I had no way to finish this post except to say:
Best TED Ever.
Tuesday, October 20
Introduction
If you’re developing commercial software, you’re going to want to sell it globally — for well-localized English-speaking software companies, for instance, I have seen between 20% and 25% of total revenue coming from non-English speaking countries. That’s, like, real money. You’ll want that.
Delicious Monster Gross Sales by Language of Country |
|---|
| English | 80.9% | | German | 8.1% | | French | 3.2% | | Japanese | 2.6% | | Unknown / Other | 2.2% | | Dutch | 1.5% | | Spanish | 0.9% | | Norwegian | 0.8% | | Italian | 0.5% | | Danish | 0.5% | | Swedish | 0.5% | | Chinese | 0.3% | | Portuguese | 0.3% | | [Delicious Monster Oct09] |
| | Omni Group Upgrades by Language |
|---|
| English | 74.4% | | German | 6.0% | | French | 5.3% | | Japanese | 5.0% | | Spanish | 2.3% | | Other | 2.3% | | Italian | 1.9% | | Dutch | 1.4% | | Chinese | 0.7% | | Portuguese | 0.4% | | Norwegian | 0.4% | | [Omni Group Oct09] |
|
Note that the data from the above tables was gathered in different ways: Omni tracks what language a user has set in her preferences
every time she upgrades any of her Omni software, whereas I tracked Delicious Monster’s
total dollar sales by country, and then assigned each country a “primary” language. (For countries like Switzerland, obviously my approach is an approximation.)
Still, you can try to make some educated guesses from these two data sets, although obviously correlations aren’t necessarily causative when analyzing only two samples. But, for example, Omni has Japanese-language support and Japanese-only boxed versions of their products; their Japanese number is twice that of Delicious Monster, which doesn’t have a partnership like this (yet). On the flip side, I have a Norwegian translation in Delicious Library 2, whereas OmniGraffle does not; my Norwegian number is twice that of Omni’s.
My German number is a bit higher, which is surprising because in general Delicious Library doesn’t appear to have penetrated overseas as well as Omni has. Maybe Germans love to organize books? Maybe they love doodads, and they bought my optional Bluetooth scanner, which makes their gross sales number much higher? There’s clearly a whole post to be written about mining this data, but this isn’t that post.
Abstract
In this post I’m going to explain to you what internationalization and localization are, how Apple’s tools handle them by default, and the huge flaws in Apple’s approach. Then I’m going to provide you with the code and tools to do localization in a much, much easier way.
Then you’re going to think, “That will never work, because of blah!” and I’m going to respond,
as if I can read your mind or I’ve already had this argument with a dozen developers, “It already did — I used these tools in Delicious Library and
Delicious Library 2 and they've won three Apple Design Awards between them.”
And you’re going to think, “Stupid show-off… why does he keep mentioning those?” And I’m going to say, “Look, in this case I think it’s justified, because the Design Awards are given based largely on the interface of an application, and I’m merely trying to demonstrate that the compromises I had to make in interface design didn’t have a huge impact on the feel of…” and then we’ll go back and forth for a while in an imaginary argument while I try to convince you to do things my way, dammit, because I’m the daddy.
“Internationalization” vs. “Localization”
What is internationalization, and what is localization? Good question! I’m so glad you asked.
First, you should really read
Apple’s documentation on it. I mean, seriously, this should ALWAYS be your first target of inquiry. Did you read it yet? How about now? Now?
Ok, fine… briefly, ‘internationalization’ is getting your app
ready to be localized for different languages/countries, and ‘localization’ is the process of actually adding a particular language to your app. The two are obviously are very closely related (you can't localize until you internationalize, and there's no point in internationalizing if you're not going to actually do any localizations) but it's vital to understand they are two distinct steps.
For instance, as a developer, you don't need to speak German to get a German localization — you just need to do the internationalization part and find one friendly deutschophone to do the localization, and you're golden. (I mean
literally golden, as in covered in gold — don't ignore the German market! It's 8% of my total sales!)
Apple's Localization Process
Ok, you’re sold on localization. But how do you proceed? Another good question! (Again,
thank you for tossing me all these softballs.)
First off (as I’ve said many-a-time), when you are writing your source code you should surround EVERY string that the user might eventually see in the
NSLocalizedString() macro (or a variant). This will enable you to later automatically pull the English phrases out of your app, for translation by people who actually, like, speak other languages. Errrr… whatchacallums. “Polyglots!”
But then what? Apple’s recommended process is:
- You run Apple’s command-line tool, ‘genstrings’, over your source files (hopefully as part of your build process in Xcode), to pull out all the English phrases you marked with NSLocalizedString() and its cousins. See, I told you that’d be useful! And here’s it’s already paid off, only a couple paragraphs later!
- You give the “.strings” files that genstrings generates, along with any XIBs and image files that contain English words, to a localizer, who is a native speaker of the language you want.
- The localizer creates a new directory of translated “.strings” files, plus XIBs and images with English words translated as well.
- You take this directory, and put in into a language bundle in your project, like “ja.lproj”.
Easy? Sort of. But you can already see problems:
Problem 1: Localizers Can’t Effectively Localize ImagesYou really
really don’t want your localizers editing your image files. I mean, your artists spent a lot of time making your images really nice (since you’re
not an idiot and you didn’t try to do your artwork yourself) and they used things like
layers and
paths and
filters and stuff, and your localizers don’t know how to use Photoshop and aren’t artists, and furthermore you probably gave your localizers the final PNG or JPEG2000 files instead of PSDs and so they don’t have all those layers and paths and filters and they’d do a really crappy job if they even could do it which they can’t.
Also, your artists are going to keep tweaking the images right up until the day you ship (and after — and then they’re going to beg you to delay) and you could spend 100% of your time just sending out updated images to localizers for re-translation, and your localizers are either going to charge you a fortune or get pissed off and quit (depending on if you pay them or not, respectively).
Luckily, there’s a simple and obvious solution to this one: DO NOT EVER PUT WORDS IN YOUR IMAGES.


this is wrong, wrong, so wrong.Yes, I KNOW Apple does it. I think it’s some bizarre hold-over from their Carbon days, when drawing
actual styled text on a 3D button using
actual source code was CRAZY TALK. Plus Apple has a huge in-house art staff, so it’s easy for engineers to say, “Hey, art dudes, give me a button in three different states that says, ‘Buy Album’ in our latest iTunes-only-style-that’s-not-quite-like-Cocoa’s-standards,” and the engineer is done for the day and didn’t have to write any code and can go suck down a mojito.
Never mind that every time Apple’s (separate-but-equal) iTunes UI team changes the look of their buttons, the artists have to redo several
hundred images and then re-localize them, resulting in
thousands of fiddly images. That’s certainly a lot more efficient than the programmer spending a couple hours writing code to draw the button border and inside and the shadow on the text,
once, and then just modifying that code in
one place whenever the look changes, right? RIGHT? (
* see answer at end of post.)
Luckily, you don’t even have the option to do it this stupid way, because you don’t have an art department whose time you’d like to waste. So, use icons whenever possible on buttons, and if you
must use text, then make it localizable. Luckily, this is pretty easy in Cocoa, since the button edges, insides, and the various shines and shadows are
all done for you if you use the standard mechanisms, and they’re easy to subclass, once, if you want a custom look.
For button titles that require text, you can either type English words directly into Interface Builder, or if you’re not on an iPhone you can bind the button’s title some method in your code and use NSLocalizedString() to make sure it’s localized. Since you’re going to have to localize most of your XIBs anyhow (because of menu items, mouseover help text, explanatory text boxes, field titles, window names, etc.), I really only recommend the binding approach if your button’s title changes in a way you can’t model in Interface Builder itself. (That is, don’t write an extra method if you don’t have to. Less code is better.)
Problem 2: Localizers Can’t Effectively Localize XIBsAsking your localizers to modify XIBs directly (using Interface Builder) is a huge pain: First, you limit your pool of potential localizers if they have to have the developer tools installed AND understand how to use them.
hope the localizer finds this "display pattern!"Second, XIB files aren’t flat or self-documenting, so it’s VERY hard for localizers to find EVERY last English word in your XIB. Did they forget a tooltip? What about an alternate title for a button? What about a button’s title binding’s formatter string? Maybe there’s a hidden view somewhere? You’ll have to keep testing the localizations you get for complete coverage, and sending them back to be redone, and then re-testing them again. And again. Yuck.
Sure, there’s a cool command in Interface Builder now, where you can hit control-S and see all the strings in your interface — but that’s ANOTHER thing you have to teach
each localizer to do. In addition to knowing how to use Interface Builder in the first place.
Third, XIBs are like source code: they are written by programmers and contain functional parts. If your localizers happen to delete a button, or disconnect a binding,
your program stops working for that language. Remember, your localizers are NOT coders — they don’t have the same innate fear of changing XIBs that you’ve learned from years of boning yourself. And how fun is it to debug a program that works differently in different languages? Not fun.
Fourth, many language are not as compact as English: the French and Germans are particularly fond of using the descriptions with the lots of the words or compoundwordstodescribeasingleconcept, respectively. You’re asking your localizers to resize your buttons and titles, and then, if those don’t fit in their containers, resize your views and widgets and panels as well.
Let me restate that last point:
you are asking your localizers to design your interface. If you’re making an application that catalogs users’ books, CDs, DVDs, and other physical media, then I urge you to go ahead and do this. For the rest of you, WHAT THE HELL ARE YOU THINKING?
Problem 3: Localizers Can’t Localize from Only Your AppIn the old days, the NIBs we shipped were the same as the NIBs we used to build the app (and to localize), so any polyglot user in the world could just make a new language folder in our app wrapper by copying our English resources, and then start localizing the NIBs and .strings files, and at any time she could launch our app and check her progress. (Which is good, because it’s AMAZINGLY common for localizers to screw up the punctuation in .strings files, and Cocoa’s localization machinery will just silently ignore all strings after any punctuation mistake, resulting in mystery partial localizations.)
Sure, the user had to understand Interface Builder, but at least if she did, she had everything she needed.
Nowadays, Apple doesn’t want people mucking about CHANGING their programs, so we compile XIBs down into read-only NIBs, which means we have to give localizers our original XIB files (as well as our .strings) before they can start work.
Think about the difference between some user, somewhere, just deciding to start editing a folder full of files that she already has, versus her having to write you, you having to bundle up all your XIBs and .strings and send them to her, her having to edit them all without seeing ANY results while she is doing it, then bundle them up and send them back to you, then you have to integrate them into your build system and make a test release, and either look at it yourself or send it to her again.
Lather, rinse, repeat, lather, stab, stab, stab.
Even if this
weren’t a horrible, time-wasting process (it is), this points to the final and biggest problem with localization:
Problem 4: You Have to Maintain Multiple Copies of Each Localized ResourceIf you have nine translations and each has a localized copy of every XIB in your application, then you’ll have to manually maintain nine different versions of each XIB. The strings in XIBs don’t change super-often, but the connections, flags, layout, and other object properties change all the time. And every time you change a flag or layout or connection in one XIB, you have to do the same thing eight more times. And do it perfectly, or you’ll have a language-specific bug.
Of course, you can wait until all your XIBs are fully finished before you localize, but, if you’re like me, you are still fiddling with your XIBs until the day you ship your 1.0 code. So you’d have wait to localize until AFTER shipping 1.0, which stinks, or put off shipping until you’ve finished the product, sent out the localizations, AND gotten them all back — which stinks. And then you find have to change some buggy XIBs for your version 1.2, and you’d still have the exact same problem of having to edit nine XIBs to make one change.
(This is obviously also true of images that have been localized, but hopefully I’ve talked you out of EVER doing that, so I’m not going to discuss that further.)
Now, Apple has provided some tools in an attempt to make this all easier — they have something called “ibtool” (“nibtool” before Leopard) which can pull all the strings from a file, and then put new ones in their place, supposedly. In fact, it’s intended to allow you to look at the geometry and language changes between two localized versions of the same XIB, so you can generate, say, a Japanese version of your Main Menu nib with the latest changes from your English Main Menu nib, except keeping changes that you specifically made in the geometry of objects in the German version (since some words are longer and/or shorter in other languages, and thus XIBs sometimes require relayout).
Apple also has a tool called
AppleGlot which sets up an entire “translation environment.” It apparently dates from Carbon days, and although it’s been updated to work with some versions of Interface Builder last time I tried it, it was a lot more trouble than value to me.
Finally, there are some third-party tools that attempt to simplify some of this: for example
Polyglot, but it appears to have not been updated in a while, and I’m honestly not sure what all it does. (I’m providing the link so you can research it and other products if you want.)
I disagree with the entire approach of storing a bunch of geometry diffs to your XIBs — this seems INCREDIBLY fragile to me. What happens when you move a text field from one view to another? What happens if you even move views around? All those geometry changes turn to gooblity-gook.
The fundamental problem with every tool I’ve seen to fix your XIBs, however,is that
you still have to maintain all the localized versions.
You have nine
extra copies of each XIB in your source code,
you have to make sure they are all synced up, somehow.
Now, if I told you, “Look, it’s easy to localize your Objective-C files — just maintain ten copies of each! Every time you change one, you’ll have to change all the others… but don’t worry! I’ve provided some tools that kind of help with this… until they don’t…” What would you think?
You’d spit in my eye. And I don’t like spitty eyes. So, I came up with a better way, with exactly the same solution Apple used for Objective-C files.
An Almost Ideal Solution
Let’s design, in our heads, the perfect localization system, or as close as we can get without, you know, having to do a lot of work:
• First, we simply promise ourselves we won’t put words in image files. There, that solves a lot. This is usually Apple’s convention as well, although even certain Cocoa teams (-cough-iWeb-cough-) have used fully-rendered images of words like “PUBLISH” instead of using strings — hey, THAT’S not going to be resolution-independent, is it? (I keeed! I keeeeeeed!)
• For XIBS, what we’d like (as programmers) is to have and maintain ONE single English XIB, which will polymorph to the user’s chosen language
at launch time, so we don’t have to ship with ten versions of the same XIB (they’re surprisingly big) or maintain ten versions of the same XIB in our source code (with the concomitant increase in errors as our XIB localizations slowly drift out of sync).
• If we (the programmers) change a string or the whole look of a XIB, we want partial localizations to still work — it’s fine with with us if sometimes we ship with one or two English words “peeking through” a localization, since in most countries (obviously not France) they mix in English words all the time anyhow. It’s certainly better to have a partial localization (as long as it is of high-quality) than either no localization or a corrupted one. (This goes against a programmer’s natural inclination to insist that solutions be provably perfect. Get used to it.)
• To make our localizers’ jobs easier, what we’d like to do is give them ONLY .strings files, so all localization takes is any text editor – localizers don’t have to know how to use Interface Builder or any developer tools.
• We’re forgetful, so we want to generate the strings files from our XIBs and source files every time we do a build, so we never give out-of-date versions to our localizers — if they have a build of the app, then they have the latest strings files, AND the means to test their localizations themselves.
• Since our app is going to ship with the complete set of .strings files needed to localize it, ANY customer who speaks another language can copy our English.lproj to TheirLanguage.lproj, edit the .strings files, and voila create a localized version of our app after a couple hours’ work.
Here's the Code
Now, as you might guess, I've already done this, and, if I may plug myself for a second, this code was already available to clients of
Golden % Braeburn, which is my other company where I give my Delicious store source code (and other miscellaneous helpful code) to Mac developers for a paltry percentage of sales. (Yes, Golden % Braeburn has launched, and yes, one of Golden % Braeburn's clients, Acacia Tree Software, is
currently selling its app with our store.)
The first step to slurp all the English strings out of your XIB files at build time, so your localizers have nice flat strings files to work with. Add two new build phases to your application target in Xcode using “Project ▶ New Build Phase ▶ New Run Script Build Phase”, set the shells to
/bin/zsh, and have them look like this:
|
# -q silences duplicate comments with same key warning genstrings -q -o ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${DEVELOPMENT_LANGUAGE}.lproj ${SRCROOT}/**/*.[hm] |
|
foreach nibFile (${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/**/*.nib) stringsFilePath=${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${DEVELOPMENT_LANGUAGE}.lproj/`basename ${nibFile} .nib`.strings xibFile=`basename ${nibFile} .nib`.xib xibFilePath=`echo ${SOURCE_ROOT}/**/${xibFile}` if [[ -e ${xibFilePath} ]] { ibtool --generate-stringsfile ${stringsFilePath}~ ${xibFilePath} ${BUILT_PRODUCTS_DIR}/xibLocalizationPostprocessor ${stringsFilePath}~ ${stringsFilePath} rm ${stringsFilePath}~ } end |
Now comes a slight hitch — under Leopard and beyond, the “ibtool” command
idiotically outputs a new format of .strings file that looks like this:
/* Class = "NSButtonCell"; title = "Get iPhone & iPod Touch App";
ObjectID = "1401603"; */
"1401603.title" = "Get iPhone & iPod Touch App";
/* Class = "NSTextFieldCell"; title = "Remote Libraries:";
ObjectID = "1401604"; */
"1401604.title" = "Remote Libraries:";
Instead of the standard file format (output by the older “nibtool” and the current “genstrings”):
/* Class = "NSButtonCell"; title = "Get iPhone & iPod Touch App";
ObjectID = "1401603"; */
"Get iPhone & iPod Touch App" = "Get iPhone & iPod Touch App";
/* Class = "NSTextFieldCell"; title = "Remote Libraries:";
ObjectID = "1401604"; */
"Remote Libraries:" = "Remote Libraries:";
This new format completely violates the whole .strings file paradigm and obviously makes it impossible for us to use this to localize XIBs on-the-fly, because when we load XIBs on our own,
we don't have those ObjectIDs so we can't match objects up to the strings they need. Nice going, Apple.
So, I wrote a little program to post-process “ibtool”s output to make it like “nibtool” and “genstrings”, you should add this code to your Xcode project and add a new “tool” target for it and make sure your main app depends on this tool being built, and all will be well:
|
// xibLocalizationPostprocessor.m // // Created by William Shipley on 4/14/08. // Copyright © 2005-2009 Golden % Braeburn, LLC.
#import <Cocoa/Cocoa.h>
int main(int argc, const char *argv[]) { NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; { if (argc != 3) { fprintf(stderr, "Usage: %s inputfile outputfile\n", argv[0]); exit (-1); }
NSError *error = nil; NSStringEncoding usedEncoding; NSString *rawXIBStrings = [NSString stringWithContentsOfFile:[NSString stringWithUTF8String:argv[1]] usedEncoding:&usedEncoding error:&error]; if (error) { fprintf(stderr, "Error reading %s: %s\n", argv[1], error.localizedDescription.UTF8String); exit (-1); } NSMutableString *outputStrings = [NSMutableString string]; NSUInteger lineCount = 0; NSString *lastComment = nil; for (NSString *line in [rawXIBStrings componentsSeparatedByString:@"\n"]) { lineCount++; if ([line hasPrefix:@"/*"]) { // eg: /* Class = "NSMenuItem"; title = "Quit Library"; ObjectID = "136"; */ lastComment = line; continue;
} else if (line.length == 0) { lastComment = nil; continue;
} else if ([line hasPrefix:@"\""] && [line hasSuffix:@"\";"]) { // eg: "136.title" = "Quit Library"; NSRange quoteEqualsQuoteRange = [line rangeOfString:@"\" = \""]; if (quoteEqualsQuoteRange.length && NSMaxRange(quoteEqualsQuoteRange) < line.length - 1) { if (lastComment) { [outputStrings appendString:lastComment]; [outputStrings appendString:@"\n"]; } NSString *stringNeedingLocalization = [line substringFromIndex:NSMaxRange(quoteEqualsQuoteRange)]; // chop off leading: "blah" = " stringNeedingLocalization = [stringNeedingLocalization substringToIndex:stringNeedingLocalization.length - 2]; // chop off trailing: "; [outputStrings appendFormat:@"\"%@\" = \"%@\";\n\n", stringNeedingLocalization, stringNeedingLocalization]; continue; } } NSLog(@"Warning: skipped garbage input line %d, contents: \"%@\"", lineCount, line); } if (outputStrings.length && ![outputStrings writeToFile:[NSString stringWithUTF8String:argv[2]] atomically:NO encoding:NSUTF8StringEncoding error:&error]) { fprintf(stderr, "Error writing %s: %s\n", argv[2], error.localizedDescription.UTF8String); exit (-1); } } [autoreleasePool release]; }
|
Finally, add this class to your main project. Whenever you load a XIB file now, this code will be invoked, and it’ll run through all the displayed strings in your XIB and see if there is a localization in the correspondingly-named strings file. (Eg, if you load “MainMenu.xib”, it’ll automatically be localized with strings from “MainMenu.strings”.)
|
// DMLocalizedNibBundle.m // // Created by William Jon Shipley on 2/13/05. // Copyright © 2005-2009 Golden % Braeburn, LLC. All rights reserved except as below: // This code is provided as-is, with no warranties or anything. You may use it in your projects as you wish, but you must leave this comment block (credits and copyright) intact. That's the only restriction -- Golden % Braeburn otherwise grants you a fully-paid, worldwide, transferrable license to use this code as you see fit, including but not limited to making derivative works.
#import <Cocoa/Cocoa.h> #import <objc/runtime.h>
@interface NSBundle (DMLocalizedNibBundle) + (BOOL)deliciousLocalizingLoadNibFile:(NSString *)fileName externalNameTable:(NSDictionary *)context withZone:(NSZone *)zone; @end
@interface NSBundle () + (void)_localizeStringsInObject:(id)object table:(NSString *)table; + (NSString *)_localizedStringForString:(NSString *)string table:(NSString *)table; // localize particular attributes in objects + (void)_localizeTitleOfObject:(id)object table:(NSString *)table; + (void)_localizeAlternateTitleOfObject:(id)object table:(NSString *)table; + (void)_localizeStringValueOfObject:(id)object table:(NSString *)table; + (void)_localizePlaceholderStringOfObject:(id)object table:(NSString *)table; + (void)_localizeToolTipOfObject:(id)object table:(NSString *)table; @end
@implementation NSBundle (DMLocalizedNibBundle)
#pragma mark NSObject
+ (void)load; { NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; if (self == [NSBundle class]) { method_exchangeImplementations(class_getClassMethod(self, @selector(loadNibFile:externalNameTable:withZone:)), class_getClassMethod(self, @selector(deliciousLocalizingLoadNibFile:externalNameTable:withZone:))); } [autoreleasePool release]; }
#pragma mark API
+ (BOOL)deliciousLocalizingLoadNibFile:(NSString *)fileName externalNameTable:(NSDictionary *)context withZone:(NSZone *)zone; { NSString *localizedStringsTableName = [[fileName lastPathComponent] stringByDeletingPathExtension]; NSString *localizedStringsTablePath = [[NSBundle mainBundle] pathForResource:localizedStringsTableName ofType:@"strings"]; if (localizedStringsTablePath && ![[[localizedStringsTablePath stringByDeletingLastPathComponent] lastPathComponent] isEqualToString:@"English.lproj"]) { NSNib *nib = [[NSNib alloc] initWithContentsOfURL:[NSURL fileURLWithPath:fileName]]; NSMutableArray *topLevelObjectsArray = [context objectForKey:NSNibTopLevelObjects]; if (!topLevelObjectsArray) { topLevelObjectsArray = [NSMutableArray array]; context = [NSMutableDictionary dictionaryWithDictionary:context]; [(NSMutableDictionary *)context setObject:topLevelObjectsArray forKey:NSNibTopLevelObjects]; } BOOL success = [nib instantiateNibWithExternalNameTable:context]; [self _localizeStringsInObject:topLevelObjectsArray table:localizedStringsTableName]; [nib release]; return success; } else { return [self deliciousLocalizingLoadNibFile:fileName externalNameTable:context withZone:zone]; } }
#pragma mark Private API
+ (void)_localizeStringsInObject:(id)object table:(NSString *)table; { if ([object isKindOfClass:[NSArray class]]) { NSArray *array = object; for (id nibItem in array) [self _localizeStringsInObject:nibItem table:table];
} else if ([object isKindOfClass:[NSCell class]]) { NSCell *cell = object; if ([cell isKindOfClass:[NSActionCell class]]) { NSActionCell *actionCell = (NSActionCell *)cell; if ([actionCell isKindOfClass:[NSButtonCell class]]) { NSButtonCell *buttonCell = (NSButtonCell *)actionCell; if ([buttonCell imagePosition] != NSImageOnly) { [self _localizeTitleOfObject:buttonCell table:table]; [self _localizeStringValueOfObject:buttonCell table:table]; [self _localizeAlternateTitleOfObject:buttonCell table:table]; } } else if ([actionCell isKindOfClass:[NSTextFieldCell class]]) { NSTextFieldCell *textFieldCell = (NSTextFieldCell *)actionCell; // Following line is redundant with other code, localizes twice. // [self _localizeTitleOfObject:textFieldCell table:table]; [self _localizeStringValueOfObject:textFieldCell table:table]; [self _localizePlaceholderStringOfObject:textFieldCell table:table];
} else if ([actionCell type] == NSTextCellType) { [self _localizeTitleOfObject:actionCell table:table]; [self _localizeStringValueOfObject:actionCell table:table]; } } } else if ([object isKindOfClass:[NSMenu class]]) { NSMenu *menu = object; [self _localizeTitleOfObject:menu table:table]; [self _localizeStringsInObject:[menu itemArray] table:table]; } else if ([object isKindOfClass:[NSMenuItem class]]) { NSMenuItem *menuItem = object; [self _localizeTitleOfObject:menuItem table:table]; [self _localizeStringsInObject:[menuItem submenu] table:table]; } else if ([object isKindOfClass:[NSView class]]) { NSView *view = object; [self _localizeToolTipOfObject:view table:table];
if ([view isKindOfClass:[NSBox class]]) { NSBox *box = (NSBox *)view; [self _localizeTitleOfObject:box table:table]; } else if ([view isKindOfClass:[NSControl class]]) { NSControl *control = (NSControl *)view;
if ([view isKindOfClass:[NSButton class]]) { NSButton *button = (NSButton *)control; if ([button isKindOfClass:[NSPopUpButton class]]) { NSPopUpButton *popUpButton = (NSPopUpButton *)button; NSMenu *menu = [popUpButton menu]; [self _localizeStringsInObject:[menu itemArray] table:table]; } else [self _localizeStringsInObject:[button cell] table:table];
} else if ([view isKindOfClass:[NSMatrix class]]) { NSMatrix *matrix = (NSMatrix *)control; NSArray *cells = [matrix cells]; [self _localizeStringsInObject:cells table:table]; for (NSCell *cell in cells) { NSString *localizedCellToolTip = [self _localizedStringForString:[matrix toolTipForCell:cell] table:table]; if (localizedCellToolTip) [matrix setToolTip:localizedCellToolTip forCell:cell]; } } else if ([view isKindOfClass:[NSSegmentedControl class]]) { NSSegmentedControl *segmentedControl = (NSSegmentedControl *)control; NSUInteger segmentIndex, segmentCount = [segmentedControl segmentCount]; for (segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) { NSString *localizedSegmentLabel = [self _localizedStringForString:[segmentedControl labelForSegment:segmentIndex] table:table]; if (localizedSegmentLabel) [segmentedControl setLabel:localizedSegmentLabel forSegment:segmentIndex]; [self _localizeStringsInObject:[segmentedControl menuForSegment:segmentIndex] table:table]; } } else [self _localizeStringsInObject:[control cell] table:table];
} [self _localizeStringsInObject:[view subviews] table:table]; } else if ([object isKindOfClass:[NSWindow class]]) { NSWindow *window = object; [self _localizeTitleOfObject:window table:table]; [self _localizeStringsInObject:[window contentView] table:table]; } }
+ (NSString *)_localizedStringForString:(NSString *)string table:(NSString *)table; { if (![string length]) return nil; static NSString *defaultValue = @"I AM THE DEFAULT VALUE"; NSString *localizedString = [[NSBundle mainBundle] localizedStringForKey:string value:defaultValue table:table]; if (localizedString != defaultValue) { return localizedString; } else { #ifdef BETA_BUILD NSLog(@" not going to localize string %@", string); return string; // [string uppercaseString] #else return string; #endif } }
#define DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(blahName, capitalizedBlahName) \ + (void)_localize ##capitalizedBlahName ##OfObject:(id)object table:(NSString *)table; \ { \ NSString *localizedBlah = [self _localizedStringForString:[object blahName] table:table]; \ if (localizedBlah) \ [object set ##capitalizedBlahName:localizedBlah]; \ }
DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(title, Title) DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(alternateTitle, AlternateTitle) DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(stringValue, StringValue) DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(placeholderString, PlaceholderString) DM_DEFINE_LOCALIZE_BLAH_OF_OBJECT(toolTip, ToolTip)
@end |
Unresolved Issues
Issue 1: Not all XIB properties are localizedI didn’t have any NSTableViews with standard column headers in Delicious Library, so there’s no code to handle NSTableViews at all in here. I also don’t even attempt to localize bindings strings, I believe. So, that’d be a good thing to do!
I haven’t touched this code in, like, four years. Which says a lot about how well it’s worked for me, but, obviously, there’s room for improving it. So, please pimp *my* code, and send it back to me, and I’ll post your changes. (We’ll all be better off for it!)
Issue 2: You can’t use the same phrase with two different meanings in a single XIBImagine if you have, say, a button labelled “Wind” for “what you do to a watch,” and a text field labelled “Wind” for “moving air.” You obviously can’t localize ‘em with this method. Hasn’t proven to be a big deal with me, but this is clearly what motivated Apple to completely hose string the file format for “ibtool.”
Issue 3: iPhone limitationsI never got around to porting this to work on the iPhone, since
Amazon made me destroy the iPhone version of Delicious Library, but it’d be pretty easy to make this work with hierarchies of UIViews and UIControls instead of their NSCousins.
I don’t think the swap-methods-at-load technique will work on the iPhone, either, so you’d have to manually call the translation method when you loaded a XIB, but I don’t think that’s a huge deal, since on the iPhone you typically are MUCH more conscious of when you load and unload XIBs.
Also, obviously your customers couldn’t simply get the .strings from your app bundle or add their localizations themselves, so you lose those advantages. Still, this is the technique I’ll be using in my future iPhone and apps.
Issue 4: You can’t change the geometry of XIBs depending on the localizationThe method requires that we, as programmers and interface designers, agree to make ALL our text fields and checkbuttons wide enough to allow for the wordiest of languages, since we’re localizing on-the-fly.
Everyone raises this as an objection, but, seriously, the alternatives are MUCH worse — either you maintain ten sets of geometries yourself, OR you let your localizers be your interface designers. Both are horrible.
And, seriously, you’d be surprised how little people notice when you have extra space. With checkboxes, just always stretch ‘em to the edge of the screen. With text fields — well, the user can’t tell HOW wide they are, can she? The only problem one is buttons, and I think those look better with extra space around them. (Update: my German localizer wrote me to tell me he disagreed with this — that it was a huge pain for him to find short enough German words. I apologized to him for not telling him he could request more width.)
I’ve even thought up two ways around this issue: one if obvious, which is you could just store some geometry changes based on the container type & string in a separate file, and merge those changes at run-time. But, that’d be hard to maintain, for reasons I’ve gone over a bunch by now.
The other would be cooler: have your widgets re-lay themselves out at run time based on their resizing properties if their localized titles don’t fit. For example, if button “a” has a button “b” 20 pixels to the right of it, and both buttons have the stretchy spring to their right, then it’s a fair guess that if you grow/shrink button “a”, you need to do so from its right edge, and you need to move “a” by that many pixels, as well. The other combinations of stretchy springs can be assigned values, and all you have to do is use them consistently in your XIBs and you’ll be golden. (You do have to watch for accidentally localizing Apple’s XIBs.)
But, seriously, I haven’t really done either of these yet, and no English speaker has ever said, “Hey, you have too much space around this button!”
And, importantly, I’m able to integrate all thirteen localizations of Delicious Library 2 by myself, and still have time to program and run my company and drink lots of mojitos.
Footnotes
*No, it isn't.
Labels: business, code, mac community