Bug 653141 - Allow language choice on first-run. r=mfinkle
authorWes Johnston <wjohnston@mozilla.com>
Tue, 07 Jun 2011 12:11:37 -0700
changeset 70700 fc090d032d45843271be9419ee852870900340c7
parent 70699 b42bd885a3d59e9796dc289a73a76e4191e2fdb9
child 70701 bfd819293f59c0f7fde2e377867dfd80ea8ded33
push id20388
push userwjohnston@mozilla.com
push dateTue, 07 Jun 2011 19:12:34 +0000
treeherdermozilla-central@bfd819293f59 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs653141
milestone7.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 653141 - Allow language choice on first-run. r=mfinkle
build/automation.py.in
mobile/app/mobile.js
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/firstrun/firstrun.xhtml
mobile/chrome/content/input.js
mobile/chrome/content/localePicker.js
mobile/chrome/content/localePicker.xul
mobile/chrome/jar.mn
mobile/components/BrowserCLH.js
mobile/locales/en-US/chrome/localepicker.properties
mobile/locales/jar.mn
mobile/modules/LocaleRepository.jsm
mobile/modules/Makefile.in
mobile/themes/core/browser.css
mobile/themes/core/gingerbread/browser.css
mobile/themes/core/gingerbread/defines.inc
mobile/themes/core/gingerbread/localePicker.css
mobile/themes/core/gingerbread/platform.css
mobile/themes/core/jar.mn
mobile/themes/core/localePicker.css
mobile/themes/core/platform.css
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -329,16 +329,17 @@ class Automation(object):
     # Set up permissions database
     locations = self.readLocations()
     self.setupPermissionsDatabase(profileDir,
       {'allowXULXBL':[(l.host, 'noxul' not in l.options) for l in locations]});
 
     part = """\
 user_pref("browser.console.showInPanel", true);
 user_pref("browser.dom.window.dump.enabled", true);
+user_pref("browser.firstrun.show.localepicker", false);
 user_pref("dom.allow_scripts_to_close_windows", true);
 user_pref("dom.disable_open_during_load", false);
 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
 user_pref("dom.max_chrome_script_run_time", 0);
 user_pref("dom.popup_maximum", -1);
 user_pref("dom.send_after_paint_to_content", true);
 user_pref("dom.successive_dialog_time_limit", 0);
 user_pref("signed.applets.codebase_principal_support", true);
--- a/mobile/app/mobile.js
+++ b/mobile/app/mobile.js
@@ -210,16 +210,19 @@ pref("extensions.getAddons.cache.enabled
 pref("extensions.getAddons.maxResults", 15);
 pref("extensions.getAddons.recommended.browseURL", "https://addons.mozilla.org/%LOCALE%/mobile/recommended/");
 pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/mobile/api/%API_VERSION%/list/featured/all/%MAX_RESULTS%/%OS%/%VERSION%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/mobile/search?q=%TERMS%");
 pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/mobile/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%");
 pref("extensions.getAddons.browseAddons", "https://addons.mozilla.org/%LOCALE%/mobile/");
 pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/mobile/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
 
+/* preference for the locale picker */
+pref("extensions.getLocales.get.url", "");
+
 /* blocklist preferences */
 pref("extensions.blocklist.enabled", true);
 pref("extensions.blocklist.interval", 86400);
 pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
 pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
 
 /* block popups by default, and notify the user about blocked popups */
 pref("dom.disable_open_during_load", true);
@@ -638,11 +641,14 @@ pref("urlclassifier.confirm-age", 2700);
 
 // Maximum size of the sqlite3 cache during an update, in bytes
 pref("urlclassifier.updatecachemax", 4194304);
 
 // URL for checking the reason for a malware warning.
 pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
 #endif
 
-// prevent focus to show/hide the virtual keyboard if the action is not
+// True if this is the first time we are showing about:firstrun
+pref("browser.firstrun.show.uidiscovery", true);
+pref("browser.firstrun.show.localepicker", true);
+
 // initiated by a user
 pref("content.ime.strict_policy", true);
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -261,17 +261,17 @@ var Browser = {
       Browser.styles["window-height"].height = h + "px";
       Browser.styles["toolbar-height"].height = toolbarHeight + "px";
 
       // Tell the UI to resize the browser controls
       BrowserUI.sizeControls(w, h);
       ViewableAreaObserver.update();
 
       // Restore the previous scroll position
-      let restorePosition = Browser.controlsPosition;
+      let restorePosition = Browser.controlsPosition || { hideSidebars: true };
       if (restorePosition.hideSidebars) {
         restorePosition.hideSidebars = false;
         Browser.hideSidebars();
       } else {
         // Handle Width transformation of the tabs sidebar
         if (restorePosition.x) {
           let [,, leftWidth, rightWidth] = Browser.computeSidebarVisibility();
           let delta = ((restorePosition.leftSidebar - leftWidth) || (restorePosition.rightSidebar - rightWidth));
@@ -373,16 +373,21 @@ var Browser = {
     messageManager.addMessageListener("scroll", this);
     messageManager.addMessageListener("Browser:CertException", this);
     messageManager.addMessageListener("Browser:BlockedSite", this);
 
     // broadcast a UIReady message so add-ons know we are finished with startup
     let event = document.createEvent("Events");
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
+
+    // if we have an opener this was not the first window opened and will not
+    // receive an initial resize event. instead we fire the resize handler manually
+    if (window.opener)
+      resizeHandler({ target: window });
   },
 
   _alertShown: function _alertShown() {
     // ensure that the full notification still visible, even if the urlbar is floating
     if (BrowserUI.isToolbarLocked())
       Browser.pageScrollboxScroller.scrollTo(0, 0);
   },
 
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -531,25 +531,25 @@
       <dialog id="syncsetup-dialog" class="content-dialog" flex="1">
         <hbox class="prompt-title">
           <description>&sync.setup.title;</description>
         </hbox>
         <separator class="prompt-line"/>
         <vbox id="syncsetup-simple" class="syncsetup-page" flex="1">
           <scrollbox id="sync-message" class="prompt-message" orient="vertical" flex="1">
             <description class="syncsetup-desc syncsetup-center" flex="1">&sync.setup.jpake;</description>
-            <description class="syncsetup-center syncsetup-link" flex="1" onclick="WeaveGlue.openTutorial();">&sync.setup.tutorial;</description>
+            <description class="syncsetup-center link" flex="1" onclick="WeaveGlue.openTutorial();">&sync.setup.tutorial;</description>
             <separator/>
             <vbox align="center" flex="1">
               <description id="syncsetup-code1" class="syncsetup-code">....</description>
               <description id="syncsetup-code2" class="syncsetup-code">....</description>
               <description id="syncsetup-code3" class="syncsetup-code">....</description>
             </vbox>
             <separator/>
-            <description class="syncsetup-center syncsetup-link" flex="1" onclick="WeaveGlue.openManual();">&sync.fallback;</description>
+            <description class="syncsetup-center link" flex="1" onclick="WeaveGlue.openManual();">&sync.fallback;</description>
             <separator flex="1"/>
           </scrollbox>
           <hbox class="prompt-buttons" pack="center">
             <button oncommand="WeaveGlue.close();">&sync.setup.cancel;</button>
           </hbox>
         </vbox>
         <vbox id="syncsetup-fallback" class="syncsetup-page" flex="1" hidden="true">
           <scrollbox class="prompt-message" orient="vertical" flex="1">
--- a/mobile/chrome/content/firstrun/firstrun.xhtml
+++ b/mobile/chrome/content/firstrun/firstrun.xhtml
@@ -112,18 +112,24 @@
         }
 
         function loadAddons() {
           let win = getChromeWin();
           win.BrowserUI.showPanel("addons-container");
         }
 
         function init() {
+          let prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).QueryInterface(Ci.nsIPrefBranch2);
+          if (prefs.getBoolPref("browser.firstrun.show.uidiscovery")) {
+            startDiscovery();
+            prefs.setBoolPref("browser.firstrun.show.uidiscovery", false);
+          } else {
+            endDiscovery();
+          }
           setupLinks();
-          startDiscovery();
         }
         
         function startDiscovery() {
           let win = getChromeWin();
           let [leftWidth, rightWidth] = win.Browser.computeSidebarVisibility();
           if (leftWidth > 0 || rightWidth > 0) {
             endDiscovery();
             return;
--- a/mobile/chrome/content/input.js
+++ b/mobile/chrome/content/input.js
@@ -37,16 +37,18 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
+Components.utils.import("resource://gre/modules/Geometry.jsm");
+
 // Maximum delay in ms between the two taps of a double-tap
 const kDoubleClickInterval = 400;
 
 // Maximum distance in inches between the taps of a double-tap
 const kDoubleClickRadius = 0.4;
 
 // Amount of time to wait before tap is generate a mousemove 
 const kOverTapWait = 150;
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/localePicker.js
@@ -0,0 +1,365 @@
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource:///modules/LocaleRepository.jsm");
+
+let stringPrefs = [
+  { selector: "#continue-in-button", pref: "continueIn", data: ["CURRENT_LANGUAGE"] },
+  { selector: "#change-language", pref: "choose", data: null },
+  { selector: "#picker-title", pref: "chooseLanguage", data: null },
+  { selector: "#continue-button", pref: "continue", data: null },
+  { selector: "#cancel-button", pref: "cancel", data: null },
+  { selector: "#intalling-message", pref: "installing", data: ["CURRENT_LANGUAGE"]  },
+  { selector: "#cancel-install-button", pref: "cancel", data: null },
+  { selector: "#installing-error", pref: "installerror", data: null },
+  { selector: "#install-continue", pref: "continue", data: null }
+];
+
+let LocaleUI = {
+  _strings: null,
+
+  get strings() {
+    if (!this._strings)
+      this._strings = Services.strings.createBundle("chrome://browser/locale/localepicker.properties");
+    return this._strings;
+  },
+
+  set strings(aVal) {
+    this._strings = aVal;
+  },
+
+  get _mainPage() {
+    delete this._mainPage;
+    return this._mainPage = document.getElementById("main-page");
+  },
+
+  get _pickerPage() {
+    delete this._pickerPage;
+    return this._pickerPage = document.getElementById("picker-page");
+  },
+
+  get _installerPage() {
+    delete this._installerPage;
+    return this._installerPage = document.getElementById("installer-page");
+  },
+
+  get _deck() {
+    delete this._deck;
+    return this._deck = document.getElementById("language-deck");
+  },
+
+  _currentInstall: null, // used to cancel an install
+
+  get selectedPanel() {
+    return this._deck.selectedPanel;
+  },
+
+  set selectedPanel(aPanel) {
+    this._deck.selectedPanel = aPanel;
+  },
+
+  get list() {
+    delete this.list;
+    return this.list = document.getElementById("language-list");
+  },
+
+  _createItem: function(aId, aText, aLocale) {
+    let item = document.createElement("richlistitem");
+    item.setAttribute("id", aId);
+
+    let description = document.createElement("description");
+    description.appendChild(document.createTextNode(aText));
+    description.setAttribute('flex', 1);
+    item.appendChild(description);
+    item.setAttribute("locale", getTargetLanguage(aLocale.addon));
+
+    if (aLocale) {
+      item.locale = aLocale.addon;
+      let checkbox = document.createElement("image");
+      checkbox.classList.add("checkbox");
+      item.appendChild(checkbox);
+    } else {
+      item.classList.add("message");
+    }
+    return item;
+  },
+
+  addLocales: function(aLocales) {
+    let fragment = document.createDocumentFragment();
+    let selectedItem = null;
+    let bestMatch = NO_MATCH;
+
+    for each (let locale in aLocales) {
+      let targetLang = getTargetLanguage(locale.addon);
+      if (document.querySelector('[locale="' + targetLang + '"]'))
+        continue;
+
+      let item = this._createItem(targetLang, locale.addon.name, locale);
+      let match = localesMatch(targetLang, this.language);
+      if (match > bestMatch) {
+        bestMatch = match;
+        selectedItem = item;
+      }
+      fragment.appendChild(item);
+    }
+    this.list.appendChild(fragment);
+    if (selectedItem && !this.list.selectedItem);
+      this.list.selectedItem = selectedItem;
+  },
+
+  loadLocales: function() {
+    while (this.list.firstChild)
+      this.list.removeChild(this.list.firstChild);
+    this.addLocales(this.availableLocales);
+    LocaleRepository.getLocales(this.addLocales.bind(this));
+  },
+
+  showPicker: function() {
+    LocaleUI.selectedPanel = LocaleUI._pickerPage;
+    LocaleUI.loadLocales();
+  },
+
+  closePicker: function() {
+    if (this._currentInstall) {
+      Services.prefs.setBoolPref("intl.locale.matchOS", false);
+      Services.prefs.setCharPref("general.useragent.locale", getTargetLanguage(this._currentInstall));
+    }
+    this.selectedPanel = this._mainPage;
+  },
+
+  _language: "",
+
+  set language(aVal) {
+    if (aVal == this._language)
+      return;
+
+    Services.prefs.setBoolPref("intl.locale.matchOS", false);
+    Services.prefs.setCharPref("general.useragent.locale", aVal);
+    this._language = aVal;
+
+    this.strings = null;
+    this.updateStrings();
+  },
+
+  get language() {
+    return this._language;
+  },
+
+  set installStatus(aVal) {
+    this._installerPage.selectedPanel = document.getElementById("installer-page-" + aVal);
+  },
+
+  clearInstallError: function() {
+    this.installStatus = "installing";
+    this.selectedPanel = this._pickerPage;
+  },
+
+  selectLanguage: function(aEvent) {
+    let locale = this.list.selectedItem.locale;
+    if (locale.install)
+      this.updateStrings(locale);
+    else {
+      this.language = getTargetLanguage(locale);
+      if (this._currentInstall)
+        this._currentInstall = null;
+    }
+  },
+
+  installAddon: function() {
+    let locale = LocaleUI.list.selectedItem.locale;
+    LocaleUI._currentInstall = locale;
+
+    if (locale.install) {
+      LocaleUI.selectedPanel = LocaleUI._installerPage;
+      locale.install.addListener(installListener);
+      locale.install.install();
+    } else {
+      this.closePicker();
+    }
+  },
+
+  cancelPicker: function() {
+    if (this._currentInstall)
+      this._currentInstall = null;
+    // restore the last known "good" locale
+    this.language = this.defaultLanguage;
+    this.updateStrings();
+    this.closePicker();
+  },
+
+  closeWindow : function() {
+    // Trying to close this window and open a new one results in a corrupt UI.
+    if (false && LocaleUI._currentInstall) {
+      let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+      Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+    
+      if (cancelQuit.data == false) {
+        let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+        appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eForceQuit);
+        Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false);
+      }
+    } else {
+      let argString = null;
+      if (window.arguments) {
+        argString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+        argString.data = window.arguments.join(",");
+      }
+      let win = Services.ww.openWindow(window, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", argString);
+      window.close();
+    }
+  },
+
+  cancelInstall: function () {
+    if (LocaleUI._currentInstall) {
+      let addonInstall = LocaleUI._currentInstall.install;
+      try { addonInstall.cancel(); }
+      catch(ex) { }
+      LocaleUI._currentInstall = null;
+
+      this.language = this.defaultLanguage;
+    }
+  },
+
+  updateStrings: function (aAddon) {
+    stringPrefs.forEach(function(aPref) {
+      if (!aPref.element)
+        aPref.element = document.querySelector(aPref.selector);
+  
+      let string = "";
+      try {
+        string = getString(aPref.pref, aPref.data, aAddon);
+      } catch(ex) { }
+      aPref.element.textContent = string;
+    });
+  }
+}
+
+// Gets the target language for a locale addon
+// For now this returns the targetLocale, although if and addon doesn't
+// specify a targetLocale we could attempt to guess the locale from the addon's name
+function getTargetLanguage(aAddon) {
+  return aAddon.targetLocale;
+}
+
+// Gets a particular string for the passed in locale
+// Parameters: aStringName - The name of the string property to get
+//             aDataset - an array of properties to use in a formatted string
+//             aAddon - An addon to attempt to get dataset properties from
+function getString(aStringName, aDataSet, aAddon) {
+  if (aDataSet) {
+    let databundle = aDataSet.map(function(aData) {
+      switch (aData) {
+        case "CURRENT_LANGUAGE" :
+          if (aAddon)
+            return aAddon.name;
+          try { return LocaleUI.strings.GetStringFromName("name"); }
+          catch(ex) { return null; }
+          break;
+        default :
+      }
+      return "";
+    });
+    if (databundle.some(function(aItem) aItem === null))
+      throw("String not found");
+    return LocaleUI.strings.formatStringFromName(aStringName, databundle, databundle.length);
+  }
+
+  return LocaleUI.strings.GetStringFromName(aStringName);
+}
+
+let installListener = {
+  onNewInstall: function(install) { },
+  onDownloadStarted: function(install) { },
+  onDownloadProgress: function(install) { },
+  onDownloadEnded: function(install) { },
+  onDownloadCancelled: function(install) {
+    LocaleUI.cancelInstall();
+    LocaleUI.showPicker();
+  },
+  onDownloadFailed: function(install) {
+    LocaleUI.cancelInstall();
+    LocaleUI.installStatus = "error";
+  },
+  onInstallStarted: function(install) { },
+  onInstallEnded: function(install, addon) {
+    LocaleUI.updateStrings(LocaleUI._currentInstall);
+    LocaleUI.closePicker();
+  },
+  onInstallCancelled: function(install) {
+    LocaleUI.cancelInstall();
+    LocaleUI.showPicker();
+  },
+  onInstallFailed: function(install) {
+    LocaleUI.cancelInstall();
+    LocaleUI.installStatus = "error";
+  },
+  onExternalInstall: function(install, existingAddon, needsRestart) { }
+}
+
+const PERFECT_MATCH = 2;
+const GOOD_MATCH = 1;
+const NO_MATCH = 0;
+//Compares two locales of the form AB or AB-CD
+//returns GOOD_MATCH if AB == AB in both locales, PERFECT_MATCH if AB-CD == AB-CD
+function localesMatch(aLocale1, aLocale2) {
+  if (aLocale1 == aLocale2)
+    return PERFECT_MATCH;
+
+  let short1 = aLocale1.split("-")[0];
+  let short2 = aLocale2.split("-")[0];
+  return (short1 == short2) ? GOOD_MATCH : NO_MATCH;
+}
+
+function start() {
+  let mouseModule = new MouseModule();
+
+  let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+  chrome.QueryInterface(Ci.nsIToolkitChromeRegistry);
+  let availableLocales = chrome.getLocalesForPackage("browser");
+
+  let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService);
+  let systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE");
+  let matchingLocale = "";
+
+  let bestMatch = NO_MATCH;
+
+  let strings = Services.strings.createBundle("chrome://browser/content/languages.properties");
+  LocaleUI.availableLocales = [];
+  while (availableLocales.hasMore()) {
+    let locale = availableLocales.getNext();
+
+    let label = locale;
+    try { label = strings.GetStringFromName(locale); }
+    catch (e) { }
+    LocaleUI.availableLocales.push({addon: { id: locale, name: label, targetLocale: locale }});
+
+    // see if we have a locale that looks like the system locale
+    // if it is not the current locale, switch to it
+    let match = localesMatch(systemLocale, locale);
+    if (match > bestMatch) {
+      bestMatch = match;
+      matchingLocale = locale;
+    }
+  }
+
+  if (matchingLocale != chrome.getSelectedLocale("browser"))
+    LocaleUI.language = matchingLocale;
+  else {
+    LocaleUI._language = chrome.getSelectedLocale("browser");
+    LocaleUI.updateStrings();
+  }
+
+  // update the page strings and show the correct page
+  LocaleUI.defaultLanguage = LocaleUI._language;
+  window.addEventListener("resize", resizeHandler, false);
+}
+
+function resizeHandler() {
+  let elements = document.getElementsByClassName("window-width");
+  for (let i = 0; i < elements.length; i++)
+    elements[i].setAttribute("width", Math.min(800, window.innerWidth));
+}
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/localePicker.xul
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/localePicker.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="start();"
+        width="480"
+        height="800">
+  <script src="chrome://browser/content/Util.js" type="application/javascript;version=1.8"/>
+  <script src="chrome://browser/content/input.js" type="application/javascript;version=1.8"/>
+  <script src="chrome://browser/content/localePicker.js" type="application/javascript;version=1.8"/>
+  <deck id="language-deck" flex="1">
+    <vbox id="main-page" class="pane" flex="1">
+      <spacer flex="1"/>
+      <button class="continue" id="continue-in-button" onclick="LocaleUI.closeWindow();" crop="center"/>
+      <description id="change-language" class="link" onclick="LocaleUI.showPicker();" role="button"/>
+    </vbox>
+
+    <vbox id="picker-page" class="pane" flex="1">
+      <description id="picker-title"/>
+      <richlistbox id="language-list" onclick="LocaleUI.selectLanguage(event);" flex="1" class="window-width"/>
+      <hbox class="footer">
+        <button id="cancel-button" class="cancel" onclick="LocaleUI.cancelPicker();" crop="center"/>
+        <button id="continue-button" class="continue" onclick="LocaleUI.installAddon();" crop="center"/>
+      </hbox>
+    </vbox>
+
+    <deck id="installer-page" class="pane" flex="1">
+      <vbox id="installer-page-installing" flex="1" pack="center" align="center">
+        <description id="intalling-message" class="install-message"/>
+        <button id="cancel-install-button" class="cancel" onclick="LocaleUI.cancelInstall();" crop="center"/>
+      </vbox>
+      <vbox id="installer-page-error" flex="1" pack="center" align="center">
+        <description id="installing-error" class="install-message"/>
+        <button id="install-continue" class="continue" onclick="LocaleUI.clearInstallError();" crop="center"/>
+      </vbox>
+    </deck>
+
+  </deck>
+</window>
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -10,16 +10,18 @@ chrome.jar:
   content/firstrun/nav-arrow.png       (content/firstrun/nav-arrow.png)
 
 * content/about.xhtml                  (content/about.xhtml)
   content/config.xul                   (content/config.xul)
   content/config.js                    (content/config.js)
   content/aboutCertError.xhtml         (content/aboutCertError.xhtml)
   content/aboutCertError.css           (content/aboutCertError.css)
   content/aboutHome.xhtml              (content/aboutHome.xhtml)
+  content/localePicker.xul             (content/localePicker.xul)
+  content/localePicker.js              (content/localePicker.js)
 * content/aboutRights.xhtml            (content/aboutRights.xhtml)
   content/blockedSite.xhtml            (content/blockedSite.xhtml)
   content/languages.properties         (content/languages.properties)
 * content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
 * content/browser-ui.js                (content/browser-ui.js)
 * content/browser-scripts.js           (content/browser-scripts.js)
 * content/common-ui.js                 (content/common-ui.js)
--- a/mobile/components/BrowserCLH.js
+++ b/mobile/components/BrowserCLH.js
@@ -189,26 +189,29 @@ BrowserCLH.prototype = {
     // Open the main browser window, if we don't already have one
     let win;
     try {
       win = Services.wm.getMostRecentWindow("navigator:browser");
       if (!win) {
         // Default to the saved homepage
         let defaultURL = getHomePage();
 
-        // Override the default if we have a new profile
-        if (needHomepageOverride() == "new profile")
-            defaultURL = "about:firstrun";
-
         // Override the default if we have a URL passed on command line
         if (uris.length > 0) {
           defaultURL = uris[0].spec;
           uris = uris.slice(1);
         }
 
+        // Show the locale selector if we have a new profile
+        if (needHomepageOverride() == "new profile" && Services.prefs.getBoolPref("browser.firstrun.show.localepicker")) {
+          win = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL);
+          aCmdLine.preventDefault = true;
+          return;
+        }
+
         win = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL);
       }
 
       win.focus();
 
       // Stop the normal commandline processing from continuing. We just opened the main browser window
       aCmdLine.preventDefault = true;
     } catch (e) { }
new file mode 100644
--- /dev/null
+++ b/mobile/locales/en-US/chrome/localepicker.properties
@@ -0,0 +1,10 @@
+title=Select a language
+continueIn=Continue in %S
+name=English
+choose=Choose a different language
+chooseLanguage=Choose a Language
+cancel=Cancel
+continue=Continue
+installing=Installing %S
+installerror=Error installing language
+list.loading=Loading...
--- a/mobile/locales/jar.mn
+++ b/mobile/locales/jar.mn
@@ -4,16 +4,17 @@
 % locale browser @AB_CD@ %locale/@AB_CD@/browser/
   locale/@AB_CD@/browser/about.dtd                (%chrome/about.dtd)
   locale/@AB_CD@/browser/aboutCertError.dtd       (%chrome/aboutCertError.dtd)
   locale/@AB_CD@/browser/aboutHome.dtd            (%chrome/aboutHome.dtd)
   locale/@AB_CD@/browser/browser.dtd              (%chrome/browser.dtd)
   locale/@AB_CD@/browser/browser.properties       (%chrome/browser.properties)
   locale/@AB_CD@/browser/config.dtd               (%chrome/config.dtd)
   locale/@AB_CD@/browser/firstrun.dtd             (%chrome/firstrun.dtd)
+  locale/@AB_CD@/browser/localepicker.properties  (%chrome/localepicker.properties)
   locale/@AB_CD@/browser/region.properties        (%chrome/region.properties)
   locale/@AB_CD@/browser/preferences.dtd          (%chrome/preferences.dtd)
   locale/@AB_CD@/browser/checkbox.dtd             (%chrome/checkbox.dtd)
   locale/@AB_CD@/browser/notification.dtd         (%chrome/notification.dtd)
   locale/@AB_CD@/browser/sync.dtd                 (%chrome/sync.dtd)
   locale/@AB_CD@/browser/sync.properties          (%chrome/sync.properties)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
   locale/@AB_CD@/browser/feedback.dtd             (%chrome/feedback.dtd)
new file mode 100644
--- /dev/null
+++ b/mobile/modules/LocaleRepository.jsm
@@ -0,0 +1,334 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Mark Finkle <mfinkle@mozilla.com>
+ *  Wes Johnston <wjohnston@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let EXPORTED_SYMBOLS = ["LocaleRepository"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+var gServiceURL = Services.prefs.getCharPref("extensions.getLocales.get.url");
+
+// A map between XML keys to LocaleSearchResult keys for string values
+// that require no extra parsing from XML
+const STRING_KEY_MAP = {
+  name:               "name",
+  target_locale:      "targetLocale",
+  version:            "version",
+  icon:               "iconURL",
+  homepage:           "homepageURL",
+  support:            "supportURL"
+};
+
+var LocaleRepository = {
+  loggingEnabled: false,
+
+  log: function(aMessage) {
+    if (this.loggingEnabled)
+      dump(aMessage + "\n");
+  },
+
+  _getUniqueDescendant: function _getUniqueDescendant(aElement, aTagName) {
+    let elementsList = aElement.getElementsByTagName(aTagName);
+    return (elementsList.length == 1) ? elementsList[0] : null;
+  },
+  
+  _getTextContent: function _getTextContent(aElement) {
+    let textContent = aElement.textContent.trim();
+    return (textContent.length > 0) ? textContent : null;
+  },
+  
+  _getDescendantTextContent: function _getDescendantTextContent(aElement, aTagName) {
+    let descendant = this._getUniqueDescendant(aElement, aTagName);
+    return (descendant != null) ? this._getTextContent(descendant) : null;
+  },
+
+  getLocales: function getLocales(aCallback) {
+    if (!gServiceURL) {
+      aCallback([]);
+      return;
+    }
+    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+    request.mozBackgroundRequest = true;
+    request.open("GET", gServiceURL, true);
+    request.overrideMimeType("text/xml");
+  
+    let self = this;
+    request.onreadystatechange = function () {
+      if (request.readyState == 4) {
+        if (request.status == 200) {
+          self.log("---- got response")
+          let documentElement = request.responseXML.documentElement;
+          let elements = documentElement.getElementsByTagName("addon");
+          let totalResults = elements.length;
+          let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
+          if (parsedTotalResults >= totalResults)
+            totalResults = parsedTotalResults;
+
+          // TODO: Create a real Skip object from installed locales
+          self._parseLocales(elements, totalResults, { ids: [], sourceURIs: [] }, aCallback);
+        } else {
+          Cu.reportError("Locale Repository: Error getting locale from AMO [" + request.status + "]");
+        }
+      }
+    };
+  
+    request.send(null);
+  },
+
+  _parseLocale: function _parseLocale(aElement, aSkip) {
+    let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
+    let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
+  
+    let guid = this._getDescendantTextContent(aElement, "guid");
+    if (guid == null || skipIDs.indexOf(guid) != -1)
+      return null;
+  
+    let addon = new LocaleSearchResult(guid);
+    let result = {
+      addon: addon,
+      xpiURL: null,
+      xpiHash: null
+    };
+  
+    let self = this;
+    for (let node = aElement.firstChild; node; node = node.nextSibling) {
+      if (!(node instanceof Ci.nsIDOMElement))
+        continue;
+  
+      let localName = node.localName;
+  
+      // Handle case where the wanted string value is located in text content
+      // but only if the content is not empty
+      if (localName in STRING_KEY_MAP) {
+        addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
+        continue;
+      }
+  
+      // Handle cases that aren't as simple as grabbing the text content
+      switch (localName) {
+        case "type":
+          // Map AMO's type id to corresponding string
+          let id = parseInt(node.getAttribute("id"));
+          switch (id) {
+            case 5:
+              addon.type = "language";
+              break;
+            default:
+              WARN("Unknown type id when parsing addon: " + id);
+          }
+          break;
+        case "authors":
+          let authorNodes = node.getElementsByTagName("author");
+          Array.forEach(authorNodes, function(aAuthorNode) {
+            let name = self._getDescendantTextContent(aAuthorNode, "name");
+            if (name == null)
+              name = self._getTextContent(aAuthorNode);
+            let link = self._getDescendantTextContent(aAuthorNode, "link");
+            if (name == null && link == null)
+              return;
+  
+            let author = { name: name, link: link };
+            if (addon.creator == null) {
+              addon.creator = author;
+            } else {
+              if (addon.developers == null)
+                addon.developers = [];
+  
+              addon.developers.push(author);
+            }
+          });
+          break;
+        case "status":
+          let repositoryStatus = parseInt(node.getAttribute("id"));
+          if (!isNaN(repositoryStatus))
+            addon.repositoryStatus = repositoryStatus;
+          break;
+        case "all_compatible_os":
+          let nodes = node.getElementsByTagName("os");
+          addon.isPlatformCompatible = Array.some(nodes, function(aNode) {
+            let text = aNode.textContent.toLowerCase().trim();
+            return text == "all" || text == Services.appinfo.OS.toLowerCase();
+          });
+          break;
+        case "install":
+          // No os attribute means the xpi is compatible with any os
+          if (node.hasAttribute("os") && node.getAttribute("os")) {
+            let os = node.getAttribute("os").trim().toLowerCase();
+            // If the os is not ALL and not the current OS then ignore this xpi
+            if (os != "all" && os != Services.appinfo.OS.toLowerCase())
+              break;
+          }
+  
+          let xpiURL = this._getTextContent(node);
+          if (xpiURL == null)
+            break;
+  
+          if (skipSourceURIs.indexOf(xpiURL) != -1)
+            return null;
+  
+          result.xpiURL = xpiURL;
+          addon.sourceURI = NetUtil.newURI(xpiURL);
+  
+          let size = parseInt(node.getAttribute("size"));
+          addon.size = (size >= 0) ? size : null;
+  
+          let xpiHash = node.getAttribute("hash");
+          if (xpiHash != null)
+            xpiHash = xpiHash.trim();
+          result.xpiHash = xpiHash ? xpiHash : null;
+          break;
+      }
+    }
+  
+    return result;
+  },
+
+  _parseLocales: function _parseLocales(aElements, aTotalResults, aSkip, aCallback) {
+    let self = this;
+    let results = [];
+    for (let i = 0; i < aElements.length; i++) {
+      let element = aElements[i];
+
+      // Ignore add-ons not compatible with this Application
+      let tags = this._getUniqueDescendant(element, "compatible_applications");
+      if (tags == null)
+        continue;
+
+      let applications = tags.getElementsByTagName("appID");
+      let compatible = Array.some(applications, function(aAppNode) {
+        if (self._getTextContent(aAppNode) != Services.appinfo.ID)
+          return false;
+
+        let parent = aAppNode.parentNode;
+        let minVersion = self._getDescendantTextContent(parent, "min_version");
+        let maxVersion = self._getDescendantTextContent(parent, "max_version");
+        if (minVersion == null || maxVersion == null)
+          return false;
+
+        let currentVersion = Services.appinfo.version;
+        return (Services.vc.compare(minVersion, currentVersion) <= 0 && Services.vc.compare(currentVersion, maxVersion) <= 0);
+      });
+
+      if (!compatible)
+        continue;
+
+      // Add-on meets all requirements, so parse out data
+      let result = this._parseLocale(element, aSkip);
+      if (result == null)
+        continue;
+
+      // Ignore add-on missing a required attribute
+      let requiredAttributes = ["id", "name", "version", "type", "targetLocale"];
+      if (requiredAttributes.some(function(aAttribute) !result.addon[aAttribute]))
+        continue;
+
+      // Add only if the add-on is compatible with the platform
+      if (!result.addon.isPlatformCompatible)
+        continue;
+
+      // Add only if there was an xpi compatible with this OS
+      if (!result.xpiURL)
+        continue;
+
+      results.push(result);
+
+      // Ignore this add-on from now on by adding it to the skip array
+      aSkip.ids.push(result.addon.id);
+    }
+
+    // Immediately report success if no AddonInstall instances to create
+    let pendingResults = results.length;
+    if (pendingResults == 0) {
+      aCallback([]);
+      return;
+    }
+
+    // Create an AddonInstall for each result
+    let self = this;
+    results.forEach(function(aResult) {
+      let addon = aResult.addon;
+      let callback = function(aInstall) {
+        aResult.addon.install = aInstall;
+        pendingResults--;
+        if (pendingResults == 0)
+          aCallback(results);
+      }
+
+      if (aResult.xpiURL) {
+        AddonManager.getInstallForURL(aResult.xpiURL, callback,
+                                      "application/x-xpinstall", aResult.xpiHash,
+                                      addon.name, addon.iconURL, addon.version);
+      } else {
+        callback(null);
+      }
+    });
+  }
+};
+
+function LocaleSearchResult(aId) {
+  this.id = aId;
+}
+
+LocaleSearchResult.prototype = {
+  id: null,
+  type: null,
+  targetLocale: null,
+  name: null,
+  addon: null,
+  version: null,
+  iconURL: null,
+  install: null,
+  sourceURI: null,
+  repositoryStatus: null,
+  size: null,
+  updateDate: null,
+  isCompatible: true,
+  isPlatformCompatible: true,
+  providesUpdatesSecurely: true,
+  blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+  appDisabled: false,
+  userDisabled: false,
+  scope: AddonManager.SCOPE_PROFILE,
+  isActive: true,
+  pendingOperations: AddonManager.PENDING_NONE,
+  permissions: 0
+};
--- a/mobile/modules/Makefile.in
+++ b/mobile/modules/Makefile.in
@@ -38,16 +38,17 @@
 DEPTH      = ../..
 topsrcdir  = @top_srcdir@
 srcdir     = @srcdir@
 VPATH      = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
+  LocaleRepository.jsm \
   linuxTypes.jsm \
   video.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = \
   contacts.jsm \
   $(NULL)
 
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -1448,20 +1448,16 @@ setting {
   font-size: @font_xlarge@ !important;
   padding: 0.2em 0.4em;
   -moz-padding-end: 0.2em;
   letter-spacing: 0.2em;
   text-align: center;
   min-width: 5.5em;
 }
 
-.syncsetup-link {
-  text-decoration: underline;
-}
-
 .syncsetup-label {
   color: #fff;
 }
 
 #syncsetup-customserver {
   -moz-margin-start: @margin_xnormal@;
 }
 
--- a/mobile/themes/core/gingerbread/browser.css
+++ b/mobile/themes/core/gingerbread/browser.css
@@ -1425,20 +1425,16 @@ setting {
   font-size: @font_xlarge@ !important;
   padding: 0.2em 0.4em;
   -moz-padding-end: 0.2em;
   letter-spacing: 0.2em;
   text-align: center;
   min-width: 5.5em;
 }
 
-.syncsetup-link {
-  text-decoration: underline;
-}
-
 .syncsetup-label {
   color: @color_text_default@;
 }
 
 #syncsetup-customserver {
   -moz-margin-start: @margin_xnormal@;
 }
 
--- a/mobile/themes/core/gingerbread/defines.inc
+++ b/mobile/themes/core/gingerbread/defines.inc
@@ -11,16 +11,17 @@
 %define color_background_header #292929
 %define color_text_header #999999
 %define color_background_scroller #9a9a9a
 %define color_background_inverse #fff
 %define color_text_inverse #000
 %define color_text_button #000
 %define color_text_disabled #808080
 %define color_text_placeholder #808080
+%define color_text_as_link #febc2b
 
 %define color_background_highlight #febc2b
 %define color_background_highlight_overlay rgba(254, 188, 43, 0.8)
 %define color_text_highlight #000
 
 %define color_subtext_default lightgray
 %define color_subtext_inverse #414141
 
new file mode 100644
--- /dev/null
+++ b/mobile/themes/core/gingerbread/localePicker.css
@@ -0,0 +1,90 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Wes Johnston <wjohnston@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+%filter substitution
+%include defines.inc
+
+.pane {
+  -moz-box-pack: center;
+  -moz-box-align: center;
+  -moz-box-flex: 1;
+}
+
+#main-page {
+  background-image: url("chrome://branding/content/logo.png");
+  background-repeat: no-repeat;
+  background-position: center center;
+}
+
+#picker-title {
+  font-weight: bold;
+  font-size: @font_normal@;
+}
+
+.link {
+  padding: @padding_xlarge@ 0px;
+  font-weight: bold;
+}
+
+richlistbox {
+  padding: 0px;
+  margin: 0px;
+  background-color: transparent;
+}
+
+#installer-page {
+  background-color: black;
+  color: white;
+}
+
+richlistitem {
+  height: @touch_row@;
+  font-size: @font_normal@;
+  border-bottom: @border_width_tiny@ solid gray;
+  padding: 0px @padding_normal@;
+  -moz-box-align: center;
+}
+
+richlistitem .checkbox {
+  width: 46px;
+  height: 46px;
+  list-style-image: url("chrome://browser/skin/images/radio-unselected-hdpi.png");
+}
+
+richlistitem[selected] .checkbox {
+  list-style-image: url("chrome://browser/skin/images/radio-selected-hdpi.png");
+}
--- a/mobile/themes/core/gingerbread/platform.css
+++ b/mobile/themes/core/gingerbread/platform.css
@@ -40,16 +40,18 @@
 
 %filter substitution
 %include defines.inc
 
 /* general stuff ------------------------------------------------------------ */
 :root {
   font-family: "Nokia Sans", Tahoma, sans-serif !important;
   font-size: @font_normal@ !important;
+  background-color: @color_background_default@; /* force */
+  color: @color_text_default@; /* force */
 }
 
 ::-moz-selection {
   background-color: @color_background_highlight@;
   color: @color_text_highlight@;
 }
 
 menu,
@@ -91,16 +93,21 @@ textbox.search-bar {
   background: url("chrome://browser/skin/images/textbox-bg.png") top left repeat-x;
   background-size: 100% 100%; 
 }
 
 textbox[disabled="true"] {
   background-color: lightgray;
 }
 
+.link {
+  color: @color_text_as_link@;
+  text-decoration: underline;
+}
+
 /* sidebars spacer --------------------------------------------------------- */
 .sidebar-spacer {
   background-color: #767973;
 }
 
 /* prompt dialogs ---------------------------------------------------------- */
 .context-block,
 .modal-block,
--- a/mobile/themes/core/jar.mn
+++ b/mobile/themes/core/jar.mn
@@ -13,16 +13,17 @@ chrome.jar:
 * skin/content.css                          (content.css)
   skin/config.css                           (config.css)
   skin/firstRun.css                         (firstRun.css)
 * skin/forms.css                            (forms.css)
   skin/header.css                           (header.css)
 * skin/notification.css                     (notification.css)
 * skin/platform.css                         (platform.css)
   skin/touchcontrols.css                    (touchcontrols.css)
+* skin/localePicker.css                     (localePicker.css)
 % override chrome://global/skin/about.css chrome://browser/skin/about.css
 % override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
 
   skin/images/appmenu-addons-hdpi.png       (images/appmenu-addons-hdpi.png)
   skin/images/appmenu-active-hdpi.png       (images/appmenu-active-hdpi.png)
   skin/images/appmenu-downloads-hdpi.png    (images/appmenu-downloads-hdpi.png)
   skin/images/appmenu-findinpage-hdpi.png   (images/appmenu-findinpage-hdpi.png)
   skin/images/appmenu-more-hdpi.png         (images/appmenu-more-hdpi.png)
@@ -133,16 +134,17 @@ chrome.jar:
 * skin/gingerbread/content.css                          (gingerbread/content.css)
   skin/gingerbread/config.css                           (config.css)
   skin/gingerbread/firstRun.css                         (firstRun.css)
 * skin/gingerbread/forms.css                            (gingerbread/forms.css)
   skin/gingerbread/header.css                           (header.css)
 * skin/gingerbread/notification.css                     (notification.css)
 * skin/gingerbread/platform.css                         (gingerbread/platform.css)
   skin/gingerbread/touchcontrols.css                    (touchcontrols.css)
+* skin/gingerbread/localePicker.css                     (gingerbread/localePicker.css)
 % override chrome://global/skin/about.css chrome://browser/skin/about.css
 % override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
 
   skin/gingerbread/images/appmenu-addons-hdpi.png       (gingerbread/images/appmenu-addons-hdpi.png)
   skin/gingerbread/images/appmenu-active-hdpi.png       (gingerbread/images/appmenu-active-hdpi.png)
   skin/gingerbread/images/appmenu-downloads-hdpi.png    (gingerbread/images/appmenu-downloads-hdpi.png)
   skin/gingerbread/images/appmenu-findinpage-hdpi.png   (gingerbread/images/appmenu-findinpage-hdpi.png)
   skin/gingerbread/images/appmenu-more-hdpi.png         (gingerbread/images/appmenu-more-hdpi.png)
@@ -250,16 +252,17 @@ chrome.jar:
 * skin/honeycomb/content.css                          (content.css)
   skin/honeycomb/config.css                           (config.css)
   skin/honeycomb/firstRun.css                         (firstRun.css)
 * skin/honeycomb/forms.css                            (forms.css)
   skin/honeycomb/header.css                           (header.css)
 * skin/honeycomb/notification.css                     (notification.css)
 * skin/honeycomb/platform.css                         (platform.css)
   skin/honeycomb/touchcontrols.css                    (touchcontrols.css)
+* skin/honeycomb/localePicker.css                     (localePicker.css)
 % override chrome://global/skin/about.css chrome://browser/skin/about.css
 % override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
 
   skin/honeycomb/images/appmenu-addons-hdpi.png       (honeycomb/images/appmenu-addons-hdpi.png)
   skin/honeycomb/images/appmenu-active-hdpi.png       (honeycomb/images/appmenu-active-hdpi.png)
   skin/honeycomb/images/appmenu-downloads-hdpi.png    (honeycomb/images/appmenu-downloads-hdpi.png)
   skin/honeycomb/images/appmenu-findinpage-hdpi.png   (honeycomb/images/appmenu-findinpage-hdpi.png)
   skin/honeycomb/images/appmenu-more-hdpi.png         (honeycomb/images/appmenu-more-hdpi.png)
new file mode 100644
--- /dev/null
+++ b/mobile/themes/core/localePicker.css
@@ -0,0 +1,90 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Wes Johnston <wjohnston@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+%filter substitution
+%include defines.inc
+
+.pane {
+  -moz-box-pack: center;
+  -moz-box-align: center;
+  -moz-box-flex: 1;
+}
+
+#main-page {
+  background-image: url("chrome://branding/content/logo.png");
+  background-repeat: no-repeat;
+  background-position: center center;
+}
+
+#picker-title {
+  font-weight: bold;
+  font-size: @font_normal@;
+}
+
+.link {
+  padding: @padding_xlarge@ 0px;
+  font-weight: bold;
+}
+
+richlistbox {
+  padding: 0px;
+  margin: 0px;
+  background-color: transparent;
+}
+
+#installer-page {
+  background-color: black;
+  color: white;
+}
+
+richlistitem {
+  height: @touch_row@;
+  font-size: @font_normal@;
+  border-bottom: @border_width_tiny@ solid gray;
+  padding: 0px @padding_normal@;
+  -moz-box-align: center;
+}
+
+richlistitem .checkbox {
+  width: 30px;
+  height: 30px;
+  list-style-image: url("chrome://browser/skin/images/check-unselected-30.png");
+}
+
+richlistitem[selected] .checkbox {
+  list-style-image: url("chrome://browser/skin/images/check-selected-30.png");
+}
--- a/mobile/themes/core/platform.css
+++ b/mobile/themes/core/platform.css
@@ -40,16 +40,18 @@
 
 %filter substitution
 %include defines.inc
 
 /* general stuff ------------------------------------------------------------ */
 :root {
   font-family: "Nokia Sans", Tahoma, sans-serif !important;
   font-size: @font_normal@ !important;
+  background-color: white; /* force */
+  color: black; /* force */
 }
 
 ::-moz-selection {
   background-color: #8db8d8;
   color: black;
 }
 
 menu,
@@ -93,16 +95,21 @@ textbox.search-bar {
   background: url("chrome://browser/skin/images/textbox-bg.png") top left repeat-x;
   background-size: 100% 100%; 
 }
 
 textbox[disabled="true"] {
   background-color: lightgray;
 }
 
+.link {
+  color: blue;
+  text-decoration: underline;
+}
+
 /* sidebars spacer --------------------------------------------------------- */
 .sidebar-spacer {
   background-color: #767973;
 }
 
 /* prompt dialogs ---------------------------------------------------------- */
 .context-block,
 .modal-block,
@@ -576,16 +583,20 @@ dialog {
 
 .prompt-message {
   text-align: center;
   -moz-box-pack: center;
   font-size: @font_snormal@;
   margin: @padding_normal@;
 }
 
+.prompt-message .link {
+  color: white;
+}
+
 .prompt-title {
   text-align: center;
   font-size: @font_xnormal@;
   -moz-box-align: center;
   -moz-box-pack: center;
   padding-top: @padding_xnormal@;
 }