Pseudolocale support for grammatical gender

Default behavior is generate grammatical gender strings with different
prefix when pseudolocale is enabled. Also extend two specifiers to
support generate grammatical gender string for a specific gender and for
a spefific ratio. go/pseudolocale-support-for-grammatical-gender

Bug: b/272626712
Test: Added and verified affected atests pass
Change-Id: I6b7514425898facb68f0b1f6fb09e4f87978c03d
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp
index 09a8560..8143052 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp
@@ -16,11 +16,15 @@
 
 #include "compile/PseudolocaleGenerator.h"
 
+#include <stdint.h>
+
 #include <algorithm>
+#include <random>
 
 #include "ResourceTable.h"
 #include "ResourceValues.h"
 #include "ValueVisitor.h"
+#include "androidfw/ResourceTypes.h"
 #include "androidfw/Util.h"
 #include "compile/Pseudolocalizer.h"
 #include "util/Util.h"
@@ -293,8 +297,85 @@
   Pseudolocalizer localizer_;
 };
 
+class GrammaticalGenderVisitor : public ValueVisitor {
+ public:
+  std::unique_ptr<Value> value;
+  std::unique_ptr<Item> item;
+
+  GrammaticalGenderVisitor(android::StringPool* pool, uint8_t grammaticalInflection)
+      : pool_(pool), grammaticalInflection_(grammaticalInflection) {
+  }
+
+  void Visit(Plural* plural) override {
+    CloningValueTransformer cloner(pool_);
+    std::unique_ptr<Plural> grammatical_gendered = util::make_unique<Plural>();
+    for (size_t i = 0; i < plural->values.size(); i++) {
+      if (plural->values[i]) {
+        GrammaticalGenderVisitor sub_visitor(pool_, grammaticalInflection_);
+        plural->values[i]->Accept(&sub_visitor);
+        if (sub_visitor.item) {
+          grammatical_gendered->values[i] = std::move(sub_visitor.item);
+        } else {
+          grammatical_gendered->values[i] = plural->values[i]->Transform(cloner);
+        }
+      }
+    }
+    grammatical_gendered->SetSource(plural->GetSource());
+    grammatical_gendered->SetWeak(true);
+    value = std::move(grammatical_gendered);
+  }
+
+  std::string AddGrammaticalGenderPrefix(const std::string_view& original_string) {
+    std::string result;
+    switch (grammaticalInflection_) {
+      case android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE:
+        result = std::string("(M)") + std::string(original_string);
+        break;
+      case android::ResTable_config::GRAMMATICAL_GENDER_FEMININE:
+        result = std::string("(F)") + std::string(original_string);
+        break;
+      case android::ResTable_config::GRAMMATICAL_GENDER_NEUTER:
+        result = std::string("(N)") + std::string(original_string);
+        break;
+      default:
+        result = std::string(original_string);
+        break;
+    }
+    return result;
+  }
+
+  void Visit(String* string) override {
+    std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(*string->value));
+    std::unique_ptr<String> grammatical_gendered =
+        util::make_unique<String>(pool_->MakeRef(prefixed_string));
+    grammatical_gendered->SetSource(string->GetSource());
+    grammatical_gendered->SetWeak(true);
+    item = std::move(grammatical_gendered);
+  }
+
+  void Visit(StyledString* string) override {
+    std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(string->value->value));
+    android::StyleString new_string;
+    new_string.str = std::move(prefixed_string);
+    for (const android::StringPool::Span& span : string->value->spans) {
+      new_string.spans.emplace_back(android::Span{*span.name, span.first_char, span.last_char});
+    }
+    std::unique_ptr<StyledString> grammatical_gendered =
+        util::make_unique<StyledString>(pool_->MakeRef(new_string));
+    grammatical_gendered->SetSource(string->GetSource());
+    grammatical_gendered->SetWeak(true);
+    item = std::move(grammatical_gendered);
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(GrammaticalGenderVisitor);
+  android::StringPool* pool_;
+  uint8_t grammaticalInflection_;
+};
+
 ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base,
-                                              Pseudolocalizer::Method m) {
+                                              Pseudolocalizer::Method m,
+                                              uint8_t grammaticalInflection) {
   ConfigDescription modified = base;
   switch (m) {
     case Pseudolocalizer::Method::kAccent:
@@ -313,12 +394,64 @@
     default:
       break;
   }
+  modified.grammaticalInflection = grammaticalInflection;
   return modified;
 }
 
+void GrammaticalGender(ResourceConfigValue* original_value,
+                       ResourceConfigValue* localized_config_value, android::StringPool* pool,
+                       ResourceEntry* entry, const Pseudolocalizer::Method method,
+                       uint8_t grammaticalInflection) {
+  GrammaticalGenderVisitor visitor(pool, grammaticalInflection);
+  localized_config_value->value->Accept(&visitor);
+
+  std::unique_ptr<Value> grammatical_gendered_value;
+  if (visitor.value) {
+    grammatical_gendered_value = std::move(visitor.value);
+  } else if (visitor.item) {
+    grammatical_gendered_value = std::move(visitor.item);
+  }
+  if (!grammatical_gendered_value) {
+    return;
+  }
+
+  ConfigDescription config =
+      ModifyConfigForPseudoLocale(original_value->config, method, grammaticalInflection);
+
+  ResourceConfigValue* grammatical_gendered_config_value =
+      entry->FindOrCreateValue(config, original_value->product);
+  if (!grammatical_gendered_config_value->value) {
+    // Only use auto-generated pseudo-localization if none is defined.
+    grammatical_gendered_config_value->value = std::move(grammatical_gendered_value);
+  }
+}
+
+const uint32_t MASK_MASCULINE = 1;  // Bit mask for masculine
+const uint32_t MASK_FEMININE = 2;   // Bit mask for feminine
+const uint32_t MASK_NEUTER = 4;     // Bit mask for neuter
+
+void GrammaticalGenderIfNeeded(ResourceConfigValue* original_value, ResourceConfigValue* new_value,
+                               android::StringPool* pool, ResourceEntry* entry,
+                               const Pseudolocalizer::Method method, uint32_t gender_state) {
+  if (gender_state & MASK_FEMININE) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_FEMININE);
+  }
+
+  if (gender_state & MASK_MASCULINE) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE);
+  }
+
+  if (gender_state & MASK_NEUTER) {
+    GrammaticalGender(original_value, new_value, pool, entry, method,
+                      android::ResTable_config::GRAMMATICAL_GENDER_NEUTER);
+  }
+}
+
 void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method,
                             ResourceConfigValue* original_value, android::StringPool* pool,
-                            ResourceEntry* entry) {
+                            ResourceEntry* entry, uint32_t gender_state, bool gender_flag) {
   Visitor visitor(pool, method);
   original_value->value->Accept(&visitor);
 
@@ -333,8 +466,8 @@
     return;
   }
 
-  ConfigDescription config_with_accent =
-      ModifyConfigForPseudoLocale(original_value->config, method);
+  ConfigDescription config_with_accent = ModifyConfigForPseudoLocale(
+      original_value->config, method, android::ResTable_config::GRAMMATICAL_GENDER_ANY);
 
   ResourceConfigValue* new_config_value =
       entry->FindOrCreateValue(config_with_accent, original_value->product);
@@ -342,6 +475,9 @@
     // Only use auto-generated pseudo-localization if none is defined.
     new_config_value->value = std::move(localized_value);
   }
+  if (gender_flag) {
+    GrammaticalGenderIfNeeded(original_value, new_config_value, pool, entry, method, gender_state);
+  }
 }
 
 // A value is pseudolocalizable if it does not define a locale (or is the default locale) and is
@@ -356,16 +492,71 @@
 
 }  // namespace
 
+bool ParseGenderValuesAndSaveState(const std::string& grammatical_gender_values,
+                                   uint32_t* gender_state, android::IDiagnostics* diag) {
+  std::vector<std::string> values = util::SplitAndLowercase(grammatical_gender_values, ',');
+  for (size_t i = 0; i < values.size(); i++) {
+    if (values[i].length() != 0) {
+      if (values[i] == "f") {
+        *gender_state |= MASK_FEMININE;
+      } else if (values[i] == "m") {
+        *gender_state |= MASK_MASCULINE;
+      } else if (values[i] == "n") {
+        *gender_state |= MASK_NEUTER;
+      } else {
+        diag->Error(android::DiagMessage() << "Invalid grammatical gender value: " << values[i]);
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+bool ParseGenderRatio(const std::string& grammatical_gender_ratio, float* gender_ratio,
+                      android::IDiagnostics* diag) {
+  const char* input = grammatical_gender_ratio.c_str();
+  char* endPtr;
+  errno = 0;
+  *gender_ratio = strtof(input, &endPtr);
+  if (endPtr == input || *endPtr != '\0' || errno == ERANGE || *gender_ratio < 0 ||
+      *gender_ratio > 1) {
+    diag->Error(android::DiagMessage()
+                << "Invalid grammatical gender ratio: " << grammatical_gender_ratio
+                << ", must be a real number between 0 and 1");
+    return false;
+  }
+  return true;
+}
+
 bool PseudolocaleGenerator::Consume(IAaptContext* context, ResourceTable* table) {
+  uint32_t gender_state = 0;
+  if (!ParseGenderValuesAndSaveState(grammatical_gender_values_, &gender_state,
+                                     context->GetDiagnostics())) {
+    return false;
+  }
+
+  float gender_ratio = 0;
+  if (!ParseGenderRatio(grammatical_gender_ratio_, &gender_ratio, context->GetDiagnostics())) {
+    return false;
+  }
+
+  std::random_device rd;
+  std::mt19937 gen(rd());
+  std::uniform_real_distribution<> distrib(0.0, 1.0);
+
   for (auto& package : table->packages) {
     for (auto& type : package->types) {
       for (auto& entry : type->entries) {
+        bool gender_flag = false;
+        if (distrib(gen) < gender_ratio) {
+          gender_flag = true;
+        }
         std::vector<ResourceConfigValue*> values = entry->FindValuesIf(IsPseudolocalizable);
         for (ResourceConfigValue* value : values) {
           PseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value, &table->string_pool,
-                                 entry.get());
+                                 entry.get(), gender_state, gender_flag);
           PseudolocalizeIfNeeded(Pseudolocalizer::Method::kBidi, value, &table->string_pool,
-                                 entry.get());
+                                 entry.get(), gender_state, gender_flag);
         }
       }
     }
diff --git a/tools/aapt2/compile/PseudolocaleGenerator.h b/tools/aapt2/compile/PseudolocaleGenerator.h
index 44e6e3e..ce92008 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.h
+++ b/tools/aapt2/compile/PseudolocaleGenerator.h
@@ -27,8 +27,19 @@
                                                          Pseudolocalizer::Method method,
                                                          android::StringPool* pool);
 
-struct PseudolocaleGenerator : public IResourceTableConsumer {
-  bool Consume(IAaptContext* context, ResourceTable* table) override;
+class PseudolocaleGenerator : public IResourceTableConsumer {
+ public:
+  explicit PseudolocaleGenerator(std::string grammatical_gender_values,
+                                 std::string grammatical_gender_ratio)
+      : grammatical_gender_values_(std::move(grammatical_gender_values)),
+        grammatical_gender_ratio_(std::move(grammatical_gender_ratio)) {
+  }
+
+  bool Consume(IAaptContext* context, ResourceTable* table);
+
+ private:
+  std::string grammatical_gender_values_;
+  std::string grammatical_gender_ratio_;
 };
 
 }  // namespace aapt
diff --git a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
index 2f90cbf..1477ebf 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
@@ -197,7 +197,7 @@
   val->SetTranslatable(false);
 
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   // Normal pseudolocalization should take place.
@@ -249,7 +249,7 @@
   expected->values = {util::make_unique<String>(table->string_pool.MakeRef("[žéŕö one]")),
                       util::make_unique<String>(table->string_pool.MakeRef("[öñé one]"))};
 
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   const auto* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo",
@@ -287,7 +287,7 @@
                                    context->GetDiagnostics()));
   }
 
-  PseudolocaleGenerator generator;
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
   ASSERT_TRUE(generator.Consume(context.get(), table.get()));
 
   StyledString* new_styled_string = test::GetValueForConfig<StyledString>(
@@ -305,4 +305,213 @@
   EXPECT_NE(std::string::npos, new_string->value->find("world"));
 }
 
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForString) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  String* locale = test::GetValueForConfig<String>(table.get(), "android:string/foo",
+                                                   test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale);
+
+  // Grammatical gendered string
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* feminine =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine);
+  EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* masculine =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_masculine);
+  ASSERT_NE(nullptr, masculine);
+  EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* neuter =
+      test::GetValueForConfig<String>(table.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter);
+  EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value);
+}
+
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForPlural) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+  std::unique_ptr<Plural> plural = util::make_unique<Plural>();
+  plural->values = {util::make_unique<String>(table->string_pool.MakeRef("zero")),
+                    util::make_unique<String>(table->string_pool.MakeRef("one"))};
+  ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("com.pkg:plurals/foo"))
+                                     .SetValue(std::move(plural))
+                                     .Build(),
+                                 context->GetDiagnostics()));
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  Plural* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo",
+                                                   test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, actual);
+
+  // Grammatical gendered Plural
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_feminine =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_feminine);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* feminine = ValueCast<String>(actual_feminine->values[i].get());
+      EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value);
+    }
+  }
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_masculine =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_masculine);
+  ASSERT_NE(nullptr, actual_masculine);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* masculine = ValueCast<String>(actual_masculine->values[i].get());
+      EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value);
+    }
+  }
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  Plural* actual_neuter =
+      test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_neuter);
+  for (size_t i = 0; i < actual->values.size(); i++) {
+    if (actual->values[i]) {
+      String* locale = ValueCast<String>(actual->values[i].get());
+      String* neuter = ValueCast<String>(actual_neuter->values[i].get());
+      EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value);
+    }
+  }
+}
+
+TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForStyledString) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+  android::StyleString original_style;
+  original_style.str = "Hello world!";
+  original_style.spans = {android::Span{"i", 1, 10}};
+
+  std::unique_ptr<StyledString> original =
+      util::make_unique<StyledString>(table->string_pool.MakeRef(original_style));
+  ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("android:string/foo"))
+                                     .SetValue(std::move(original))
+                                     .Build(),
+                                 context->GetDiagnostics()));
+  PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0"));
+  ASSERT_TRUE(generator.Consume(context.get(), table.get()));
+
+  StyledString* locale = test::GetValueForConfig<StyledString>(table.get(), "android:string/foo",
+                                                               test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale);
+  EXPECT_EQ(1, locale->value->spans.size());
+  EXPECT_EQ(std::string("i"), *locale->value->spans[0].name);
+
+  // Grammatical gendered StyledString
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* feminine =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine);
+  EXPECT_EQ(1, feminine->value->spans.size());
+  EXPECT_EQ(std::string("i"), *feminine->value->spans[0].name);
+  EXPECT_EQ(std::string("(F)") + locale->value->value, feminine->value->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* masculine =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_masculine);
+  ASSERT_NE(nullptr, masculine);
+  EXPECT_EQ(1, masculine->value->spans.size());
+  EXPECT_EQ(std::string("i"), *masculine->value->spans[0].name);
+  EXPECT_EQ(std::string("(M)") + locale->value->value, masculine->value->value);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  StyledString* neuter =
+      test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter);
+  EXPECT_EQ(1, neuter->value->spans.size());
+  EXPECT_EQ(std::string("i"), *neuter->value->spans[0].name);
+  EXPECT_EQ(std::string("(N)") + locale->value->value, neuter->value->value);
+}
+
+TEST(PseudolocaleGeneratorTest, GrammaticalGenderForCertainValues) {
+  // single gender value
+  std::unique_ptr<ResourceTable> table_0 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_0 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_0(std::string("f"), std::string("1.0"));
+  ASSERT_TRUE(generator_0.Consume(context_0.get(), table_0.get()));
+
+  String* locale_0 = test::GetValueForConfig<String>(table_0.get(), "android:string/foo",
+                                                     test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale_0);
+
+  auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine");
+  config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* feminine_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine_0);
+  EXPECT_EQ(std::string("(F)") + *locale_0->value, *feminine_0->value);
+
+  auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine");
+  config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* masculine_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_masculine);
+  EXPECT_EQ(nullptr, masculine_0);
+
+  auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter");
+  config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY;
+  String* neuter_0 =
+      test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_neuter);
+  EXPECT_EQ(nullptr, neuter_0);
+
+  // multiple gender values
+  std::unique_ptr<ResourceTable> table_1 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_1 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_1(std::string("f,n"), std::string("1.0"));
+  ASSERT_TRUE(generator_1.Consume(context_1.get(), table_1.get()));
+
+  String* locale_1 = test::GetValueForConfig<String>(table_1.get(), "android:string/foo",
+                                                     test::ParseConfigOrDie("en-rXA"));
+  ASSERT_NE(nullptr, locale_1);
+
+  String* feminine_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_feminine);
+  ASSERT_NE(nullptr, feminine_1);
+  EXPECT_EQ(std::string("(F)") + *locale_1->value, *feminine_1->value);
+
+  String* masculine_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_masculine);
+  EXPECT_EQ(nullptr, masculine_1);
+
+  String* neuter_1 =
+      test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_neuter);
+  ASSERT_NE(nullptr, neuter_1);
+  EXPECT_EQ(std::string("(N)") + *locale_1->value, *neuter_1->value);
+
+  // invalid gender value
+  std::unique_ptr<ResourceTable> table_2 =
+      test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build();
+
+  std::unique_ptr<IAaptContext> context_2 = test::ContextBuilder().Build();
+  PseudolocaleGenerator generator_2(std::string("invald,"), std::string("1.0"));
+  ASSERT_FALSE(generator_2.Consume(context_2.get(), table_2.get()));
+}
+
 }  // namespace aapt