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 +}