diff --git a/chrome/browser/extensions/extension_file_util.cc b/chrome/browser/extensions/extension_file_util.cc
index 9c15da362ac94038936ca113523ba5f48085aec9..1692c6726034a36c48cb3cbcf8d0240ce7437771 100644
--- a/chrome/browser/extensions/extension_file_util.cc
+++ b/chrome/browser/extensions/extension_file_util.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/extensions/extension_file_util.h"
 
+#include "app/l10n_util.h"
 #include "base/file_util.h"
 #include "base/logging.h"
 #include "base/scoped_temp_dir.h"
@@ -94,7 +95,8 @@ bool InstallExtension(const FilePath& src_dir,
   return true;
 }
 
-Extension* LoadExtension(const FilePath& extension_path, bool require_key,
+Extension* LoadExtension(const FilePath& extension_path,
+                         bool require_key,
                          std::string* error) {
   FilePath manifest_path =
       extension_path.AppendASCII(Extension::kManifestFilename);
@@ -113,9 +115,16 @@ Extension* LoadExtension(const FilePath& extension_path, bool require_key,
     return NULL;
   }
 
+  DictionaryValue* manifest = static_cast<DictionaryValue*>(root.get());
+  ExtensionMessageBundle* message_bundle =
+    LoadLocaleInfo(extension_path, *manifest, error);
+  if (!message_bundle && !error->empty())
+    return NULL;
+
   scoped_ptr<Extension> extension(new Extension(extension_path));
-  if (!extension->InitFromValue(*static_cast<DictionaryValue*>(root.get()),
-                                require_key, error))
+  // Assign message bundle to extension.
+  extension->set_message_bundle(message_bundle);
+  if (!extension->InitFromValue(*manifest, require_key, error))
     return NULL;
 
   if (!ValidateExtension(extension.get(), error))
@@ -218,22 +227,6 @@ bool ValidateExtension(Extension* extension, std::string* error) {
     }
   }
 
-  // Load locale information if available.
-  FilePath locale_path =
-      extension->path().AppendASCII(Extension::kLocaleFolder);
-  if (file_util::PathExists(locale_path)) {
-    if (!extension_l10n_util::AddValidLocales(locale_path,
-                                              extension,
-                                              error)) {
-      return false;
-    }
-
-    if (!extension_l10n_util::ValidateDefaultLocale(extension)) {
-      *error = extension_manifest_errors::kLocalesNoDefaultLocaleSpecified;
-      return false;
-    }
-  }
-
   // Check children of extension root to see if any of them start with _ and is
   // not on the reserved list.
   if (!CheckForIllegalFilenames(extension->path(), error)) {
@@ -311,6 +304,40 @@ void GarbageCollectExtensions(const FilePath& install_directory,
   }
 }
 
+ExtensionMessageBundle* LoadLocaleInfo(const FilePath& extension_path,
+                                       const DictionaryValue& manifest,
+                                       std::string* error) {
+  error->clear();
+  // Load locale information if available.
+  FilePath locale_path = extension_path.AppendASCII(Extension::kLocaleFolder);
+  if (!file_util::PathExists(locale_path))
+    return NULL;
+
+  std::set<std::string> locales;
+  if (!extension_l10n_util::GetValidLocales(locale_path, &locales, error))
+    return NULL;
+
+  std::string default_locale =
+    extension_l10n_util::GetDefaultLocaleFromManifest(manifest, error);
+  if (default_locale.empty() ||
+      locales.find(default_locale) == locales.end()) {
+    *error = extension_manifest_errors::kLocalesNoDefaultLocaleSpecified;
+    return NULL;
+  }
+
+  // We can't call g_browser_process->GetApplicationLocale() since we are not
+  // on the main thread.
+  static std::string app_locale = l10n_util::GetApplicationLocale(L"");
+  if (locales.find(app_locale) == locales.end())
+    app_locale = "";
+  ExtensionMessageBundle* message_bundle =
+    extension_l10n_util::LoadMessageCatalogs(locale_path,
+                                             default_locale,
+                                             app_locale,
+                                             error);
+  return message_bundle;
+}
+
 bool CheckForIllegalFilenames(const FilePath& extension_path,
                               std::string* error) {
   // Reserved underscore names.
diff --git a/chrome/browser/extensions/extension_file_util.h b/chrome/browser/extensions/extension_file_util.h
index 9c13ea21656fa92813de4b357cecc270a79436be..b6a737f0cc910f37de96300a7761e6ada615dd71 100644
--- a/chrome/browser/extensions/extension_file_util.h
+++ b/chrome/browser/extensions/extension_file_util.h
@@ -11,6 +11,8 @@
 #include "base/file_path.h"
 #include "chrome/common/extensions/extension.h"
 
+class ExtensionMessageBundle;
+
 // Utilties for manipulating the on-disk storage of extensions.
 namespace extension_file_util {
 
@@ -28,7 +30,8 @@ extern const char kCurrentVersionFileName[];
 bool MoveDirSafely(const FilePath& source_dir, const FilePath& dest_dir);
 
 // Updates the Current Version file inside the installed extension.
-bool SetCurrentVersion(const FilePath& dest_dir, const std::string& version,
+bool SetCurrentVersion(const FilePath& dest_dir,
+                       const std::string& version,
                        std::string* error);
 
 // Reads the Current Version file.
@@ -66,7 +69,8 @@ bool InstallExtension(const FilePath& src_dir,
 
 // Loads and validates an extension from the specified directory. Returns NULL
 // on failure, with a description of the error in |error|.
-Extension* LoadExtension(const FilePath& extension_root, bool require_key,
+Extension* LoadExtension(const FilePath& extension_root,
+                         bool require_key,
                          std::string* error);
 
 // Returns true if the given extension object is valid and consistent.
@@ -82,6 +86,14 @@ void UninstallExtension(const std::string& id, const FilePath& extensions_dir);
 void GarbageCollectExtensions(const FilePath& extensions_dir,
                               const std::set<std::string>& installed_ids);
 
+// Loads locale information if _locales folder is present.
+// Returns message bundle if there were no errors. If _locales folder is not
+// present it returns NULL with empty error string.
+// Loading failed only if function returns NULL and error is not empty.
+ExtensionMessageBundle* LoadLocaleInfo(const FilePath& extension_path,
+                                       const DictionaryValue& manifest,
+                                       std::string* error);
+
 // We need to reserve the namespace of entries that start with "_" for future
 // use by Chrome.
 // If any files or directories are found using "_" prefix and are not on
diff --git a/chrome/browser/extensions/extension_file_util_unittest.cc b/chrome/browser/extensions/extension_file_util_unittest.cc
index 9f9960638a724f382e134e4200936d694423a02c..7230ecd738a6cfc1387de873fbc196e1e4480f41 100644
--- a/chrome/browser/extensions/extension_file_util_unittest.cc
+++ b/chrome/browser/extensions/extension_file_util_unittest.cc
@@ -10,7 +10,6 @@
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/extensions/extension.h"
 #include "chrome/common/extensions/extension_constants.h"
-#include "chrome/common/json_value_serializer.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace keys = extension_manifest_keys;
@@ -111,66 +110,37 @@ TEST(ExtensionFileUtil, CompareToInstalledVersion) {
                 temp.path(), kBadId, kMissingVersion, "1.0.0", &version_dir));
 }
 
-// Creates minimal manifest, with or without default_locale section.
-bool CreateMinimalManifest(const std::string& locale,
-                           const FilePath& manifest_path) {
-  DictionaryValue manifest;
-
-  manifest.SetString(keys::kVersion, "1.0.0.0");
-  manifest.SetString(keys::kName, "my extension");
-  if (!locale.empty()) {
-    manifest.SetString(keys::kDefaultLocale, locale);
-  }
-
-  JSONFileValueSerializer serializer(manifest_path);
-  return serializer.Serialize(manifest);
-}
-
 TEST(ExtensionFileUtil, LoadExtensionWithValidLocales) {
-  ScopedTempDir temp;
-  ASSERT_TRUE(temp.CreateUniqueTempDir());
-  ASSERT_TRUE(CreateMinimalManifest(
-      "en_US", temp.path().AppendASCII(Extension::kManifestFilename)));
-
-  FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
-  ASSERT_TRUE(file_util::CreateDirectory(src_path));
-
-  FilePath locale_1 = src_path.AppendASCII("sr");
-  ASSERT_TRUE(file_util::CreateDirectory(locale_1));
-
-  std::string data = "foobar";
-  ASSERT_TRUE(
-      file_util::WriteFile(locale_1.AppendASCII(Extension::kMessagesFilename),
-                           data.c_str(), data.length()));
-
-  FilePath locale_2 = src_path.AppendASCII("en_US");
-  ASSERT_TRUE(file_util::CreateDirectory(locale_2));
-
-  ASSERT_TRUE(
-      file_util::WriteFile(locale_2.AppendASCII(Extension::kMessagesFilename),
-                           data.c_str(), data.length()));
+  FilePath install_dir;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
+  install_dir = install_dir.AppendASCII("extensions")
+      .AppendASCII("good")
+      .AppendASCII("Extensions")
+      .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
+      .AppendASCII("1.0.0.0");
 
   std::string error;
   scoped_ptr<Extension> extension(
-      extension_file_util::LoadExtension(temp.path(), false, &error));
-  ASSERT_FALSE(extension == NULL);
-  EXPECT_EQ(static_cast<unsigned int>(2),
-            extension->supported_locales().size());
-  EXPECT_EQ("en-US", extension->default_locale());
+      extension_file_util::LoadExtension(install_dir, false, &error));
+  ASSERT_TRUE(extension != NULL);
+  EXPECT_EQ("The first extension that I made.", extension->description());
 }
 
 TEST(ExtensionFileUtil, LoadExtensionWithoutLocalesFolder) {
-  ScopedTempDir temp;
-  ASSERT_TRUE(temp.CreateUniqueTempDir());
-  ASSERT_TRUE(CreateMinimalManifest(
-      "", temp.path().AppendASCII(Extension::kManifestFilename)));
+  FilePath install_dir;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
+  install_dir = install_dir.AppendASCII("extensions")
+      .AppendASCII("good")
+      .AppendASCII("Extensions")
+      .AppendASCII("bjafgdebaacbbbecmhlhpofkepfkgcpa")
+      .AppendASCII("1.0");
 
   std::string error;
   scoped_ptr<Extension> extension(
-      extension_file_util::LoadExtension(temp.path(), false, &error));
+      extension_file_util::LoadExtension(install_dir, false, &error));
   ASSERT_FALSE(extension == NULL);
-  EXPECT_TRUE(extension->supported_locales().empty());
-  EXPECT_TRUE(extension->default_locale().empty());
+  EXPECT_TRUE(NULL == extension->message_bundle());
+  EXPECT_TRUE(error.empty());
 }
 
 TEST(ExtensionFileUtil, CheckIllegalFilenamesNoUnderscores) {
@@ -180,7 +150,7 @@ TEST(ExtensionFileUtil, CheckIllegalFilenamesNoUnderscores) {
   FilePath src_path = temp.path().AppendASCII("some_dir");
   ASSERT_TRUE(file_util::CreateDirectory(src_path));
 
-  std::string data = "foobar";
+  std::string data = "{ \"name\": { \"message\": \"foobar\" } }";
   ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("some_file.txt"),
                                    data.c_str(), data.length()));
   std::string error;
diff --git a/chrome/browser/extensions/extension_l10n_util.cc b/chrome/browser/extensions/extension_l10n_util.cc
index 5176b6018670e0ea8c5072a6bc3bbaadcf5f4f24..d13a97c1abb6fe9ef9c86c9453f75b3c2b10cf6f 100644
--- a/chrome/browser/extensions/extension_l10n_util.cc
+++ b/chrome/browser/extensions/extension_l10n_util.cc
@@ -12,27 +12,31 @@
 #include "base/file_util.h"
 #include "base/string_util.h"
 #include "base/values.h"
+#include "chrome/browser/extensions/extension_message_bundle.h"
 #include "chrome/common/extensions/extension.h"
 #include "chrome/common/extensions/extension_constants.h"
+#include "chrome/common/json_value_serializer.h"
 
+namespace errors = extension_manifest_errors;
 namespace keys = extension_manifest_keys;
 
 namespace extension_l10n_util {
 
-bool ValidateDefaultLocale(const Extension* extension) {
-  std::string default_locale = extension->default_locale();
-
-  if (extension->supported_locales().find(default_locale) !=
-        extension->supported_locales().end()) {
-    return true;
-  } else {
-    return false;
+std::string GetDefaultLocaleFromManifest(const DictionaryValue& manifest,
+                                         std::string* error) {
+  std::string default_locale;
+  if (!manifest.GetString(keys::kDefaultLocale, &default_locale)) {
+    *error = errors::kInvalidDefaultLocale;
+    return "";
   }
+  // Normalize underscores to hyphens.
+  std::replace(default_locale.begin(), default_locale.end(), '_', '-');
+  return default_locale;
 }
 
 bool AddLocale(const std::set<std::string>& chrome_locales,
                const FilePath& locale_folder,
-               Extension* extension,
+               std::set<std::string>* valid_locales,
                std::string* locale_name,
                std::string* error) {
   // Normalize underscores to hyphens because that's what our locale files use.
@@ -50,7 +54,7 @@ bool AddLocale(const std::set<std::string>& chrome_locales,
   // Check if messages file is actually present (but don't check content).
   if (file_util::PathExists(
          locale_folder.AppendASCII(Extension::kMessagesFilename))) {
-    extension->AddSupportedLocale(*locale_name);
+    valid_locales->insert(*locale_name);
   } else {
     *error = StringPrintf("Catalog file is missing for locale %s.",
                           locale_name->c_str());
@@ -60,8 +64,8 @@ bool AddLocale(const std::set<std::string>& chrome_locales,
   return true;
 }
 
-bool AddValidLocales(const FilePath& locale_path,
-                     Extension* extension,
+bool GetValidLocales(const FilePath& locale_path,
+                     std::set<std::string>* valid_locales,
                      std::string* error) {
   // Get available chrome locales as a set.
   const std::vector<std::string>& available_locales =
@@ -76,13 +80,16 @@ bool AddValidLocales(const FilePath& locale_path,
   while (!(locale_folder = locales.Next()).empty()) {
     std::string locale_name =
         WideToASCII(locale_folder.BaseName().ToWStringHack());
-    if (!AddLocale(chrome_locales, locale_folder,
-                   extension, &locale_name, error)) {
+    if (!AddLocale(chrome_locales,
+                   locale_folder,
+                   valid_locales,
+                   &locale_name,
+                   error)) {
       return false;
     }
   }
 
-  if (extension->supported_locales().empty()) {
+  if (valid_locales->empty()) {
     *error = extension_manifest_errors::kLocalesNoValidLocaleNamesListed;
     return false;
   }
@@ -90,4 +97,51 @@ bool AddValidLocales(const FilePath& locale_path,
   return true;
 }
 
+// Loads contents of the messages file for given locale. If file is not found,
+// or there was parsing error we return NULL and set |error|.
+// Caller owns the returned object.
+static DictionaryValue* LoadMessageFile(const FilePath& locale_path,
+                                        const std::string& locale,
+                                        std::string* error) {
+  std::string extension_locale = locale;
+  // Normalize hyphens to underscores because that's what our locale files use.
+  std::replace(extension_locale.begin(), extension_locale.end(), '-', '_');
+  FilePath file = locale_path.AppendASCII(extension_locale)
+      .AppendASCII(Extension::kMessagesFilename);
+  JSONFileValueSerializer messages_serializer(file);
+  Value *dictionary = messages_serializer.Deserialize(error);
+  if (!dictionary && error->empty()) {
+    // JSONFileValueSerializer just returns NULL if file cannot be found. It
+    // doesn't set the error, so we have to do it.
+    *error = StringPrintf("Catalog file is missing for locale %s.",
+                          extension_locale.c_str());
+  }
+
+  return static_cast<DictionaryValue*>(dictionary);
+}
+
+ExtensionMessageBundle* LoadMessageCatalogs(
+    const FilePath& locale_path,
+    const std::string& default_locale,
+    const std::string& application_locale,
+    std::string* error) {
+  scoped_ptr<DictionaryValue> default_catalog(
+      LoadMessageFile(locale_path, default_locale, error));
+  if (!default_catalog.get()) {
+    return false;
+  }
+
+  scoped_ptr<DictionaryValue> app_catalog(
+      LoadMessageFile(locale_path, application_locale, error));
+  if (!app_catalog.get()) {
+    // Only default catalog has to be present. This is not an error.
+    app_catalog.reset(new DictionaryValue);
+    error->clear();
+  }
+
+  return ExtensionMessageBundle::Create(*default_catalog,
+                                        *app_catalog,
+                                        error);
+}
+
 }  // namespace extension_l10n_util
diff --git a/chrome/browser/extensions/extension_l10n_util.h b/chrome/browser/extensions/extension_l10n_util.h
index be73f9958e17048cb3e74b4289cb435f17cc6b6e..98759ab5c3b520d4709db71bbe574ef8a9baf845 100644
--- a/chrome/browser/extensions/extension_l10n_util.h
+++ b/chrome/browser/extensions/extension_l10n_util.h
@@ -12,13 +12,15 @@
 
 class DictionaryValue;
 class Extension;
+class ExtensionMessageBundle;
 class FilePath;
 
 namespace extension_l10n_util {
 
-// Returns true if default_locale was set to valid locale
-// (supported by the extension).
-bool ValidateDefaultLocale(const Extension* extension);
+// Returns default locale in form "en-US" or "sr" or empty string if
+// "default_locale" section was not defined in the manifest.json file.
+std::string GetDefaultLocaleFromManifest(const DictionaryValue& manifest,
+                                         std::string* error);
 
 // Adds locale_name to the extension if it's in chrome_locales, and
 // if messages file is present (we don't check content of messages file here).
@@ -27,7 +29,7 @@ bool ValidateDefaultLocale(const Extension* extension);
 // If file name starts with . return true (helps testing extensions under svn).
 bool AddLocale(const std::set<std::string>& chrome_locales,
                const FilePath& locale_folder,
-               Extension* extension,
+               std::set<std::string>* valid_locales,
                std::string* locale_name,
                std::string* error);
 
@@ -38,10 +40,21 @@ bool AddLocale(const std::set<std::string>& chrome_locales,
 // 4. Intersect both lists, and add intersection to the extension.
 // Returns false if any of supplied locales don't match chrome list of locales.
 // Fills out error with offending locale name.
-bool AddValidLocales(const FilePath& locale_path,
-                     Extension* extension,
+bool GetValidLocales(const FilePath& locale_path,
+                     std::set<std::string>* locales,
                      std::string* error);
 
+// Loads messages file for default locale, and application locale (application
+// locale doesn't have to exist).
+// It creates simplified in-memory representation of name-value pairs, where
+// value part is actual message with placeholders resolved.
+// Returns message bundle if it can load default locale messages file, and all
+// messages are valid, else returns NULL and sets error.
+ExtensionMessageBundle* LoadMessageCatalogs(const FilePath& locale_path,
+                                            const std::string& default_locale,
+                                            const std::string& app_locale,
+                                            std::string* error);
+
 }  // namespace extension_l10n_util
 
 #endif  // CHROME_BROWSER_EXTENSIONS_EXTENSION_L10N_UTIL_H_
diff --git a/chrome/browser/extensions/extension_l10n_util_unittest.cc b/chrome/browser/extensions/extension_l10n_util_unittest.cc
index b3dc3970730ded3bfe2ecaecdd3cd2942e13f9f6..f674f69bcace46f1b24b6d749cbe5ef9d079241f 100644
--- a/chrome/browser/extensions/extension_l10n_util_unittest.cc
+++ b/chrome/browser/extensions/extension_l10n_util_unittest.cc
@@ -15,146 +15,132 @@
 #include "chrome/common/extensions/extension_constants.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
-namespace keys = extension_manifest_keys;
-
 namespace {
 
-TEST(ExtensionL10nUtil, LoadGoodExtensionFromSVNTree) {
-  FilePath install_dir;
-  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
-  install_dir = install_dir.AppendASCII("extensions")
-      .AppendASCII("good")
-      .AppendASCII("Extensions")
-      .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
-      .AppendASCII("1.0.0.0");
+TEST(Extensio8nL10nUtil, GetValidLocalesEmptyLocaleFolder) {
+  ScopedTempDir temp;
+  ASSERT_TRUE(temp.CreateUniqueTempDir());
 
-  FilePath locale_path = install_dir.AppendASCII(Extension::kLocaleFolder);
-  ASSERT_TRUE(file_util::PathExists(locale_path));
+  FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
+  ASSERT_TRUE(file_util::CreateDirectory(src_path));
 
-  scoped_ptr<Extension> extension(new Extension(install_dir));
   std::string error;
-  EXPECT_TRUE(extension_l10n_util::AddValidLocales(
-      locale_path, extension.get(), &error));
-  const std::set<std::string>& supported_locales =
-    extension->supported_locales();
-  EXPECT_EQ(2U, supported_locales.size());
-  EXPECT_TRUE(supported_locales.find("en-US") != supported_locales.end());
-  EXPECT_TRUE(supported_locales.find("sr") != supported_locales.end());
+  std::set<std::string> locales;
+  EXPECT_FALSE(extension_l10n_util::GetValidLocales(src_path,
+                                                    &locales,
+                                                    &error));
+
+  EXPECT_TRUE(locales.empty());
 }
 
-Extension* CreateMinimalExtension(const std::string& default_locale) {
-#if defined(OS_WIN)
-  FilePath path(FILE_PATH_LITERAL("C:\\foo"));
-#elif defined(OS_POSIX)
-  FilePath path(FILE_PATH_LITERAL("/foo"));
-#endif
-  Extension* extension = new Extension(path);
+TEST(ExtensionL10nUtil, GetValidLocalesWithValidLocaleNoMessagesFile) {
+  ScopedTempDir temp;
+  ASSERT_TRUE(temp.CreateUniqueTempDir());
+
+  FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
+  ASSERT_TRUE(file_util::CreateDirectory(src_path));
+  ASSERT_TRUE(file_util::CreateDirectory(src_path.AppendASCII("sr")));
+
   std::string error;
-  DictionaryValue input_value;
+  std::set<std::string> locales;
+  EXPECT_FALSE(extension_l10n_util::GetValidLocales(src_path,
+                                                    &locales,
+                                                    &error));
 
-  // Test minimal extension
-  input_value.SetString(keys::kVersion, "1.0.0.0");
-  input_value.SetString(keys::kName, "my extension");
-  if (!default_locale.empty()) {
-    input_value.SetString(keys::kDefaultLocale, default_locale);
-  }
-  EXPECT_TRUE(extension->InitFromValue(input_value, false, &error));
+  EXPECT_TRUE(locales.empty());
+}
 
-  return extension;
+TEST(ExtensionL10nUtil, GetValidLocalesWithValidLocalesAndMessagesFile) {
+  FilePath install_dir;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
+  install_dir = install_dir.AppendASCII("extensions")
+      .AppendASCII("good")
+      .AppendASCII("Extensions")
+      .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
+      .AppendASCII("1.0.0.0")
+      .AppendASCII(Extension::kLocaleFolder);
+
+  std::string error;
+  std::set<std::string> locales;
+  EXPECT_TRUE(extension_l10n_util::GetValidLocales(install_dir,
+                                                   &locales,
+                                                   &error));
+  EXPECT_EQ(2U, locales.size());
+  EXPECT_TRUE(locales.find("sr") != locales.end());
 }
 
-TEST(ExtensionL10nUtil, AddValidLocalesEmptyLocaleFolder) {
+TEST(ExtensionL10nUtil, LoadMessageCatalogsMissingFiles) {
   ScopedTempDir temp;
   ASSERT_TRUE(temp.CreateUniqueTempDir());
 
   FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
   ASSERT_TRUE(file_util::CreateDirectory(src_path));
 
-  scoped_ptr<Extension> extension(CreateMinimalExtension(""));
-
   std::string error;
-  EXPECT_FALSE(extension_l10n_util::AddValidLocales(src_path,
-                                                    extension.get(),
-                                                    &error));
-
-  EXPECT_TRUE(extension->supported_locales().empty());
+  EXPECT_TRUE(NULL == extension_l10n_util::LoadMessageCatalogs(src_path,
+                                                               "en-US",
+                                                               "sr",
+                                                               &error));
+  EXPECT_FALSE(error.empty());
 }
 
-TEST(ExtensionL10nUtil, AddValidLocalesWithValidLocaleNoMessagesFile) {
+TEST(ExtensionL10nUtil, LoadMessageCatalogsBadJSONFormat) {
   ScopedTempDir temp;
   ASSERT_TRUE(temp.CreateUniqueTempDir());
 
   FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
   ASSERT_TRUE(file_util::CreateDirectory(src_path));
 
-  ASSERT_TRUE(file_util::CreateDirectory(src_path.AppendASCII("sr")));
+  FilePath locale = src_path.AppendASCII("en_US");
+  ASSERT_TRUE(file_util::CreateDirectory(locale));
 
-  scoped_ptr<Extension> extension(CreateMinimalExtension(""));
+  std::string data = "{ \"name\":";
+  ASSERT_TRUE(
+      file_util::WriteFile(locale.AppendASCII(Extension::kMessagesFilename),
+                           data.c_str(), data.length()));
 
   std::string error;
-  EXPECT_FALSE(extension_l10n_util::AddValidLocales(src_path,
-                                                    extension.get(),
-                                                    &error));
-
-  EXPECT_TRUE(extension->supported_locales().empty());
+  EXPECT_TRUE(NULL == extension_l10n_util::LoadMessageCatalogs(src_path,
+                                                              "en-US",
+                                                              "sr",
+                                                              &error));
+  EXPECT_EQ("Line: 1, column: 10, Syntax error.", error);
 }
 
-TEST(ExtensionL10nUtil, AddValidLocalesWithValidLocalesAndMessagesFile) {
+TEST(ExtensionL10nUtil, LoadMessageCatalogsDuplicateKeys) {
   ScopedTempDir temp;
   ASSERT_TRUE(temp.CreateUniqueTempDir());
 
   FilePath src_path = temp.path().AppendASCII(Extension::kLocaleFolder);
   ASSERT_TRUE(file_util::CreateDirectory(src_path));
 
-  FilePath locale_1 = src_path.AppendASCII("sr");
+  FilePath locale_1 = src_path.AppendASCII("en_US");
   ASSERT_TRUE(file_util::CreateDirectory(locale_1));
 
-  std::string data = "foobar";
+  std::string data =
+    "{ \"name\": { \"message\": \"something\" }, "
+    "\"name\": { \"message\": \"something else\" } }";
   ASSERT_TRUE(
       file_util::WriteFile(locale_1.AppendASCII(Extension::kMessagesFilename),
                            data.c_str(), data.length()));
 
-  FilePath locale_2 = src_path.AppendASCII("en_US");
+  FilePath locale_2 = src_path.AppendASCII("sr");
   ASSERT_TRUE(file_util::CreateDirectory(locale_2));
 
   ASSERT_TRUE(
       file_util::WriteFile(locale_2.AppendASCII(Extension::kMessagesFilename),
                            data.c_str(), data.length()));
 
-  scoped_ptr<Extension> extension(CreateMinimalExtension(""));
-
   std::string error;
-  EXPECT_TRUE(extension_l10n_util::AddValidLocales(src_path,
-                                                   extension.get(),
-                                                   &error));
-
-  EXPECT_EQ(static_cast<unsigned int>(2),
-            extension->supported_locales().size());
-}
-
-TEST(ExtensionL10nUtil, SetDefaultLocaleGoodDefaultLocaleInManifest) {
-  scoped_ptr<Extension> extension(CreateMinimalExtension("sr"));
-  extension->AddSupportedLocale("sr");
-  extension->AddSupportedLocale("en-US");
-
-  EXPECT_TRUE(extension_l10n_util::ValidateDefaultLocale(extension.get()));
-  EXPECT_EQ("sr", extension->default_locale());
-}
-
-TEST(ExtensionL10nUtil, SetDefaultLocaleNoDefaultLocaleInManifest) {
-  scoped_ptr<Extension> extension(CreateMinimalExtension(""));
-  extension->AddSupportedLocale("sr");
-  extension->AddSupportedLocale("en-US");
-
-  EXPECT_FALSE(extension_l10n_util::ValidateDefaultLocale(extension.get()));
-}
-
-TEST(ExtensionL10nUtil, SetDefaultLocaleWrongDefaultLocaleInManifest) {
-  scoped_ptr<Extension> extension(CreateMinimalExtension("ko"));
-  extension->AddSupportedLocale("sr");
-  extension->AddSupportedLocale("en-US");
-
-  EXPECT_FALSE(extension_l10n_util::ValidateDefaultLocale(extension.get()));
+  // JSON parser hides duplicates. We are going to get only one key/value
+  // pair at the end.
+  scoped_ptr<ExtensionMessageBundle> message_bundle(
+      extension_l10n_util::LoadMessageCatalogs(src_path,
+                                               "en-US",
+                                               "sr",
+                                               &error));
+  EXPECT_TRUE(NULL != message_bundle.get());
+  EXPECT_TRUE(error.empty());
 }
 
 }  // namespace
diff --git a/chrome/browser/extensions/extension_message_bundle.cc b/chrome/browser/extensions/extension_message_bundle.cc
new file mode 100644
index 0000000000000000000000000000000000000000..34fd1a2704468bbf405ad9cf1dd6fcc0ea118caf
--- /dev/null
+++ b/chrome/browser/extensions/extension_message_bundle.cc
@@ -0,0 +1,256 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/extensions/extension_message_bundle.h"
+
+#include <string>
+
+#include "base/hash_tables.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "base/values.h"
+
+const wchar_t* ExtensionMessageBundle::kContentKey = L"content";
+const wchar_t* ExtensionMessageBundle::kMessageKey = L"message";
+const wchar_t* ExtensionMessageBundle::kPlaceholdersKey = L"placeholders";
+
+const char* ExtensionMessageBundle::kPlaceholderBegin = "$";
+const char* ExtensionMessageBundle::kPlaceholderEnd = "$";
+const char* ExtensionMessageBundle::kMessageBegin = "__MSG_";
+const char* ExtensionMessageBundle::kMessageEnd = "__";
+
+const char* ExtensionMessageBundle::kExtensionName = "chrome_extension_name";
+const char* ExtensionMessageBundle::kExtensionDescription =
+  "chrome_extension_description";
+
+// Formats message in case we encounter a bad formed key in the JSON object.
+// Returns false and sets |error| to actual error message.
+static bool BadKeyMessage(const std::string& name, std::string* error) {
+  *error = StringPrintf("Name of a key \"%s\" is invalid. Only ASCII [a-z], "
+                        "[A-Z], [0-9] and \"_\" are allowed.", name.c_str());
+  return false;
+}
+
+// static
+ExtensionMessageBundle* ExtensionMessageBundle::Create(
+    const DictionaryValue& default_locale_catalog,
+    const DictionaryValue& current_locale_catalog,
+    std::string* error) {
+  scoped_ptr<ExtensionMessageBundle> message_bundle(
+      new ExtensionMessageBundle);
+  if (!message_bundle->Init(default_locale_catalog,
+                             current_locale_catalog,
+                             error))
+    return NULL;
+
+  return message_bundle.release();
+}
+
+bool ExtensionMessageBundle::Init(const DictionaryValue& default_locale_catalog,
+                                  const DictionaryValue& current_locale_catalog,
+                                  std::string* error) {
+  dictionary_.clear();
+
+  // Create a single dictionary out of default and current_locale catalogs.
+  // If message is missing from current_locale catalog, we take one from default
+  // catalog.
+  DictionaryValue::key_iterator key_it = current_locale_catalog.begin_keys();
+  for (; key_it != current_locale_catalog.end_keys(); ++key_it) {
+    std::string key(StringToLowerASCII(WideToUTF8(*key_it)));
+    if (!IsValidName(*key_it))
+      return BadKeyMessage(key, error);
+    std::string value;
+    if (!GetMessageValue(*key_it, current_locale_catalog, &value, error))
+      return false;
+    // Keys are not case-sensitive.
+    dictionary_[key] = value;
+  }
+
+  key_it = default_locale_catalog.begin_keys();
+  for (; key_it != default_locale_catalog.end_keys(); ++key_it) {
+    std::string key(StringToLowerASCII(WideToUTF8(*key_it)));
+    if (!IsValidName(*key_it))
+      return BadKeyMessage(key, error);
+    // Add only messages that are not provided by app_catalog.
+    if (dictionary_.find(key) != dictionary_.end())
+      continue;
+    std::string value;
+    if (!GetMessageValue(*key_it, default_locale_catalog, &value, error))
+      return false;
+    // Keys are not case-sensitive.
+    dictionary_[key] = value;
+  }
+
+  return true;
+}
+
+bool ExtensionMessageBundle::GetMessageValue(const std::wstring& wkey,
+                                             const DictionaryValue& catalog,
+                                             std::string* value,
+                                             std::string* error) const {
+  std::string key(WideToUTF8(wkey));
+  // Get the top level tree for given key (name part).
+  DictionaryValue* name_tree;
+  if (!catalog.GetDictionary(wkey, &name_tree)) {
+    *error = StringPrintf("Not a valid tree for key %s.", key.c_str());
+    return false;
+  }
+  // Extract message from it.
+  if (!name_tree->GetString(kMessageKey, value)) {
+    *error = StringPrintf("There is no \"%s\" element for key %s.",
+                          WideToUTF8(kMessageKey).c_str(),
+                          key.c_str());
+    return false;
+  }
+
+  SubstitutionMap placeholders;
+  if (!GetPlaceholders(*name_tree, key, &placeholders, error))
+    return false;
+
+  if (!ReplacePlaceholders(placeholders, value, error))
+    return false;
+
+  return true;
+}
+
+ExtensionMessageBundle::ExtensionMessageBundle() {
+}
+
+bool ExtensionMessageBundle::GetPlaceholders(const DictionaryValue& name_tree,
+                                             const std::string& name_key,
+                                             SubstitutionMap* placeholders,
+                                             std::string* error) const {
+  if (!name_tree.HasKey(kPlaceholdersKey))
+    return true;
+
+  DictionaryValue* placeholders_tree;
+  if (!name_tree.GetDictionary(kPlaceholdersKey, &placeholders_tree)) {
+    *error = StringPrintf("Not a valid \"%s\" element for key %s.",
+                          WideToUTF8(kPlaceholdersKey).c_str(),
+                          name_key.c_str());
+    return false;
+  }
+
+  for (DictionaryValue::key_iterator key_it = placeholders_tree->begin_keys();
+       key_it != placeholders_tree->end_keys();
+       ++key_it) {
+    DictionaryValue* placeholder;
+    std::string content_key = WideToUTF8(*key_it);
+    if (!IsValidName(*key_it))
+      return BadKeyMessage(content_key, error);
+    if (!placeholders_tree->GetDictionary(*key_it, &placeholder)) {
+      *error = StringPrintf("Invalid placeholder %s for key %s",
+                            content_key.c_str(),
+                            name_key.c_str());
+      return false;
+    }
+    std::string content;
+    if (!placeholder->GetString(kContentKey, &content)) {
+      *error = StringPrintf("Invalid \"%s\" element for key %s.",
+                            WideToUTF8(kContentKey).c_str(),
+                            name_key.c_str());
+      return false;
+    }
+    (*placeholders)[StringToLowerASCII(content_key)] = content;
+  }
+
+  return true;
+}
+
+bool ExtensionMessageBundle::ReplacePlaceholders(
+    const SubstitutionMap& placeholders,
+    std::string* message,
+    std::string* error) const {
+  return ReplaceVariables(placeholders,
+                          kPlaceholderBegin,
+                          kPlaceholderEnd,
+                          message,
+                          error);
+}
+
+bool ExtensionMessageBundle::ReplaceMessages(std::string* text,
+                                             std::string* error) const {
+  return ReplaceVariables(dictionary_, kMessageBegin, kMessageEnd, text, error);
+}
+
+// static
+bool ExtensionMessageBundle::ReplaceVariables(
+    const SubstitutionMap& variables,
+    const std::string& var_begin_delimiter,
+    const std::string& var_end_delimiter,
+    std::string* message,
+    std::string* error) {
+  std::string::size_type beg_index = 0;
+  const std::string::size_type var_begin_delimiter_size =
+    var_begin_delimiter.size();
+  while (true) {
+    beg_index = message->find(var_begin_delimiter, beg_index);
+    if (beg_index == message->npos)
+      return true;
+
+    // Advance it immediately to the begining of possible variable name.
+    beg_index += var_begin_delimiter_size;
+    if (beg_index >= message->size())
+      return true;
+    std::string::size_type end_index =
+      message->find(var_end_delimiter, beg_index);
+    if (end_index == message->npos)
+      return true;
+
+    // Looking for 1 in substring of ...$1$....
+    const std::string& var_name =
+      message->substr(beg_index, end_index - beg_index);
+    if (!IsValidName(var_name))
+      continue;
+    SubstitutionMap::const_iterator it =
+      variables.find(StringToLowerASCII(var_name));
+    if (it == variables.end()) {
+      *error = StringPrintf("Variable %s%s%s used but not defined.",
+                            var_begin_delimiter.c_str(),
+                            var_name.c_str(),
+                            var_end_delimiter.c_str());
+      return false;
+    }
+
+    // Replace variable with its value.
+    std::string value = it->second;
+    message->replace(beg_index - var_begin_delimiter_size,
+                     end_index - beg_index + var_begin_delimiter_size +
+                       var_end_delimiter.size(),
+                     value);
+
+    // And position pointer to after the replacement.
+    beg_index += value.size() - var_begin_delimiter_size;
+  }
+
+  return true;
+}
+
+// static
+template <typename str>
+bool ExtensionMessageBundle::IsValidName(const str& name) {
+  if (name.empty())
+    return false;
+
+  for (typename str::const_iterator it = name.begin(); it != name.end(); ++it) {
+    // Allow only ascii 0-9, a-z, A-Z, and _ in the name.
+    if (!IsAsciiAlpha(*it) && !IsAsciiDigit(*it) && *it != '_')
+      return false;
+  }
+
+  return true;
+}
+
+// Dictionary interface.
+
+std::string ExtensionMessageBundle::GetL10nMessage(
+    const std::string& name) const {
+  SubstitutionMap::const_iterator it =
+    dictionary_.find(StringToLowerASCII(name));
+  if (it != dictionary_.end()) {
+    return it->second;
+  }
+
+  return "";
+}
diff --git a/chrome/browser/extensions/extension_message_bundle.h b/chrome/browser/extensions/extension_message_bundle.h
new file mode 100644
index 0000000000000000000000000000000000000000..548f7ece8a365c96094422d0b3729ad0cde30c0f
--- /dev/null
+++ b/chrome/browser/extensions/extension_message_bundle.h
@@ -0,0 +1,110 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_MESSAGE_BUNDLE_H_
+#define CHROME_BROWSER_EXTENSIONS_EXTENSION_MESSAGE_BUNDLE_H_
+
+#include <string>
+
+#include "base/hash_tables.h"
+#include "base/values.h"
+
+// Contains localized extension messages for one locale. Any messages that the
+// locale does not provide are pulled from the default locale.
+class ExtensionMessageBundle {
+ public:
+  typedef base::hash_map<std::string, std::string> SubstitutionMap;
+
+  // JSON keys of interest for messages file.
+  static const wchar_t* kContentKey;
+  static const wchar_t* kMessageKey;
+  static const wchar_t* kPlaceholdersKey;
+
+  // Begin/end markers for placeholders and messages
+  static const char* kPlaceholderBegin;
+  static const char* kPlaceholderEnd;
+  static const char* kMessageBegin;
+  static const char* kMessageEnd;
+
+  // Extension name and description message names
+  static const char* kExtensionName;
+  static const char* kExtensionDescription;
+
+  // Creates ExtensionMessageBundle or returns NULL if there was an error.
+  static ExtensionMessageBundle* Create(
+      const DictionaryValue& default_locale_catalog,
+      const DictionaryValue& current_locale_catalog,
+      std::string* error);
+
+  // Get message from the catalog with given key.
+  // Returned message has all of the internal placeholders resolved to their
+  // value (content).
+  // Returns empty string if it can't find a message.
+  // We don't use simple GetMessage name, since there is a global
+  // #define GetMessage GetMessageW override in Chrome code.
+  std::string GetL10nMessage(const std::string& name) const;
+
+  // Number of messages in the catalog.
+  // Used for unittesting only.
+  size_t size() const { return dictionary_.size(); }
+
+  // Replaces all __MSG_message__ with values from the catalog.
+  // Returns false if there is a message in text that's not defined in the
+  // dictionary.
+  bool ReplaceMessages(std::string* text, std::string* error) const;
+
+  // Replaces each occurance of variable placeholder with its value.
+  // I.e. replaces __MSG_name__ with value from the catalog with the key "name".
+  // Returns false if for a valid message/placeholder name there is no matching
+  // replacement.
+  // Public for easier unittesting.
+  static bool ReplaceVariables(const SubstitutionMap& variables,
+                               const std::string& var_begin,
+                               const std::string& var_end,
+                               std::string* message,
+                               std::string* error);
+
+  // Allow only ascii 0-9, a-z, A-Z, and _ in the variable name.
+  // Returns false if the input is empty or if it has illegal characters.
+  // Public for easier unittesting.
+  template<typename str>
+  static bool IsValidName(const str& name);
+
+ private:
+  // Use Create to create ExtensionMessageBundle instance.
+   ExtensionMessageBundle();
+
+  // Initializes the instance from the contents of two catalogs. If a key is not
+  // present in current_locale_catalog, the value from default_local_catalog is
+  // used instead.
+  // Returns false on error.
+  bool Init(const DictionaryValue& default_locale_catalog,
+            const DictionaryValue& current_locale_catalog,
+            std::string* error);
+
+  // Helper methods that navigate JSON tree and return simplified message.
+  // They replace all $PLACEHOLDERS$ with their value, and return just key/value
+  // of the message.
+  bool GetMessageValue(const std::wstring& wkey,
+                       const DictionaryValue& catalog,
+                       std::string* value,
+                       std::string* error) const;
+
+  // Get all placeholders for a given message from JSON subtree.
+  bool GetPlaceholders(const DictionaryValue& name_tree,
+                       const std::string& name_key,
+                       SubstitutionMap* placeholders,
+                       std::string* error) const;
+
+  // For a given message, replaces all placeholders with their actual value.
+  // Returns false if replacement failed (see ReplaceVariables).
+  bool ReplacePlaceholders(const SubstitutionMap& placeholders,
+                           std::string* message,
+                           std::string* error) const;
+
+  // Holds all messages for application locale.
+  SubstitutionMap dictionary_;
+};
+
+#endif  // CHROME_BROWSER_EXTENSIONS_EXTENSION_MESSAGE_BUNDLE_H_
diff --git a/chrome/browser/extensions/extension_message_bundle_unittest.cc b/chrome/browser/extensions/extension_message_bundle_unittest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..d7344b3e68acccdc015fa0d1d057ae5b33743f5e
--- /dev/null
+++ b/chrome/browser/extensions/extension_message_bundle_unittest.cc
@@ -0,0 +1,288 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/extensions/extension_message_bundle.h"
+
+#include <string>
+
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+// Helper method for dictionary building.
+void SetDictionary(const std::wstring name,
+                   DictionaryValue* target,
+                   DictionaryValue* subtree) {
+  target->Set(name, static_cast<Value*>(subtree));
+}
+
+void CreateContentTree(const std::wstring& name,
+                       const std::string content,
+                       DictionaryValue* dict) {
+  DictionaryValue* content_tree = new DictionaryValue;
+  content_tree->SetString(ExtensionMessageBundle::kContentKey, content);
+  SetDictionary(name, dict, content_tree);
+}
+
+void CreatePlaceholdersTree(DictionaryValue* dict) {
+  DictionaryValue* placeholders_tree = new DictionaryValue;
+  CreateContentTree(L"a", "A", placeholders_tree);
+  CreateContentTree(L"b", "B", placeholders_tree);
+  CreateContentTree(L"c", "C", placeholders_tree);
+  SetDictionary(ExtensionMessageBundle::kPlaceholdersKey,
+                dict,
+                placeholders_tree);
+}
+
+void CreateMessageTree(const std::wstring& name,
+                       const std::string& message,
+                       bool create_placeholder_subtree,
+                       DictionaryValue* dict) {
+  DictionaryValue* message_tree = new DictionaryValue;
+  if (create_placeholder_subtree)
+    CreatePlaceholdersTree(message_tree);
+  message_tree->SetString(ExtensionMessageBundle::kMessageKey, message);
+  SetDictionary(name, dict, message_tree);
+}
+
+void CreateGoodDictionary(DictionaryValue* dict) {
+  dict->Clear();
+  CreateMessageTree(L"n1", "message1 $a$ $b$", true, dict);
+  CreateMessageTree(L"n2", "message2 $c$", true, dict);
+  CreateMessageTree(L"n3", "message3", false, dict);
+}
+
+enum BadDictionary {
+  INVALID_NAME,
+  NAME_NOT_A_TREE,
+  EMPTY_NAME_TREE,
+  MISSING_MESSAGE,
+  PLACEHOLDER_NOT_A_TREE,
+  EMPTY_PLACEHOLDER_TREE,
+  CONTENT_MISSING,
+  MESSAGE_PLACEHOLDER_DOESNT_MATCH,
+};
+
+void CreateBadDictionary(DictionaryValue* dict,
+                         enum BadDictionary what_is_bad) {
+  CreateGoodDictionary(dict);
+  // Now remove/break things.
+  switch (what_is_bad) {
+    case INVALID_NAME:
+      CreateMessageTree(L"n 5", "nevermind", false, dict);
+      break;
+    case NAME_NOT_A_TREE:
+      dict->SetString(L"n4", "whatever");
+      break;
+    case EMPTY_NAME_TREE: {
+        DictionaryValue* empty_tree = new DictionaryValue;
+        SetDictionary(L"n4", dict, empty_tree);
+      }
+      break;
+    case MISSING_MESSAGE:
+      dict->Remove(L"n1.message", NULL);
+      break;
+    case PLACEHOLDER_NOT_A_TREE:
+      dict->SetString(L"n1.placeholders", "whatever");
+      break;
+    case EMPTY_PLACEHOLDER_TREE: {
+        DictionaryValue* empty_tree = new DictionaryValue;
+        SetDictionary(L"n1.placeholders", dict, empty_tree);
+      }
+      break;
+    case CONTENT_MISSING:
+       dict->Remove(L"n1.placeholders.a.content", NULL);
+      break;
+    case MESSAGE_PLACEHOLDER_DOESNT_MATCH:
+      DictionaryValue* value;
+      dict->Remove(L"n1.placeholders.a", NULL);
+      dict->GetDictionary(L"n1.placeholders", &value);
+      CreateContentTree(L"x", "X", value);
+      break;
+  }
+}
+
+TEST(ExtensionMessageBundle, InitEmptyDictionaries) {
+  DictionaryValue default_dict;
+  DictionaryValue app_dict;
+  std::string error;
+  scoped_ptr<ExtensionMessageBundle> handler(
+      ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+
+  EXPECT_TRUE(handler.get() != NULL);
+  EXPECT_EQ(0U, handler->size());
+}
+
+TEST(ExtensionMessageBundle, InitGoodDefaultDictEmptyAppDict) {
+  DictionaryValue default_dict;
+  DictionaryValue app_dict;
+  std::string error;
+
+  CreateGoodDictionary(&default_dict);
+  scoped_ptr<ExtensionMessageBundle> handler(
+      ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+
+  EXPECT_TRUE(handler.get() != NULL);
+  EXPECT_EQ(3U, handler->size());
+
+  EXPECT_EQ("message1 A B", handler->GetL10nMessage("n1"));
+  EXPECT_EQ("message2 C", handler->GetL10nMessage("n2"));
+  EXPECT_EQ("message3", handler->GetL10nMessage("n3"));
+}
+
+TEST(ExtensionMessageBundle, InitAppDictConsultedFirst) {
+  DictionaryValue default_dict;
+  DictionaryValue app_dict;
+  std::string error;
+
+  CreateGoodDictionary(&default_dict);
+  CreateGoodDictionary(&app_dict);
+  // Flip placeholders in message of n1 tree.
+  app_dict.SetString(L"n1.message", "message1 $b$ $a$");
+  // Remove one message from app dict.
+  app_dict.Remove(L"n2", NULL);
+  // Replace n3 with N3.
+  app_dict.Remove(L"n3", NULL);
+  CreateMessageTree(L"N3", "message3_app_dict", false, &app_dict);
+
+  scoped_ptr<ExtensionMessageBundle> handler(
+      ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+
+  EXPECT_TRUE(handler.get() != NULL);
+  EXPECT_EQ(3U, handler->size());
+
+  EXPECT_EQ("message1 B A", handler->GetL10nMessage("n1"));
+  EXPECT_EQ("message2 C", handler->GetL10nMessage("n2"));
+  EXPECT_EQ("message3_app_dict", handler->GetL10nMessage("n3"));
+}
+
+TEST(ExtensionMessageBundle, InitBadAppDict) {
+  DictionaryValue default_dict;
+  DictionaryValue app_dict;
+  std::string error;
+
+  CreateBadDictionary(&app_dict, INVALID_NAME);
+  scoped_ptr<ExtensionMessageBundle> handler(
+      ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Name of a key \"n 5\" is invalid. Only ASCII [a-z], "
+            "[A-Z], [0-9] and \"_\" are allowed.", error);
+
+  CreateBadDictionary(&app_dict, NAME_NOT_A_TREE);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Not a valid tree for key n4.", error);
+
+  CreateBadDictionary(&app_dict, EMPTY_NAME_TREE);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("There is no \"message\" element for key n4.", error);
+
+  CreateBadDictionary(&app_dict, MISSING_MESSAGE);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("There is no \"message\" element for key n1.", error);
+
+  CreateBadDictionary(&app_dict, PLACEHOLDER_NOT_A_TREE);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Not a valid \"placeholders\" element for key n1.", error);
+
+  CreateBadDictionary(&app_dict, EMPTY_PLACEHOLDER_TREE);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Variable $a$ used but not defined.", error);
+
+  CreateBadDictionary(&app_dict, CONTENT_MISSING);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Invalid \"content\" element for key n1.", error);
+
+  CreateBadDictionary(&app_dict, MESSAGE_PLACEHOLDER_DOESNT_MATCH);
+  handler.reset(ExtensionMessageBundle::Create(default_dict, app_dict, &error));
+  EXPECT_TRUE(handler.get() == NULL);
+  EXPECT_EQ("Variable $a$ used but not defined.", error);
+}
+
+struct ReplaceVariables {
+  const char* original;
+  const char* result;
+  const char* error;
+  const char* begin_delimiter;
+  const char* end_delimiter;
+  bool pass;
+};
+
+TEST(ExtensionMessageBundle, ReplaceMessagesInText) {
+  const char* kMessageBegin = ExtensionMessageBundle::kMessageBegin;
+  const char* kMessageEnd = ExtensionMessageBundle::kMessageEnd;
+  const char* kPlaceholderBegin = ExtensionMessageBundle::kPlaceholderBegin;
+  const char* kPlaceholderEnd = ExtensionMessageBundle::kPlaceholderEnd;
+
+  static ReplaceVariables test_cases[] = {
+    // Message replacement.
+    { "This is __MSG_siMPle__ message", "This is simple message",
+      "", kMessageBegin, kMessageEnd, true },
+    { "This is __MSG_", "This is __MSG_",
+      "", kMessageBegin, kMessageEnd, true },
+    { "This is __MSG__simple__ message", "This is __MSG__simple__ message",
+      "Variable __MSG__simple__ used but not defined.",
+      kMessageBegin, kMessageEnd, false },
+    { "__MSG_LoNg__", "A pretty long replacement",
+      "", kMessageBegin, kMessageEnd, true },
+    { "A __MSG_SimpLE__MSG_ a", "A simpleMSG_ a",
+      "", kMessageBegin, kMessageEnd, true },
+    { "A __MSG_simple__MSG_long__", "A simpleMSG_long__",
+      "", kMessageBegin, kMessageEnd, true },
+    { "A __MSG_simple____MSG_long__", "A simpleA pretty long replacement",
+      "", kMessageBegin, kMessageEnd, true },
+    { "__MSG_d1g1ts_are_ok__", "I are d1g1t",
+      "", kMessageBegin, kMessageEnd, true },
+    // Placeholder replacement.
+    { "This is $sImpLe$ message", "This is simple message",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "This is $", "This is $",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "This is $$sIMPle$ message", "This is $simple message",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "$LONG_V$", "A pretty long replacement",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "A $simple$$ a", "A simple$ a",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "A $simple$long_v$", "A simplelong_v$",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "A $simple$$long_v$", "A simpleA pretty long replacement",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "This is $bad name$", "This is $bad name$",
+       "", kPlaceholderBegin, kPlaceholderEnd, true },
+    { "This is $missing$", "This is $missing$",
+       "Variable $missing$ used but not defined.",
+       kPlaceholderBegin, kPlaceholderEnd, false },
+  };
+
+  ExtensionMessageBundle::SubstitutionMap messages;
+  messages.insert(std::make_pair("simple", "simple"));
+  messages.insert(std::make_pair("long", "A pretty long replacement"));
+  messages.insert(std::make_pair("long_v", "A pretty long replacement"));
+  messages.insert(std::make_pair("bad name", "Doesn't matter"));
+  messages.insert(std::make_pair("d1g1ts_are_ok", "I are d1g1t"));
+
+  for (size_t i = 0; i < arraysize(test_cases); ++i) {
+    std::string text = test_cases[i].original;
+    std::string error;
+    EXPECT_EQ(test_cases[i].pass,
+      ExtensionMessageBundle::ReplaceVariables(messages,
+                                               test_cases[i].begin_delimiter,
+                                               test_cases[i].end_delimiter,
+                                               &text,
+                                               &error));
+    EXPECT_EQ(test_cases[i].result, text);
+  }
+}
+
+}  // namespace
diff --git a/chrome/browser/extensions/extension_ui_unittest.cc b/chrome/browser/extensions/extension_ui_unittest.cc
index f193b6a805304ab95e0dab39f5f8a1fa57a9cf4f..773ebe2067aead75ec27556954dd95d4ce550429 100644
--- a/chrome/browser/extensions/extension_ui_unittest.cc
+++ b/chrome/browser/extensions/extension_ui_unittest.cc
@@ -58,9 +58,6 @@ namespace {
   }
 }  // namespace
 
-class ExtensionUITest : public testing::Test {
-};
-
 TEST(ExtensionUITest, GenerateExtensionsJSONData) {
   FilePath data_test_dir_path, extension_path, expected_output_path;
   EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &data_test_dir_path));
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 227cd39636f5dbac402efd48ccb4e1bdc2af3bbf..09d1e706d9efe71f19ebd60d3ebbcd5df3598793 100644
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -1148,6 +1148,8 @@
         'browser/extensions/extension_install_ui.h',
         'browser/extensions/extension_l10n_util.cc',
         'browser/extensions/extension_l10n_util.h',
+        'browser/extensions/extension_message_bundle.cc',
+        'browser/extensions/extension_message_bundle.h',
         'browser/extensions/extension_message_service.cc',
         'browser/extensions/extension_message_service.h',
         'browser/extensions/extension_browser_event_router.cc',
@@ -4132,6 +4134,7 @@
         'browser/encoding_menu_controller_unittest.cc',
         'browser/extensions/extension_file_util_unittest.cc',
         'browser/extensions/extension_l10n_util_unittest.cc',
+        'browser/extensions/extension_message_bundle_unittest.cc',
         'browser/extensions/extension_messages_unittest.cc',
         'browser/extensions/extension_process_manager_unittest.cc',
         'browser/extensions/extension_ui_unittest.cc',
diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc
index cd2a8c7ae09ab88994ccc1802771c642ccbae6a8..3a73bf20a2e9cf066a64c851e643ad947dcdc2a8 100644
--- a/chrome/common/extensions/extension.cc
+++ b/chrome/common/extensions/extension.cc
@@ -615,17 +615,27 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id,
     return false;
   }
 
-  // Initialize name.
+  // Initialize & localize name.
   if (!source.GetString(keys::kName, &name_)) {
     *error = errors::kInvalidName;
     return false;
+  } else if (message_bundle_.get()) {
+    std::string l10n_name =
+      message_bundle_->GetL10nMessage(ExtensionMessageBundle::kExtensionName);
+    if (!l10n_name.empty())
+      name_ = l10n_name;
   }
 
-  // Initialize description (if present).
+  // Initialize & localize description (if present).
   if (source.HasKey(keys::kDescription)) {
     if (!source.GetString(keys::kDescription, &description_)) {
       *error = errors::kInvalidDescription;
       return false;
+    } else if (message_bundle_.get()) {
+      std::string l10n_description = message_bundle_->GetL10nMessage(
+          ExtensionMessageBundle::kExtensionDescription);
+      if (!l10n_description.empty())
+        description_ = l10n_description;
     }
   }
 
@@ -985,18 +995,6 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id,
     }
   }
 
-  // Initialize default locale (if present).
-  if (source.HasKey(keys::kDefaultLocale)) {
-    std::string default_locale;
-    if (!source.GetString(keys::kDefaultLocale, &default_locale)) {
-      *error = errors::kInvalidDefaultLocale;
-      return false;
-    }
-    // Normalize underscores to hyphens.
-    std::replace(default_locale.begin(), default_locale.end(), '_', '-');
-    set_default_locale(default_locale);
-  }
-
   // Chrome URL overrides (optional)
   if (source.HasKey(keys::kChromeURLOverrides)) {
     DictionaryValue* overrides;
diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h
index 5116bd39abe1c5fda267840967c1f3bd8071c849..eb7b86c6622fcf906fdb866f04f7675bef2e2918 100644
--- a/chrome/common/extensions/extension.h
+++ b/chrome/common/extensions/extension.h
@@ -14,8 +14,9 @@
 #include "base/scoped_ptr.h"
 #include "base/values.h"
 #include "base/version.h"
-#include "chrome/common/extensions/user_script.h"
+#include "chrome/browser/extensions/extension_message_bundle.h"
 #include "chrome/browser/extensions/user_script_master.h"
+#include "chrome/common/extensions/user_script.h"
 #include "chrome/common/extensions/url_pattern.h"
 #include "chrome/common/page_action.h"
 #include "googleurl/src/gurl.h"
@@ -251,19 +252,12 @@ class Extension {
     return manifest_value_.get();
   }
 
-  // Returns a list of all locales supported by the extension.
-  const std::set<std::string>& supported_locales() const {
-    return supported_locales_;
+  // Getter/setter for l10n message bundle.
+  const ExtensionMessageBundle* message_bundle() const {
+    return message_bundle_.get();
   }
-  // Add locale to the list of supported locales.
-  void AddSupportedLocale(const std::string& supported_locale) {
-    supported_locales_.insert(supported_locale);
-  }
-
-  // Getter/setter for a default_locale_.
-  const std::string& default_locale() const { return default_locale_; }
-  void set_default_locale(const std::string& default_locale) {
-    default_locale_ = default_locale;
+  void set_message_bundle(ExtensionMessageBundle* message_bundle) {
+    message_bundle_.reset(message_bundle);
   }
 
   // Chrome URL overrides (see ExtensionOverrideUI).
@@ -385,11 +379,8 @@ class Extension {
   // A copy of the manifest that this extension was created from.
   scoped_ptr<DictionaryValue> manifest_value_;
 
-    // List of all locales extension supports.
-  std::set<std::string> supported_locales_;
-
-  // Default locale, used for fallback.
-  std::string default_locale_;
+  // Handles the l10n messages replacement and parsing.
+  scoped_ptr<ExtensionMessageBundle> message_bundle_;
 
   // A map of chrome:// hostnames (newtab, downloads, etc.) to Extension URLs
   // which override the handling of those URLs.
diff --git a/chrome/common/extensions/extension_unittest.cc b/chrome/common/extensions/extension_unittest.cc
index 564ef0c6c5c0f312f22da50b34c0aae6f948da75..f1ee5e1b7b2ad82505c5d83b1c8d765c864619c9 100644
--- a/chrome/common/extensions/extension_unittest.cc
+++ b/chrome/common/extensions/extension_unittest.cc
@@ -219,12 +219,6 @@ TEST(ExtensionTest, InitFromValueInvalid) {
   privacy_blacklists->Set(0, Value::CreateIntegerValue(42));
   EXPECT_FALSE(extension.InitFromValue(*input_value, true, &error));
   EXPECT_TRUE(MatchPattern(error, errors::kInvalidPrivacyBlacklistsPath));
-
-  // Test invalid default locale.
-  input_value.reset(static_cast<DictionaryValue*>(valid_value->DeepCopy()));
-  input_value->SetInteger(keys::kDefaultLocale, 42);
-  EXPECT_FALSE(extension.InitFromValue(*input_value, true, &error));
-  EXPECT_EQ(errors::kInvalidDefaultLocale, error);
 }
 
 TEST(ExtensionTest, InitFromValueValid) {
diff --git a/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/en_US/messages.json b/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/en_US/messages.json
index d329ac76b93a0965987eda2529f34b7ca242d652..c600b46d350e2b9fa784db3b7f75779d8dd2bef4 100644
--- a/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/en_US/messages.json
+++ b/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/en_US/messages.json
@@ -1,5 +1,8 @@
 {
-  "name": {
-    "message": "Some message."
+  "chrome_extension_name": {
+    "message": "My extension 1"
+  },
+  "chrome_extension_description": {
+    "message": "The first extension that I made."
   }
-}
\ No newline at end of file
+}
diff --git a/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/sr/messages.json b/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/sr/messages.json
index 31e86f4e4f52999fa2b4510321623e30a710ee97..3346bdf674210fcb13725535625834a7c8be970a 100644
--- a/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/sr/messages.json
+++ b/chrome/test/data/extensions/good/Extensions/behllobkkfkfnphdnhnkndlbkcpglgmj/1.0.0.0/_locales/sr/messages.json
@@ -1,5 +1,8 @@
 {
-  "name": {
-    "message": "Нека порука."
+  "chrome_extension_name": {
+    "message": "Моја порука 1"
+  },
+  "chrome_extension_description": {
+    "message": "Прва екстензија коју сам написао."
   }
-}
\ No newline at end of file
+}