Browser Add-ons, Google Chrome, Interpr.it, Launch Announcement, Mozilla, Mozilla Firefox, Programming

Improving the browser extension localization process

Executive summary: I’ve developed a new site to help extension authors and translators collaborate; it’s called Interpr.it.

My least favorite aspect of developing browser extensions is localization. Writing the actual code to internationalize an extension isn’t so bad, but keeping the translation files up to date has always been a chore. (Localization is the process of translating software into different languages; internationalization is writing software that can be easily localized.)

The preeminent site in the extension localization space is Babelzilla. Babelzilla has been around since 2006, and it is the only site that I know of for pairing extension developers with translators. It works, but I’ve never felt that it works especially well, and when you can’t release an update to your extension because Babelzilla is down or buggy (preventing you from retrieving the latest locale files), it’s very frustrating.

Localizing Google Chrome Extensions

When I started developing browser extensions for Google Chrome, I was surprised to find that no Babelzilla equivalent exists, even though Chrome has very full-featured internationalization support for extensions – apparently, extension developers in this ecosystem are expected to come up with their own ad hoc solutions for managing their extension translations. “Ad hoc” is not a term I enjoy applying to solutions, so I decided to see what I could do to improve upon the Babelzilla experience for Chrome extension developers.

Centralizing Extension Internationalization

In developing a translation site for Chrome extensions, I had two goals:

  1. A full-featured API that allows developers to interact with the site without visiting it.
  2. A clean and simple interface for translators that reduces the barrier to entry as close to zero as possible.

With these two goals in mind, I developed Interpr.it. On Interpr.it, developers upload their extensions, translators localize them into their native language, and then developers can download the new locale files to use in their next extension update.

Interpr.it for Developers

Developer interaction is limited to the upload and download of locale files. In fact, developers can interact with Interpr.it completely from the command line via the /api/upload and /api/download API methods. (There are also API methods for translating messages and retrieving the translation history of a message.)

Interpr.it for Translators

Once an extension has been uploaded to Interpr.it, a status page is generated that shows the progress made on all of the locales supported by Chrome for extensions. Here’s an example screenshot of that page:

Each locale code links to a translation page, which is a listing of all of the messages (or “phrases” or “strings”) used in the extension. Here’s what one of those messages looks like:

There are 4 aspects to the UI for translating a single message:

  1. The original un-translated message.
  2. A description of the message from the developer, which adds context to the message.
  3. The placeholders in the message, used to insert information which can’t or shouldn’t be translated.
  4. The history of the message’s translation, which shows previous revisions and allows a developer or translator to revert changes.

(Not every message will have a description, placeholders, and a translation history.) The translator types the translated phrase in the box, and as soon as they hit the tab key (or click outside of the box), the message is automatically saved without a page reload.

Additional Interpr.it Features

Sign in/out is tied to your Google account, so you don’t have to create another username and password; it seemed like a natural fit for a site focused on Google Chrome extensions.

The Interpr.it site itself is localized using itself. (Interpr.it obviously isn’t a browser extension, but it does use Google Chrome-style JSON locale files, so it’s compatible with Interpr.it’s translation system.) To access a localized version of Interpr.it, select a locale code from the menu in top-right corner of the website, or manually type a URL like es.interpr.it.

Interpr.it will also automatically fill in any translations that have been previously completed for other extensions. For example, if a translator has already translated ‘Thank you’ into French for another extension, and a different developer’s extension uses the phrase ‘Thank you,’ Interpr.it will automatically transfer that translation. (There’s been some discussion as to whether this will backfire, but I have been unable to come up with identical English phrases that would result in different translated phrases. If you can come up with some, I’d love to hear them.)

Open for Business

If you’re a Chrome extension developer, you can upload your extension to Interpr.it right now. I’m already using it to manage localization for the Interpr.it website and two of my Chrome extensions, and so far, thirteen different translators have translated a total of 1087 messages.

After I have some more time to collect feedback from developers and translators, I plan on either adding support for Mozilla browser extensions or possibly donating code to the Babelzilla project to improve their experience for developers (if they’re interested).

Feedback

If you’ve read this far, I want your feedback. Even if you’re not involved in the development or translation of browser extensions, I want to know what you think – were you not aware of the process at all? Does it sound like a problem worth solving? If you are familiar with the problems faced by developers and translators, do you think that Interpr.it is an improvement upon the Babelzilla experience? Or am I wasting my time reimplementing something that already (kind of) exists? What features would you like to see? What localization/internationalization issues do you find irritating? Leave your thoughts in the comments.

Standard
Browser Add-ons, Mozilla, Mozilla Add-ons, Mozilla Fennec, Mozilla Firefox for Mobile, Programming, Tapsure

Tapsure: Better password input on mobile devices

Typing passwords on mobile devices sucks. If you have even a reasonably strong password (one that includes letters, numbers, and special characters), it can take more than a few seconds to type it out on a phone’s keypad or on-screen keyboard. In this day and age, that’s time you just don’t have!

Tapsure is an extension for Firefox for Mobile that alleviates this problem by allowing you to input passwords on websites by tapping a rhythm on your touchscreen rather than hunting through the device’s keyboard.

How does it work?

Install Tapsure here, and after restarting Firefox for Mobile, log into one of your online accounts as usual. After you log in, you’ll see a notification from Tapsure:

Choose “Yes,” and you’ll see this dialog:

(If you choose “No,” Tapsure will never ask about that specific password again. If you just close the notification, Tapsure will ask the next time you use the password.)

Here comes the fun part: think of a song, a pattern, a rhythm, or even some Morse code that you want to use to log in to sites that use this password – it can be anything, as long as it doesn’t have more than a full second between taps. Got it? Ok, now tap that song/pattern/rhythm/Morse code on the screen. Tapsure will save it and close the dialog.

Now, the next time you’re logging into a site that uses that password, instead of slowly typing out your super-secure 20-character password, just hold your finger down on the password field until you see this:

Now tap out your pattern from the previous step, and Tapsure will automatically fill in your password for you. (If you tap the pattern incorrectly, Tapsure will shake it off and give you another chance.)

You can repeat these steps for as many passwords as you like – Tapsure will remember them all.

Tapsure Settings

In the add-on options panel, you can reset all of the patterns you’ve saved with Tapsure to start over. (This will also clear the list of passwords that Tapsure won’t ask you to save a pattern for.)

Is this secure?

Yes, Tapsure saves your patterns and passwords using Firefox’s built-in password manager, so it just as secure as having Firefox remember your passwords. Tapsure also has the benefit that someone could closely watch you log in to a website without knowing your password, because it’s harder to discern and remember a tapped pattern than it is to watch the keys that you press.

Couldn’t I just tell Firefox to remember the password?

You could… but if you use the same password on more than one site (which I estimate that 99.9% of people do), you’d have to type it out with excruciating care on every single site that you use it on.

Miscellaneous

Try and use a semi-unique pattern – don’t choose Happy Birthday. It’s like using the password “password.”

Tapsure probably works better on capacitive touchscreens than resistive touch screens, simply due to the fact that it’s easier to tap a quick pattern when you don’t have to press down firmly on each tap.

Tapsure was entered in the Firefox Mobile Add-ons Cup. If you want to see it win, please write to your senators and representatives.

Where can I install it again?

Or, you can search for “Tapsure” in the “Get Add-ons” portion of Firefox for Mobile.

Standard
Browser Add-ons, Google Chrome, Mozilla, Mozilla Firefox, Programming, ScribeFire

Using Google Chrome-style locales in Firefox extensions

Update: Don’t use this code. Use this new version.

If you’ve ever developed the same extension for both Google Chrome and Firefox, you’ve probably noticed that there is no easy way to reuse internationalization efforts between the two. Firefox uses a mix of Java-style properties files and DTD files to store translated strings, while Chrome uses JSON “messages” files. This was especially frustrating to me because 95% of the code in ScribeFire is shared between the Chrome and Firefox (and Safari) versions.

To eliminate this annoyance, I came up with a method to parse and query the locales from the Chrome version in the Firefox version. If you want to use this method in your Firefox extension, you need to take the following steps:

  • Replace “MY_EXTENSION_ID” with the ID of your extension.
  • Rename “MY_EXTENSION_STRINGS” to something that won’t interfere with another extension.
  • The _locales directory from your Chrome extension should be in the chrome/content/ directory of your Firefox extension.
  • Include the excellent io.js library in your extension.

View the code at GitHub.

After running this function, you can call MY_EXTENSION_STRINGS.get(key, substitutions); anywhere that you would have called chrome.i18n.getMessage(key, substitutions);. (It doesn’t work in Firefox 4 (yet) due to the extension manager changes, but I’ll post a follow-up when I have a Firefox 4-compatible version.)

What are your thoughts? Is there a better way? Would it be better to write a script to convert Firefox-style locales for use in Chrome?

Standard
BeardGuru, Programming

What kind of traffic is driven by a single mention of a domain on network TV?

Update, June 2020: I’ve sold BeardGuru.com, so I am no longer responsible for its content.

During last Wednesday’s episode of The Middle, the teenage son defended his fledgling beard to his parents:

“For your FYI, this is only two days old, and according to beardguru.com, I’m showing above-average hair growth for my age.”

When I hear a website mentioned on TV, I always check it out the next time I’m at a computer, just to see how much care the writers and producers took in extending their storyline to the Web. Usually, the domain just redirects to the network’s website. Sometimes, like in the case of The Office‘s WUPHF.com, the URL is a full-fledged experience that has obviously been given a lot of care.

In the case of beardguru.com and The Middle, there was nothing there. I loaded it twice to make sure, and then I ran a WHOIS – the domain wasn’t registered at all. I’ve never seen that happen before, so I immediately registered it myself, interested to see what kind of traffic would be driven to a website by a single mention in the middle of a fairly popular network TV show. (Given that I was watching the episode on my Tivo half an hour after its airtime made the fact that the domain was unregistered especially surprising.)

After registering it, I stuck up a simple homepage with three components:

  1. Random quotes of wisdom from the “Beard Guru” that I came up with over the course of the first hour of the site being up
  2. A form to let users sign up for the Beard Guru’s mailing list (powered by MailChimp, which took only minutes to set up)
  3. A Facebook “Like” form for the Beard Guru Facebook page that I made shortly thereafter (and a link to @beardguru on Twitter).

Within minutes, I started to see visitors trickling in – about 200 people that night, with an obvious spike when the episode aired on the west coast. Thursday brought another 250 visitors, and another 100 people hit the site on Friday. (It will be interesting to see whether there continue to be mini-traffic spikes when that episode of The Middle is in reruns.)

Traffic Statistics

  • Browser usage was led by Firefox, Safari, and Chrome; Internet Explorer usage was only at 18%.
  • 15% of the visits came from mobile devices – iPhones, BlackBerries, iPods, and Android systems – a number that I expected to be higher for people casually browsing the Web while watching TV.
  • The United States accounted for 83% of visits, with the remainder split between Canada, the UK, and Australia.
  • California sent 25% of all visitors, which makes me think that if the domain had been live when the show aired on the east coast, it would have seen significantly more traffic.

Engagement

  • Forty people signed up for the mailing list.
  • Twenty-five people “Liked” Beard Guru on Facebook.
  • Three people emailed me personally to ask how I had gotten the domain.
  • Nobody followed @beardguru on Twitter.
  • Each visit averaged about five pageviews due to the random beard quote that changes each time you refresh the page.

My expectations for total traffic were much higher; I had visions of tens of thousands of people loading the site, making it an instant traffic juggernaut, bringing the server to its knees, all while offers to buy the site would be rolling in from Google, Yahoo, and the American Beard Aficionado Coalition. Even though that didn’t happen, I’m pleased with the experience; beards and beard-related humor happen to be hobbies of mine, so I’ll continue to maintain beardguru.com. My plans are to have the site focus around an occasional humorous newsletter that will answer beard-related questions, whether user-submitted or made up by myself, with previous editions archived on the website.

(If you’re interested in receiving the Beard Guru newsletter, the signup form is front and center on BeardGuru.com.)

Standard
Browser Add-ons, Mozilla, Mozilla Firefox, Programming, URL Fixer

My experience with developing a freemium browser add-on

Update: URL Fixer was acquired and is now hosted at http://urlfixer.org/

I have written twenty-six add-ons for Firefox in the last five years that I have released for free on Mozilla Add-ons. I enjoy writing add-ons, and because I wrote most of them to fulfill my own needs, I had no problem giving that software away.

At last year’s Add-on Con, some of the major discussions centered around add-on marketplaces and non-free browser extensions. Most parties agreed that the current system of asking for donations is not a viable revenue stream for an independent developer and that add-on developers should have a marketplace to sell their software, if they choose to do so.

Freemium

Since participating in those discussions, I had been quietly considering developing a freemium add-on: an upgraded version of one of my free add-ons that comes with extra features for those willing to pay a few bucks. I was planning on waiting until a proper add-on marketplace materialized (Mozilla is working on one, as are some independent third-parties), but a few weeks ago, I decided to take the plunge on my own. (Worst case, I’d have a premium add-on ready and waiting as soon as a marketplace opened for business.) I eventually settled on freemium-izing URL Fixer.

URL Fixer is an add-on that helps you avoid typos when typing URLs in the browser’s address bar. According to the statistics gathered by Mozilla, it has been downloaded 960,000 times, and is actively used by 70,000 people. I wrote the first version in June of 2006, and it has gotten generally positive reviews, but the one request that has been consistently raised is for the capacity to add custom corrections — URL Fixer will automatically convert “google.con” to “google.com” (because google.con is most definitely an invalid URL), but it won’t fix “gogle.com” to “google.com” (because “gogle.com” might be a legitimate website).

I’ve always resisted adding any sort of correction mechanism that won’t work 100% of the time for 100% of the users (what if someone really wanted to visit gogle.com?), but I could justify this kind of feature in a non-free add-on, as the users who want the feature will pay for the add-on, and the users that don’t want it can continue using the free version.

Preparation

To confirm my assumptions that people would pay for the ability to add their own corrections, I put a poll on the page that is shown after a user installs URL Fixer. (This page contains basic instructions on how the add-on works in order to cut down on confusion for new users.) This poll asked, “Would you pay .99 for a premium version of URL Fixer that allows you to add custom corrections?”

Out of the several thousand people that saw this page, 78 participated in the poll. Out of those 78, 29 said they’d pay for a premium version of URL Fixer. That’s 37%! You can imagine the numbers that started going through my head at this point – 37% of 70,000 current users is 26,000 users; multiply that by .99, and that’s a quick ,000. (Never mind the 1.5% poll participation rate.)

Execution

I spent a few days writing the new features, making sure that the premium version (“URL Fixer Plus“) was able to coexist with the free version if both were installed, and writing a barebones add-on marketplace. Last Friday night, I published URL Fixer Plus and the marketplace, linking them from the firstrun page of URL Fixer. I then went to sleep and waited for the money to roll in.

Results

So how did things turn out? Of the 15,121 people that have upgraded or installed URL Fixer since I released URL Fixer Plus, 342 of them have clicked on the “Try URL Fixer Plus” link. Out of those 342, only 8 decided to actually pay the .99. For those of you not keeping track at home, that’s a conversion rate of 0.05%, a far cry from the ~37% that I expected. So, what could have gone wrong? Take your pick from these possibilities:

  • People don’t read firstun pages.
  • Anyone who didn’t participate in the poll was effectively voting “no,” putting the result much more in line with expectations.
  • Even people who did participate in the poll and said yes changed their mind when it came time to actually pay.
  • .99 is too much.
  • People want to buy the add-on, but they don’t want to use PayPal.
  • People don’t trust me enough to buy something from me.
  • People don’t expect to pay for add-ons at all.
  • All of the above.
  • None of the above.

Even though I’d consider this project a failure thus far, I’ll certainly participate in any legitimate add-on marketplaces that crop up. Several of the possible causes of URL Fixer Plus’s failure could be fixed by having a trusted, discoverable marketplace, and that’s something I can’t build on my own. In the meantime, I’ll continue to offer URL Fixer Plus as an upgrade to URL Fixer. The marginal cost of selling any more copies is essentially zero, so I have nothing to lose.

Questions

What are your thoughts? Why did I get such a low conversion rate? What could I have done better?

Standard
Life, Programming

Blast from the past: The Humor Archive

There’s no point to this post; it’s just a collection of memories I have about launching one of my first websites.

The first website I ever built was called “The Humor Archive:”

  • It went online in late 1999 at the URL http://ticon.net/~finke/. The motivation behind the building of the site was that our ISP offered free Web space to all subscribers; my dad agreed that I could use the space if I learned how to create a website, so I chose to use the spacious 5MB to build a collection of funny lists and jokes. I curated the archive by copying/pasting funny things I found online (usually without attribution, because I was young and didn’t know any better), and I also included a new humor column each week written by my dad. He didn’t write them specifically for my site; they had been published previously in a number of Midwestern newspapers.

  • I dutifully submitted the site to various directories and search engines; at some point, I realized that I was getting the short end of the stick from dmoz, since it listed sites in alphabetical order. That’s when I had the stroke of genius to rename “The Humor Archive” to “Absurd! The Humor Archive.” I would later rename it again in an attempt to game the listings, this time to “!Absurd! The Humor Archive.” My SEO skills were obviously ahead of their time. (Looking back through the archives of alt.html, I realized that I had originally named the site “Did you hear the one about?…”, but changed it after this response from legendary Finn Jukka Korpela.)

  • Not long after I launched it (using Notepad and WS_FTP), I noticed that my odometer-style hit counter was jumping up by dozens each time I would refresh the page. As it turned out, The Humor Archive was the fourth result when AOL users searched for “humor.” That high placement didn’t last long, but the feeling while it did was exhilarating.

  • For a short while, I ran a mailing list called “The Daily Laugh.” I doubt I sent out more than five editions of the “daily” laugh over a period of six months, but I never bothered to change the name to “The Occasional Laugh.”

  • When my family moved and I no longer had access to our old ISPs Web space, The Humor Archive disappeared. Surprisingly, someone actually noticed.

Eventually, I took The Humor Archive offline and forgot about it.

UNTIL NOW!

I’ve collected the pieces of the site that I could find online and in my backups and reinstated it at humorarchive.efinke.com.

Sharp-eyed readers with a memory for early-21st century websites may recognize the header graphic as coming from FlamingText.com. I was unable to find the original header image, but as it turns out, FlamingText is still up and running, producing exactly the same graphics as it did ten years ago, so the current header is a faithful reproduction. (I don’t remember why I chose cows as the theme for the site – besides the bovine header, the list bullets are all little cow heads – but it was probably because cows are hilarious.)

Standard
Browser Add-ons, Google Chrome, ScribeFire

ScribeFire for Google Chrome

I’ve published a post over on the ScribeFire blog with the details, but if you’re running Google Chrome, you can now install ScribeFire for Google Chrome:

This first (alpha) version took three weeks of development, contains about 3,000 lines of JavaScript, uses jQuery, and is completely open-source. Oh, and I’m using it to write this blog post.  

Standard
Browser Add-ons, Mozilla, Mozilla Add-ons, Mozilla Firefox

Ambilight for Your Browser or: Monetizing an Add-on with Fat Plug

I’ve written a new Firefox extension; it’s called True Colors, and it bleeds the colors from the web page you’re viewing into the tab bar and status bar. Think of it as Ambilight for your browser.

The technical details behind the extension are interesting, but I won’t go into detail on that here, since Splashnology’s blog post describing how Ambilight for video works explains 99% of the technique.

There’s another important aspect to this extension. I’ve used it as a testbed for Fat Plug’s add-on monetization system. A Fat Plug-enabled extension will add/change ads on various websites, and, in turn, funnel a portion of the revenue from those ads to the extension developer.

There’s some controversy around this technique. Mozilla has opted to deny any Fat Plug extensions that are submitted to the Mozilla Add-ons Gallery. Website owners, I imagine, wouldn’t appreciate their ads being replaced with ads that don’t earn them any money. (Although it would harm them no more than ad-blocking extensions, which Mozilla does allow.)

However, to an add-on developer, the idea is intriguing: “What if I collected half of all the website ad revenue from all of the users that use my extensions?” A developer of a popular add-on could retire after a couple of years to the sands of Grand Cayman and spend his days writing free software that needs no monetization.

So True Colors is my testbed for Fat Plug’s technology. I won’t be uploading it to Mozilla Add-ons, and I won’t be publishing it anywhere that doesn’t make the Fat Plug integration obvious. I’m not looking to stealthily trick users into becoming my little monetization machines, which is why I purposefully wrote a simple extension that doesn’t add functionality to the browser as my first foray into Fat Plug.

If you’re interested in seeing how Fat Plug modifies ads on websites, you can install True Colors, agree to the license agreement, and then set the preferences extensions.fatplug.enableoutlinediv and extensions.fatplug.enableoutlinelink to true in about:config. Any ads that Fat Plug adds or modifies will be outlined in red for your convenience.

If you’re interested in the coloring functionality but are wary of the Fat Plug integration, you can install the extension and just not agree to the license agreement. That will keep the tab and status bar coloring functionality but disable Fat Plug’s code.

Standard
Browser Add-ons, Life, Mozilla, Mozilla Firefox, Programming, TwitterBar

Making Add-on/User Communication Less Annoying

Update: TwitterBar was sold to HootSuite and renamed HootBar in March of 2011. TwitterBar for Chrome was discontinued in October of 2012.

When a new user downloads TwitterBar, there are a number of things I want them to know or questions I want to ask them. So what is the best method to communicate with an add-on user?

The solution I’ve been using for a while is to pop up a dialog like this:

There are several problems with this approach, all of which I decided to ignore when I implemented it:

  • It steals the user’s focus.
  • It’s annoying.
  • The user might click cancel without reading it just to get rid of it.
  • It’s annoying.
  • The user might immediately (but accidentally) click elsewhere, hiding the dialog behind another window, never to be seen again.
  • It’s annoying.
  • It’s extra code and work to pop up a special dialog like this.
  • It’s annoying.

Back when there was only one dialog, I decided that these were acceptable faults. However, since then, I’ve come up with a few more questions I want to ask users, so now instead of one annoying dialog, there are three or four annoying dialogs – a new one appearing each time you restart Firefox.

Predictably (or so it should have been), users don’t like to be assaulted with new dialogs each time they start their browser. Most likely, they’re starting their browser for some purpose other than using my add-on, so my add-on shouldn’t steal their attention. As one user so elegantly put it,

“I really love the TwitterBar, but after the most recent TwitterBar update, I noticed I kept getting these annoying as hell pop-ups from TwitterBar about TwitterBar. After the third one (while I was in the middle of doing something and became distracted with this pop-up dialog box TwitterBar tip of the day), I uninstalled it. If you want to keep your clients, don’t constantly tap them on the shoulder.

I had already been working on redesigning these add-on/user interactions when I got that email, so the user’s message reinforced what I had suspected: I was alienating my userbase.

Here’s the new scheme I’ve settled on for now:

It’s a notification bar, much like the one that appears when Firefox blocks a popup. It has these positive qualities:

  • It doesn’t steal focus or interrupt the user.
  • It’s not in-your-face, so it’s less likely (I assume) to be dismissed without thought.
  • It can’t be lost behind another window.
  • The amount of code to implement it is less, and it’s more in tune with the browser interface.
  • It’s not as annoying.

I’d love your feedback on this change. Is it enough? Should I stop bothering users altogether and just let them discover their way around the add-on? I’m open to all ideas.

(If you’d like to try a version of TwitterBar with this new notification method, you can download it here. Although, if you’ve already seen the old dialog-style version of these notifications, you won’t see the new-style ones anyway.)

Standard
Browser Add-ons, Mozilla Firefox, Twitter, TwitterBar

TwitterBar 2.9 Available: Post to Multiple Twitter Accounts

Update: TwitterBar was sold to HootSuite and renamed HootBar in March of 2011. TwitterBar for Chrome was discontinued in October of 2012.

Version 2.9 of TwitterBar for Firefox was made available on Mozilla Add-ons today, and it has a very cool new feature: you can now use TwitterBar with more than one Twitter account.

To post to a specific account, just type your message like this:

I am posting to my other account. –@other_account –post

If you haven’t yet authorized TwitterBar for @other_account, you’ll be walked through the authorization process.

If you’ve authorized more than one account, and you don’t specify which account you want to post to, you’ll be given a list of choices:

You can manage your accounts from the TwitterBar options (just type “–options”).

Finally, to authorize a new account without posting to it, just type “–account” in the URL bar.

To install this new version of TwitterBar, download it from Mozilla Add-ons.

The next obvious step is the ability to post to multiple accounts simultaneously, and the next version of TwitterBar will offer than feature. If you’d like to beta-test that update, e-mail me and let me know.

Standard
Browser Add-ons, JavaScript, Mozilla, Programming

Uploading form data and files with JavaScript (Mozilla)

One problem I stumble across occasionally in writing Firefox extensions is properly uploading form data that includes a file – that is, assembling the POST request in JavaScript while still maintaining the sanctity of any file or string data. You can’t just do this:

var request = "--boundary\r\n some text\r\n--boundary" + fileBytes + "\r\n--boundary--";

I had to spend a bit of time getting this just right in order to allow ScribeFire to upload media to Posterous, so I’m posting below the final solution at which I arrived; it was cobbled together from a dozen different examples I found around the Web (none of them solving the full problem), then lovingly massaged into the elegant function you see before you. With this function, you can pass in an array of fields and files, and the request will be crafted and returned to you, ready for upload.

Instructions for use are in the comment block at the top of the function.

function createPostRequest(args) {
  /**
   * Generates a POST request body for uploading.
   *
   * args is an associative array of the form fields.
   *
   * Example:
   * var args = { "field1": "abc", "field2" : "def", "fileField" : 
   *              { "file": theFile, "headers" : [ "X-Fake-Header: foo" ] } };
   * 
   * theFile is an nsILocalFile; the headers param for the file field is optional.
   *
   * This function returns an array like this:
   * { "requestBody" : uploadStream, "boundary" : BOUNDARY }
   * 
   * To upload:
   *
   * var postRequest = createPostRequest(args);
   * var req = new XMLHttpRequest();
   * req.open("POST", ...);
   * req.setRequestHeader("Content-Type","multipart/form-data; boundary="+postRequest.boundary);
   * req.setRequestHeader("Content-Length", (postRequest.requestBody.available()));
   * req.send(postRequest.requestBody);
   */
  
  function stringToStream(str) {
    function encodeToUtf8(oStr) {
      var utfStr = oStr;
      var uConv = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                    .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
      uConv.charset = "UTF-8";
      utfStr = uConv.ConvertFromUnicode(oStr);

      return utfStr;
    }
    
    str = encodeToUtf8(str);
    
    var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
                   .createInstance(Components.interfaces.nsIStringInputStream);
    stream.setData(str, str.length);
    
    return stream;
  }
  
  function fileToStream(file) {
    var fpLocal  = Components.classes['@mozilla.org/file/local;1']
                     .createInstance(Components.interfaces.nsILocalFile);
    fpLocal.initWithFile(file);

    var finStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                      .createInstance(Components.interfaces.nsIFileInputStream);                
    finStream.init(fpLocal, 1, 0, false);

    var bufStream = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
                      .createInstance(Components.interfaces.nsIBufferedInputStream);
    bufStream.init(finStream, 9000000);
    
    return bufStream;
  }
  
  var mimeSvc = Components.classes["@mozilla.org/mime;1"]
                  .getService(Components.interfaces.nsIMIMEService);
  const BOUNDARY = "---------------------------32191240128944"; 
  
  var streams = [];
  
  for (var i in args) {
    var buffer = "--" + BOUNDARY + "\r\n";
    buffer += "Content-Disposition: form-data; name=\"" + i + "\"";
    streams.push(stringToStream(buffer));
    
    if (typeof args[i] == "object") {
      buffer = "; filename=\"" + args[i].file.leafName + "\"";
      
      if ("headers" in args[i]) {
        if (args[i].headers.length > 0) {
          for (var q = 0; q < args[i].headers.length; q++){
            buffer += "\r\n" + args[i].headers[q];
          }
        }
      }
      
      var theMimeType = mimeSvc.getTypeFromFile(args[i].file);
      
      buffer += "\r\nContent-Type: " + theMimeType;
      buffer += "\r\n\r\n";
      
      streams.push(stringToStream(buffer));
      
      streams.push(fileToStream(args[i].file));
    }
    else {
      buffer = "\r\n\r\n";
      buffer += args[i];
      buffer += "\r\n";
      streams.push(stringToStream(buffer));
    }
  }
  
  var buffer = "--" + BOUNDARY + "--\r\n";
  streams.push(stringToStream(buffer));
  
  var uploadStream = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
                       .createInstance(Components.interfaces.nsIMultiplexInputStream);
  
  for (var i = 0; i < streams.length; i++) {
    uploadStream.appendStream(streams[i]);
  }
  
  return { "requestBody" : uploadStream, "boundary": BOUNDARY };
}
Standard