← Back to Blog

Flutter Gender and Select Messages: ICU Format Guide for Grammatical Gender

fluttergenderselecticulocalizationinclusive

Flutter Gender and Select Messages: ICU Format Guide for Grammatical Gender

Master gender-aware translations and select messages in Flutter. Learn to handle grammatical gender, create dynamic message choices, and build inclusive apps for all languages.

Why Gender Matters in Localization

Many languages have grammatical gender that affects translations:

Language Example (Friend invited)
English "Your friend invited you" (no gender)
Spanish "Tu amigo te invitó" (male) / "Tu amiga te invitó" (female)
German "Dein Freund hat dich eingeladen" (male) / "Deine Freundin hat dich eingeladen" (female)
French "Ton ami t'a invité" (male) / "Ton amie t'a invitée" (female)

Ignoring grammatical gender makes your app feel unpolished to billions of users.

Understanding ICU Select Messages

Flutter's ARB files support ICU (International Components for Unicode) message format, which includes the select syntax for handling variations like gender.

Basic Select Syntax

{variable, select, option1{text1} option2{text2} other{default text}}

Gender Select Example

{
  "invitedBy": "{gender, select, male{He invited you} female{She invited you} other{They invited you}}",
  "@invitedBy": {
    "placeholders": {
      "gender": {
        "type": "String",
        "example": "male"
      }
    }
  }
}

Usage:

// Based on user's gender
Text(AppLocalizations.of(context)!.invitedBy('female'));
// Output: "She invited you"

Complete Gender Implementation

Step 1: Define Gender Enum

enum Gender {
  male,
  female,
  other,
}

extension GenderExtension on Gender {
  String get value {
    switch (this) {
      case Gender.male: return 'male';
      case Gender.female: return 'female';
      case Gender.other: return 'other';
    }
  }
}

Step 2: Create ARB Messages

app_en.arb:

{
  "@@locale": "en",

  "profileTitle": "{gender, select, male{His Profile} female{Her Profile} other{Their Profile}}",
  "@profileTitle": {
    "placeholders": {
      "gender": {"type": "String"}
    }
  },

  "lastActive": "{gender, select, male{He was last active {time}} female{She was last active {time}} other{They were last active {time}}}",
  "@lastActive": {
    "placeholders": {
      "gender": {"type": "String"},
      "time": {"type": "String", "example": "2 hours ago"}
    }
  }
}

app_es.arb:

{
  "@@locale": "es",

  "profileTitle": "{gender, select, male{Su Perfil} female{Su Perfil} other{Su Perfil}}",

  "lastActive": "{gender, select, male{Él estuvo activo por última vez {time}} female{Ella estuvo activa por última vez {time}} other{Estuvo activo/a por última vez {time}}}"
}

app_de.arb:

{
  "@@locale": "de",

  "profileTitle": "{gender, select, male{Sein Profil} female{Ihr Profil} other{Profil}}",

  "lastActive": "{gender, select, male{Er war zuletzt {time} aktiv} female{Sie war zuletzt {time} aktiv} other{Zuletzt {time} aktiv}}"
}

Step 3: Use in Widgets

class UserProfile extends StatelessWidget {
  final User user;

  UserProfile({required this.user});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        Text(l10n.profileTitle(user.gender.value)),
        Text(l10n.lastActive(user.gender.value, '2 hours ago')),
      ],
    );
  }
}

Select Messages Beyond Gender

The select syntax works for any categorical choice, not just gender.

User Role Selection

{
  "welcomeMessage": "{role, select, admin{Welcome, Administrator} moderator{Welcome, Moderator} member{Welcome, Member} other{Welcome}}",
  "@welcomeMessage": {
    "placeholders": {
      "role": {
        "type": "String",
        "example": "admin"
      }
    }
  }
}

Status Selection

{
  "orderStatus": "{status, select, pending{Your order is being processed} shipped{Your order has shipped} delivered{Your order was delivered} cancelled{Your order was cancelled} other{Order status unknown}}",
  "@orderStatus": {
    "placeholders": {
      "status": {
        "type": "String",
        "example": "shipped"
      }
    }
  }
}

Platform Selection

{
  "downloadButton": "{platform, select, ios{Download on App Store} android{Get it on Google Play} web{Open in Browser} other{Download}}",
  "@downloadButton": {
    "placeholders": {
      "platform": {
        "type": "String",
        "example": "ios"
      }
    }
  }
}

Combining Select with Plurals

You can nest select within plural messages (and vice versa):

Gender + Count

{
  "friendsCount": "{gender, select, male{{count, plural, =0{He has no friends} =1{He has 1 friend} other{He has {count} friends}}} female{{count, plural, =0{She has no friends} =1{She has 1 friend} other{She has {count} friends}}} other{{count, plural, =0{They have no friends} =1{They have 1 friend} other{They have {count} friends}}}}",
  "@friendsCount": {
    "placeholders": {
      "gender": {"type": "String"},
      "count": {"type": "int"}
    }
  }
}

Usage:

Text(l10n.friendsCount('female', 5));
// "She has 5 friends"

Text(l10n.friendsCount('male', 1));
// "He has 1 friend"

Simplified with Multiple Keys

For complex messages, split into separate keys for maintainability:

{
  "friendsCount_male": "{count, plural, =0{He has no friends} =1{He has 1 friend} other{He has {count} friends}}",
  "friendsCount_female": "{count, plural, =0{She has no friends} =1{She has 1 friend} other{She has {count} friends}}",
  "friendsCount_other": "{count, plural, =0{They have no friends} =1{They have 1 friend} other{They have {count} friends}}",

  "@friendsCount_male": {
    "placeholders": {"count": {"type": "int"}}
  },
  "@friendsCount_female": {
    "placeholders": {"count": {"type": "int"}}
  },
  "@friendsCount_other": {
    "placeholders": {"count": {"type": "int"}}
  }
}

With a helper method:

String getFriendsCount(BuildContext context, Gender gender, int count) {
  final l10n = AppLocalizations.of(context)!;
  switch (gender) {
    case Gender.male: return l10n.friendsCount_male(count);
    case Gender.female: return l10n.friendsCount_female(count);
    case Gender.other: return l10n.friendsCount_other(count);
  }
}

Handling Multiple Genders in Messages

When a message refers to multiple people with different genders:

{
  "sentGiftTo": "{senderGender, select, male{{recipientGender, select, male{He sent him a gift} female{He sent her a gift} other{He sent them a gift}}} female{{recipientGender, select, male{She sent him a gift} female{She sent her a gift} other{She sent them a gift}}} other{{recipientGender, select, male{They sent him a gift} female{They sent her a gift} other{They sent them a gift}}}}",
  "@sentGiftTo": {
    "placeholders": {
      "senderGender": {"type": "String"},
      "recipientGender": {"type": "String"}
    }
  }
}

Practical Alternative: Use Names

Complex gender combinations are hard to maintain. Often, using names is cleaner:

{
  "sentGiftTo": "{sender} sent {recipient} a gift",
  "@sentGiftTo": {
    "placeholders": {
      "sender": {"type": "String", "example": "Alice"},
      "recipient": {"type": "String", "example": "Bob"}
    }
  }
}

Inclusive Language Options

Non-Binary and Gender-Neutral

Always provide an other option for non-binary users or when gender is unknown:

{
  "userBio": "{gender, select, male{He is a developer} female{She is a developer} other{They are a developer}}",
  "@userBio": {
    "placeholders": {
      "gender": {"type": "String"}
    }
  }
}

Language-Specific Inclusive Forms

Some languages have specific inclusive forms:

Spanish (using -e ending):

{
  "@@locale": "es",
  "userBio": "{gender, select, male{Él es desarrollador} female{Ella es desarrolladora} other{Elle es desarrolladore}}"
}

German (using neutral constructions):

{
  "@@locale": "de",
  "userBio": "{gender, select, male{Er ist Entwickler} female{Sie ist Entwicklerin} other{Diese Person ist in der Entwicklung tätig}}"
}

Select with Named Options

Use descriptive option names for clarity:

{
  "subscriptionMessage": "{tier, select, free{You're on the Free plan} basic{You're on the Basic plan} premium{You're on Premium - thank you!} enterprise{Enterprise account} other{Unknown plan}}",
  "@subscriptionMessage": {
    "placeholders": {
      "tier": {
        "type": "String",
        "example": "premium"
      }
    }
  }
}
enum SubscriptionTier { free, basic, premium, enterprise }

Text(l10n.subscriptionMessage(user.tier.name));

Common Patterns

User Actions

{
  "userAction_like": "{gender, select, male{He liked your post} female{She liked your post} other{They liked your post}}",
  "userAction_comment": "{gender, select, male{He commented on your post} female{She commented on your post} other{They commented on your post}}",
  "userAction_share": "{gender, select, male{He shared your post} female{She shared your post} other{They shared your post}}",
  "userAction_follow": "{gender, select, male{He started following you} female{She started following you} other{They started following you}}",

  "@userAction_like": {"placeholders": {"gender": {"type": "String"}}},
  "@userAction_comment": {"placeholders": {"gender": {"type": "String"}}},
  "@userAction_share": {"placeholders": {"gender": {"type": "String"}}},
  "@userAction_follow": {"placeholders": {"gender": {"type": "String"}}}
}

Chat Messages

{
  "typing": "{gender, select, male{He is typing...} female{She is typing...} other{Typing...}}",
  "lastSeen": "{gender, select, male{He was last seen {time}} female{She was last seen {time}} other{Last seen {time}}}",
  "online": "{gender, select, male{He is online} female{She is online} other{Online}}",

  "@typing": {"placeholders": {"gender": {"type": "String"}}},
  "@lastSeen": {"placeholders": {"gender": {"type": "String"}, "time": {"type": "String"}}},
  "@online": {"placeholders": {"gender": {"type": "String"}}}
}

Achievement Messages

{
  "achievementUnlocked": "{gender, select, male{He unlocked the \"{achievement}\" badge} female{She unlocked the \"{achievement}\" badge} other{The \"{achievement}\" badge was unlocked}}",
  "@achievementUnlocked": {
    "placeholders": {
      "gender": {"type": "String"},
      "achievement": {"type": "String", "example": "Early Bird"}
    }
  }
}

Error Handling

Validate Select Values

class GenderHelper {
  static String normalize(String? input) {
    switch (input?.toLowerCase()) {
      case 'male':
      case 'm':
      case 'man':
        return 'male';
      case 'female':
      case 'f':
      case 'woman':
        return 'female';
      default:
        return 'other';
    }
  }
}

// Usage
final gender = GenderHelper.normalize(user.gender);
Text(l10n.profileTitle(gender));

Fallback for Invalid Options

The other option in select messages acts as a fallback:

// Even with invalid input, 'other' handles it
Text(l10n.profileTitle('invalid_value'));
// Output: "Their Profile" (falls back to other)

Testing Select Messages

Unit Tests

void main() {
  group('Gender select messages', () {
    late AppLocalizations l10n;

    setUp(() {
      l10n = lookupAppLocalizations(Locale('en'));
    });

    test('returns correct male form', () {
      expect(l10n.invitedBy('male'), 'He invited you');
    });

    test('returns correct female form', () {
      expect(l10n.invitedBy('female'), 'She invited you');
    });

    test('returns other form for unknown gender', () {
      expect(l10n.invitedBy('other'), 'They invited you');
      expect(l10n.invitedBy('unknown'), 'They invited you');
    });
  });
}

Widget Tests

testWidgets('displays correct gendered message', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: UserProfile(user: User(gender: Gender.female)),
    ),
  );

  expect(find.text('Her Profile'), findsOneWidget);
});

Best Practices

1. Always Include 'other'

// Good - has fallback
"{gender, select, male{His} female{Her} other{Their}}"

// Bad - will error on unknown values
"{gender, select, male{His} female{Her}}"

2. Keep Messages Simple

// Good - simple and maintainable
{
  "greeting": "Hello, {name}!",
  "userOnline": "{name} is online"
}

// Avoid - overly complex
{
  "greeting": "{gender, select, male{Hello, Mr. {name}!} female{Hello, Ms. {name}!} other{Hello, {name}!}}"
}

3. Document Gender Values

{
  "@profileTitle": {
    "description": "Title for user profile page. Gender values: 'male', 'female', 'other'",
    "placeholders": {
      "gender": {
        "type": "String",
        "example": "female"
      }
    }
  }
}

4. Consider Cultural Sensitivity

Different cultures have different approaches to gender. Some considerations:

  • Some languages have more than 2 grammatical genders (e.g., German: masculine, feminine, neuter)
  • Some cultures have additional gender categories
  • Legal names vs. display names
  • Respect user preferences from profile settings

Simplify Gender Messages with FlutterLocalisation

Managing gender variations across languages is complex. FlutterLocalisation.com helps you:

  • Visual select editing - See all gender variants side-by-side
  • AI translations - Generate grammatically correct gender forms for all languages
  • Syntax validation - Catch ICU format errors before building
  • Completeness check - Ensure all variants are translated

Stop manually editing nested ICU syntax. Try FlutterLocalisation free and handle gender elegantly in every language.

Summary

Flutter gender and select messages enable:

  1. Grammatically correct translations - Match grammatical gender requirements
  2. Inclusive language - Always include 'other' option
  3. Dynamic message selection - Beyond gender: roles, status, platforms
  4. Combined patterns - Nest select with plurals when needed
  5. Cultural sensitivity - Respect language-specific gender conventions

With proper gender handling, your Flutter app delivers natural, respectful translations to users worldwide.