diff options
author | cira@chromium.org <cira@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-09-18 21:07:39 +0000 |
---|---|---|
committer | cira@chromium.org <cira@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-09-18 21:07:39 +0000 |
commit | 42b6f0f83579563a94bd9fdeec83574a1a932f18 (patch) | |
tree | bfdb770830f27f306eaeca8da46a6dd9e7ed8647 /chrome/browser/extensions | |
parent | 60ad3e2b8540ba00a2e5d0624fd2796e7780b11c (diff) | |
download | chromium_src-42b6f0f83579563a94bd9fdeec83574a1a932f18.zip chromium_src-42b6f0f83579563a94bd9fdeec83574a1a932f18.tar.gz chromium_src-42b6f0f83579563a94bd9fdeec83574a1a932f18.tar.bz2 |
CL is the same as http://codereview.chromium.org/173487, but had to be moved to new CL number because I switched machines.
Implemented the rest of loading/parsing logic for extension i18n:
1. Loading message catalogs for default and application locale.
2. Parsing JSON and replacing placeholders with actual content within a message.
3. Creating unified dictionary (union of default and application dictionaries,
where application dict. has priority for common messages).
New class ExtensionMessageBundle holds new dictionary, and parses data. It's
injected into Extension.
ExtensionMessageHandler::ReplaceVariablesInString can replace both
$placeholders$ and __MSG_messages__ in given string (HTML, manifest, actual
message string...).
Implemented actual manifest name/description replacement too, as an example.
Extension is now pretty agnostic about localization, and this makes it easier to use message bundles
with things that are not extensions...
BUG=12131
Review URL: http://codereview.chromium.org/202063
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@26609 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/extensions')
-rw-r--r-- | chrome/browser/extensions/extension_file_util.cc | 65 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_file_util.h | 16 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_file_util_unittest.cc | 72 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_l10n_util.cc | 84 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_l10n_util.h | 25 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_l10n_util_unittest.cc | 164 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_message_bundle.cc | 256 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_message_bundle.h | 110 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_message_bundle_unittest.cc | 288 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_ui_unittest.cc | 3 |
10 files changed, 898 insertions, 185 deletions
diff --git a/chrome/browser/extensions/extension_file_util.cc b/chrome/browser/extensions/extension_file_util.cc index 9c15da3..1692c67 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 9c13ea2..b6a737f 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 9f99606..7230ecd 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 5176b60..d13a97c 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 be73f99..98759ab 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 b3dc397..f674f69 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 0000000..34fd1a2 --- /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 0000000..548f7ec --- /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 0000000..d7344b3 --- /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 f193b6a..773ebe2 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)); |