Commit bb1e351c authored by Vidhan's avatar Vidhan Committed by Commit Bot

[Autofill][States] Implemented AlternativeStateNameMap

AlternativeStateNameMap encapsulates mappings from state names in the
profiles to their localized and the abbreviated version.

AlternativeStateNameMap can provide the following data for the states:
  1. The state string stored in the address profile.
  2. The state canonical name (StateEntry::canonical_name) which acts
     as the unique identifier representing the state. It is used for
     the comparison and determining the mergeability of the address
     profiles.
  3. The abbreviations of the state (StateEntry::abbreviations).
  4. The alternative names of the state (StateEntry::alternative_names).

StateEntry holds the information about the abbreviations and the
alternative names of the state.
Example of a StateEntry object:
  {
    'canonical_name': 'California',
    'abbreviations': ['CA'],
    'alternate_names': ['The Golden State']
  }

Bug: 1111960
Change-Id: I3dc3aa6f9c52c160d179e6dcad646b52b619b9de
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2360274Reviewed-by: default avatarMatthias Körber <koerber@google.com>
Reviewed-by: default avatarChristoph Schwering <schwering@google.com>
Reviewed-by: default avatarDominic Battré <battre@chromium.org>
Commit-Queue: Vidhan Jain <vidhanj@google.com>
Cr-Commit-Position: refs/heads/master@{#821693}
parent bc68a3bf
......@@ -153,6 +153,8 @@ static_library("browser") {
"form_types.h",
"geo/address_i18n.cc",
"geo/address_i18n.h",
"geo/alternative_state_name_map.cc",
"geo/alternative_state_name_map.h",
"geo/autofill_country.cc",
"geo/autofill_country.h",
"geo/country_data.cc",
......@@ -445,6 +447,8 @@ static_library("test_support") {
"autofill_test_utils.h",
"data_driven_test.cc",
"data_driven_test.h",
"geo/alternative_state_name_map_test_utils.cc",
"geo/alternative_state_name_map_test_utils.h",
"geo/test_region_data_loader.cc",
"geo/test_region_data_loader.h",
"logging/stub_log_manager.cc",
......@@ -628,6 +632,7 @@ source_set("unit_tests") {
"form_parsing/search_field_unittest.cc",
"form_structure_unittest.cc",
"geo/address_i18n_unittest.cc",
"geo/alternative_state_name_map_unittest.cc",
"geo/autofill_country_unittest.cc",
"geo/country_names_for_locale_unittest.cc",
"geo/country_names_unittest.cc",
......
// Copyright 2020 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 "components/autofill/core/browser/geo/alternative_state_name_map.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
namespace autofill {
namespace {
// Assuming a user can have maximum 500 profiles each containing a different
// state string in the worst case scenario.
constexpr int kMaxMapSize = 500;
// The characters to be removed from the state strings before the comparison.
constexpr char kCharsToStrip[] = ".- ";
} // namespace
// static
AlternativeStateNameMap* AlternativeStateNameMap::GetInstance() {
static base::NoDestructor<AlternativeStateNameMap>
g_alternative_state_name_map;
return g_alternative_state_name_map.get();
}
AlternativeStateNameMap::AlternativeStateNameMap() = default;
// static
AlternativeStateNameMap::StateName AlternativeStateNameMap::NormalizeStateName(
const StateName& text) {
base::string16 normalized_text;
base::RemoveChars(text.value(), base::ASCIIToUTF16(kCharsToStrip),
&normalized_text);
return StateName(normalized_text);
}
base::Optional<AlternativeStateNameMap::CanonicalStateName>
AlternativeStateNameMap::GetCanonicalStateName(
const CountryCode& country_code,
const StateName& state_name,
bool is_state_name_normalized) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(alternative_state_name_map_sequence_checker_);
// Example:
// Entries in |localized_state_names_map_| are:
// ("DE", "Bavaria") -> {
// "canonical_name": "Bayern",
// "abbreviations": "BY",
// "alternative_names": "Bavaria"
// }
// Entries in |localized_state_names_reverse_lookup_map_| are:
// ("DE", "Bayern") -> "Bayern"
// ("DE", "BY") -> "Bayern"
// ("DE", "Bavaria") -> "Bayern"
// then, AlternativeStateNameMap::GetCanonicalStateName("DE", "Bayern") =
// AlternativeStateNameMap::GetCanonicalStateName("DE", "BY") =
// AlternativeStateNameMap::GetCanonicalStateName("DE", "Bavaria") =
// CanonicalStateName("Bayern")
StateName normalized_state_name = state_name;
if (!is_state_name_normalized)
normalized_state_name = NormalizeStateName(state_name);
auto it = localized_state_names_reverse_lookup_map_.find(
{country_code, normalized_state_name});
if (it != localized_state_names_reverse_lookup_map_.end())
return it->second;
return base::nullopt;
}
base::Optional<StateEntry> AlternativeStateNameMap::GetEntry(
const CountryCode& country_code,
const StateName& state_string_from_profile) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(alternative_state_name_map_sequence_checker_);
StateName normalized_state_string_from_profile =
NormalizeStateName(state_string_from_profile);
base::Optional<CanonicalStateName> canonical_state_name =
GetCanonicalStateName(country_code, normalized_state_string_from_profile,
/*is_state_name_normalized=*/true);
if (!canonical_state_name) {
canonical_state_name =
CanonicalStateName(normalized_state_string_from_profile.value());
}
DCHECK(canonical_state_name);
auto it = localized_state_names_map_.find(
{country_code, canonical_state_name.value()});
if (it != localized_state_names_map_.end())
return it->second;
return base::nullopt;
}
void AlternativeStateNameMap::AddEntry(
const CountryCode& country_code,
const StateName& normalized_state_value_from_profile,
const StateEntry& state_entry,
const std::vector<StateName>& normalized_alternative_state_names,
CanonicalStateName* normalized_canonical_state_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(alternative_state_name_map_sequence_checker_);
// Example:
// AddEntry("DE", "Bavaria", {
// "canonical_name": "Bayern",
// "abbreviations": "BY",
// "alternative_names": "Bavaria"
// }, {"Bavaria", "BY", "Bayern"}, "Bayern")
// Then entry added to |localized_state_names_map_| is:
// ("DE", "Bayern") -> {
// "canonical_name": "Bayern",
// "abbreviations": "BY",
// "alternative_names": "Bavaria"
// }
// Entries added to |localized_state_names_reverse_lookup_map_| are:
// ("DE", "Bayern") -> "Bayern"
// ("DE", "BY") -> "Bayern"
// ("DE", "Bavaria") -> "Bayern"
if (localized_state_names_map_.size() == kMaxMapSize ||
GetCanonicalStateName(country_code, normalized_state_value_from_profile,
/*is_state_name_normalized=*/true)) {
return;
}
if (normalized_canonical_state_name) {
localized_state_names_map_[{
country_code, *normalized_canonical_state_name}] = state_entry;
for (const auto& alternative_name : normalized_alternative_state_names) {
localized_state_names_reverse_lookup_map_[{
country_code, alternative_name}] = *normalized_canonical_state_name;
}
} else {
localized_state_names_map_[{
country_code,
CanonicalStateName(normalized_state_value_from_profile.value())}] =
state_entry;
}
}
bool AlternativeStateNameMap::IsLocalisedStateNamesMapEmpty() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(alternative_state_name_map_sequence_checker_);
return localized_state_names_map_.empty();
}
void AlternativeStateNameMap::ClearAlternativeStateNameMap() {
DCHECK_CALLED_ON_VALID_SEQUENCE(alternative_state_name_map_sequence_checker_);
localized_state_names_map_.clear();
localized_state_names_reverse_lookup_map_.clear();
}
} // namespace autofill
// Copyright 2020 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 COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_H_
#define COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_H_
#include "components/autofill/core/browser/proto/states.pb.h"
#include "base/i18n/case_conversion.h"
#include "base/no_destructor.h"
#include "base/optional.h"
#include "base/sequence_checker.h"
#include "base/strings/string16.h"
#include "base/util/type_safety/strong_alias.h"
namespace autofill {
// AlternativeStateNameMap encapsulates mappings from state names in the
// profiles to their localized and the abbreviated names.
//
// AlternativeStateNameMap is used for the filling of state fields, comparison
// of profiles, determining mergeability of the address profiles and required
// |ADDRESS_HOME_STATE| votes to be sent to the server.
//
// AlternativeStateNameMap can provide the following data for the states:
// 1. The state string stored in the address profile denoted by
// state_string_from_profile in this class.
// 2. The canonical state name (StateEntry::canonical_name) which acts as the
// unique identifier representing the state (unique within a country).
// 3. The abbreviations of the state (StateEntry::abbreviations).
// 4. The alternative names of the state (StateEntry::alternative_names).
//
// StateEntry holds the information about the abbreviations and the
// alternative names of the state which is determined after comparison with the
// state values saved in the address profiles if they match.
//
// The main map |localized_state_names_map_| maps the tuple
// (country_code, canonical state name) as the key to the corresponding
// StateEntry object (with the information about the abbreviations and the
// alternative names) as the value.
//
// The |localized_state_names_reverse_lookup_map_| takes in the
// country_code and StateEntry::name, StateEntry::abbreviations or
// ::alternative_names as the key and the canonical state name as the value.
//
// Example: Considering "California" as the state_string_from_profile and
// the corresponding StateEntry object:
// {
// 'canonical_name': 'California',
// 'abbreviations': ['CA'],
// 'alternate_names': ['The Golden State']
// }
//
// 1. StateEntry::canonical_name (i.e "California" in this case) acts
// as the canonical state name.
// 2. ("US", "California") acts as the key and the above StateEntry
// object is added as the value in the
// |localized_state_names_map_|.
// 3. Entries added to |localized_state_names_reverse_lookup_map_|
// are:
// a. ("US", "California") -> "California"
// b. ("US", "CA") -> "California"
// c. ("US", "TheGoldenState") -> "California"
//
// Example: Assuming the user creates an unknown state in the profile
// "Random State".
// 1. Entries added to the |localized_state_names_map_| are:
// ("RandomState", Empty StateEntry object)
// 2. Nothing is added to the
// |localized_state_names_reverse_lookup_map_| in this case
class AlternativeStateNameMap {
public:
// Represents ISO 3166-1 alpha-2 codes (always uppercase ASCII).
using CountryCode = util::StrongAlias<class CountryCodeTag, std::string>;
// Represents either a canonical state name, or an abbreviation, or an
// alternative name or normalized state name from the profile.
using StateName = util::StrongAlias<class StateNameTag, base::string16>;
// States can be represented as different strings (different spellings,
// translations, abbreviations). All representations of a single state in a
// single country are mapped to the same canonical name.
using CanonicalStateName =
util::StrongAlias<class CanonicalStateNameTag, base::string16>;
static AlternativeStateNameMap* GetInstance();
~AlternativeStateNameMap() = delete;
AlternativeStateNameMap(const AlternativeStateNameMap&) = delete;
AlternativeStateNameMap& operator=(const AlternativeStateNameMap&) = delete;
// Removes |kCharsToStrip| from |text| and returns the normalized text.
static StateName NormalizeStateName(const StateName& text);
// Returns the canonical name (StateEntry::canonical_name) from the
// |localized_state_names_map_| based on
// (|country_code|, |state_name|).
base::Optional<CanonicalStateName> GetCanonicalStateName(
const CountryCode& country_code,
const StateName& state_name,
bool is_state_name_normalized = false) const;
// Returns the value present in |localized_state_names_map_| corresponding
// to (|country_code|, |state_string_from_profile|). In case, the entry does
// not exist in the map, base::nullopt is returned.
base::Optional<StateEntry> GetEntry(
const CountryCode& country_code,
const StateName& state_string_from_profile) const;
// Adds ((|country_code|, state key), |state_entry|) to the
// |localized_state_names_map_|, where state key corresponds to
// |normalized_canonical_state_name| if it is not null, or to
// |normalized_state_value_from_profile| otherwise.
// If |normalized_canonical_state_name| is not null, each entry from
// |normalized_alternative_state_names| is added as a tuple
// ((|country_code|, entry), |normalized_canonical_state_name|) to the
// |localized_state_names_reverse_lookup_map_|.
void AddEntry(
const CountryCode& country_code,
const StateName& normalized_state_value_from_profile,
const StateEntry& state_entry,
const std::vector<StateName>& normalized_alternative_state_names,
CanonicalStateName* normalized_canonical_state_name);
// Returns true if the |localized_state_names_map_| is empty.
bool IsLocalisedStateNamesMapEmpty() const;
#if defined(UNIT_TEST)
// Clears the map for testing purposes.
void ClearAlternativeStateNameMapForTesting() {
ClearAlternativeStateNameMap();
}
#endif
private:
AlternativeStateNameMap();
// Clears the |localized_state_names_map_| and
// |localized_state_names_reverse_lookup_map_|.
// Used only for testing purposes.
void ClearAlternativeStateNameMap();
// A custom comparator for the
// |localized_state_names_reverse_lookup_map_| that ignores the case
// of the string on comparisons.
struct CaseInsensitiveLessComparator {
bool operator()(const std::pair<CountryCode, StateName>& lhs,
const std::pair<CountryCode, StateName>& rhs) const {
// Compares the country codes that are always uppercase ASCII.
if (lhs.first != rhs.first)
return lhs.first.value() < rhs.first.value();
return base::i18n::ToLower(lhs.second.value()) <
base::i18n::ToLower(rhs.second.value());
}
};
// Since the constructor is private, |base::NoDestructor| must be friend to be
// allowed to construct the class.
friend class base::NoDestructor<AlternativeStateNameMap>;
// A map that stores the alternative state names. The map is keyed
// by the country_code and the canonical state name (or
// normalized_state_value_from_profile in case no canonical state name is
// known) while the value is the StateEntry object.
std::map<std::pair<CountryCode, CanonicalStateName>, StateEntry>
localized_state_names_map_;
// The map is keyed by the country_code and the abbreviation or
// canonical name or the alternative name of the state.
std::map<std::pair<CountryCode, StateName>,
CanonicalStateName,
CaseInsensitiveLessComparator>
localized_state_names_reverse_lookup_map_;
SEQUENCE_CHECKER(alternative_state_name_map_sequence_checker_);
};
} // namespace autofill
#endif // COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_H_
// Copyright 2020 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 "components/autofill/core/browser/geo/alternative_state_name_map_test_utils.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/geo/alternative_state_name_map.h"
namespace autofill {
namespace test {
void CreateFakeStateEntry(const TestStateEntry& test_state_entry,
StateEntry* state_entry) {
state_entry->set_canonical_name(test_state_entry.canonical_name);
for (const auto& abbr : test_state_entry.abbreviations)
state_entry->add_abbreviations(abbr);
for (const auto& alternative_name : test_state_entry.alternative_names)
state_entry->add_alternative_names(alternative_name);
}
void ClearAlternativeStateNameMapForTesting() {
AlternativeStateNameMap::GetInstance()
->ClearAlternativeStateNameMapForTesting();
}
void PopulateAlternativeStateNameMapForTesting(
std::string country_code,
std::string key,
std::vector<TestStateEntry> test_state_entries) {
for (const auto& test_state_entry : test_state_entries) {
StateEntry state_entry;
CreateFakeStateEntry(test_state_entry, &state_entry);
std::vector<AlternativeStateNameMap::StateName> alternatives;
AlternativeStateNameMap::CanonicalStateName canonical_state_name =
AlternativeStateNameMap::CanonicalStateName(
base::ASCIIToUTF16(test_state_entry.canonical_name));
alternatives.emplace_back(
AlternativeStateNameMap::StateName(canonical_state_name.value()));
for (const auto& abbr : test_state_entry.abbreviations)
alternatives.emplace_back(
AlternativeStateNameMap::StateName(base::ASCIIToUTF16(abbr)));
for (const auto& alternative_name : test_state_entry.alternative_names)
alternatives.emplace_back(AlternativeStateNameMap::StateName(
base::ASCIIToUTF16(alternative_name)));
AlternativeStateNameMap::GetInstance()->AddEntry(
AlternativeStateNameMap::CountryCode(country_code),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16(key)),
state_entry, alternatives, &canonical_state_name);
}
}
} // namespace test
} // namespace autofill
// Copyright 2020 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 COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_TEST_UTILS_H_
#define COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_TEST_UTILS_H_
#include "base/optional.h"
#include "components/autofill/core/browser/proto/states.pb.h"
namespace autofill {
namespace test {
namespace internal {
template <typename = void>
struct TestStateEntry {
std::string canonical_name = "Bavaria";
std::vector<std::string> abbreviations = {"BY"};
std::vector<std::string> alternative_names = {"Bayern"};
};
} // namespace internal
using TestStateEntry = internal::TestStateEntry<>;
// Creates a fake |StateEntry|.
void CreateFakeStateEntry(const TestStateEntry& test_state_entry,
StateEntry* state_entry);
// Clears the map for testing purposes.
void ClearAlternativeStateNameMapForTesting();
// Inserts a fake |StateEntry| object into AlternativeStateNameMap for testing.
void PopulateAlternativeStateNameMapForTesting(
std::string country_code = "DE",
std::string key = "Bavaria",
std::vector<TestStateEntry> test_state_entries = {TestStateEntry()});
} // namespace test
} // namespace autofill
#endif // COMPONENTS_AUTOFILL_CORE_BROWSER_GEO_ALTERNATIVE_STATE_NAME_MAP_TEST_UTILS_H_
// Copyright 2020 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 "components/autofill/core/browser/geo/alternative_state_name_map.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/geo/alternative_state_name_map_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace autofill {
namespace test {
// Tests that map is not empty when an entry has been added to it.
TEST(AlternativeStateNameMapTest, IsEntryAddedToMap) {
test::ClearAlternativeStateNameMapForTesting();
test::PopulateAlternativeStateNameMapForTesting();
EXPECT_FALSE(
AlternativeStateNameMap::GetInstance()->IsLocalisedStateNamesMapEmpty());
}
// Tests that the state canonical name is present when an entry is added to
// the map.
TEST(AlternativeStateNameMapTest, StateCanonicalString) {
test::ClearAlternativeStateNameMapForTesting();
test::PopulateAlternativeStateNameMapForTesting();
AlternativeStateNameMap* alternative_state_name_map =
AlternativeStateNameMap::GetInstance();
const char* const kValidMatches[] = {"Bavaria", "BY", "Bayern", "by",
"BAVARIA", "B.Y", "BAYern", "B-Y"};
for (const char* valid_match : kValidMatches) {
SCOPED_TRACE(valid_match);
EXPECT_NE(alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode("DE"),
AlternativeStateNameMap::StateName(
base::ASCIIToUTF16(valid_match))),
base::nullopt);
}
EXPECT_EQ(
alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode("US"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16("Bavaria"))),
base::nullopt);
EXPECT_EQ(alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode("DE"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16(""))),
base::nullopt);
EXPECT_EQ(alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode(""),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16(""))),
base::nullopt);
}
// Tests that the separate entries are created in the map for the different
// country codes.
TEST(AlternativeStateNameMapTest, SeparateEntryForDifferentCounties) {
test::ClearAlternativeStateNameMapForTesting();
test::PopulateAlternativeStateNameMapForTesting("DE");
test::PopulateAlternativeStateNameMapForTesting("US");
AlternativeStateNameMap* alternative_state_name_map =
AlternativeStateNameMap::GetInstance();
EXPECT_NE(
alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode("DE"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16("Bavaria"))),
base::nullopt);
EXPECT_NE(
alternative_state_name_map->GetCanonicalStateName(
AlternativeStateNameMap::CountryCode("US"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16("Bavaria"))),
base::nullopt);
}
// Tests that |AlternativeStateNameMap::NormalizeStateName()| removes "-", " "
// and "." from the text.
TEST(AlternativeStateNameMapTest, StripText) {
struct {
const char* test_string;
const char* expected;
} test_cases[] = {{"B.Y", "BY"},
{"The Golden Sun", "TheGoldenSun"},
{"Bavaria - BY", "BavariaBY"}};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message() << "test_string: " << test_case.test_string
<< " | expected: " << test_case.expected);
AlternativeStateNameMap::StateName text =
AlternativeStateNameMap::StateName(
base::ASCIIToUTF16(test_case.test_string));
EXPECT_EQ(AlternativeStateNameMap::NormalizeStateName(text).value(),
base::ASCIIToUTF16(test_case.expected));
}
}
// Tests that the correct entries are returned when the maps in
// AlternativeStateNameMap are queried.
TEST(AlternativeStateNameMapTest, GetEntry) {
test::ClearAlternativeStateNameMapForTesting();
test::PopulateAlternativeStateNameMapForTesting();
AlternativeStateNameMap* alternative_state_name_map =
AlternativeStateNameMap::GetInstance();
EXPECT_EQ(
alternative_state_name_map->GetEntry(
AlternativeStateNameMap::CountryCode("DE"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16("Random"))),
base::nullopt);
auto entry = alternative_state_name_map->GetEntry(
AlternativeStateNameMap::CountryCode("DE"),
AlternativeStateNameMap::StateName(base::ASCIIToUTF16("Bavaria")));
EXPECT_NE(entry, base::nullopt);
ASSERT_TRUE(entry->has_canonical_name());
EXPECT_EQ(entry->canonical_name(), "Bavaria");
EXPECT_THAT(entry->abbreviations(),
testing::UnorderedElementsAreArray({"BY"}));
EXPECT_THAT(entry->alternative_names(),
testing::UnorderedElementsAreArray({"Bayern"}));
}
} // namespace test
} // namespace autofill
......@@ -11,6 +11,7 @@ fuzzable_proto_library("proto") {
"password_requirements.proto",
"password_requirements_shard.proto",
"server.proto",
"states.proto",
"strike_data.proto",
]
}
// Copyright 2020 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.
syntax = "proto2";
package autofill;
option optimize_for = LITE_RUNTIME;
message StateEntry {
// Full name representing the state entry unique within a country.
// Example:
// "California" for "CA", "California", "The Golden State".
// "Bavaria" for "BY", "Bavaria", "Bayern".
optional string canonical_name = 1;
// Abbreviations corresponding to the state entry.
repeated string abbreviations = 2;
// Alternative names of the state.
repeated string alternative_names = 3;
}
message StatesInCountry {
// Two digit country code.
optional string country_code = 1;
// All the states belonging to the country.
repeated StateEntry states = 2;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment