diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs
index 59a61a220f..6e27170196 100644
--- a/util/DbSeederUtility/Program.cs
+++ b/util/DbSeederUtility/Program.cs
@@ -58,7 +58,9 @@ public class Program
[Option('m', "mix-user-statuses", Description = "Use realistic status mix (85% confirmed, 5% each invited/accepted/revoked). Requires >= 10 users.")]
bool mixStatuses = true,
[Option('o', "org-structure", Description = "Org structure for collections: Traditional, Spotify, or Modern")]
- string? structure = null
+ string? structure = null,
+ [Option('r', "region", Description = "Geographic region for names: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")]
+ string? region = null
)
{
if (users < 1)
@@ -77,6 +79,7 @@ public class Program
}
var structureModel = ParseOrgStructure(structure);
+ var geographicRegion = ParseGeographicRegion(region);
var services = new ServiceCollection();
ServiceCollectionExtension.ConfigureServices(services);
@@ -99,7 +102,8 @@ public class Program
Ciphers = ciphers,
Groups = groups,
RealisticStatusMix = mixStatuses,
- StructureModel = structureModel
+ StructureModel = structureModel,
+ Region = geographicRegion
});
}
@@ -118,4 +122,24 @@ public class Program
_ => throw new ArgumentException($"Unknown structure '{structure}'. Use: Traditional, Spotify, or Modern")
};
}
+
+ private static GeographicRegion? ParseGeographicRegion(string? region)
+ {
+ if (string.IsNullOrEmpty(region))
+ {
+ return null;
+ }
+
+ return region.ToLowerInvariant() switch
+ {
+ "northamerica" => GeographicRegion.NorthAmerica,
+ "europe" => GeographicRegion.Europe,
+ "asiapacific" => GeographicRegion.AsiaPacific,
+ "latinamerica" => GeographicRegion.LatinAmerica,
+ "middleeast" => GeographicRegion.MiddleEast,
+ "africa" => GeographicRegion.Africa,
+ "global" => GeographicRegion.Global,
+ _ => throw new ArgumentException($"Unknown region '{region}'. Use: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")
+ };
+ }
}
diff --git a/util/Seeder/Data/BogusNameProvider.cs b/util/Seeder/Data/BogusNameProvider.cs
new file mode 100644
index 0000000000..4a41b6b120
--- /dev/null
+++ b/util/Seeder/Data/BogusNameProvider.cs
@@ -0,0 +1,78 @@
+using Bit.Seeder.Data.Enums;
+using Bogus;
+using Bogus.DataSets;
+
+namespace Bit.Seeder.Data;
+
+///
+/// Provides locale-aware name generation using the Bogus library.
+/// Maps GeographicRegion to appropriate Bogus locales for culturally-appropriate names.
+///
+internal sealed class BogusNameProvider
+{
+ private readonly Faker _faker;
+
+ public BogusNameProvider(GeographicRegion region, int? seed = null)
+ {
+ var locale = MapRegionToLocale(region, seed);
+ _faker = seed.HasValue
+ ? new Faker(locale) { Random = new Randomizer(seed.Value) }
+ : new Faker(locale);
+ }
+
+ public string FirstName() => _faker.Name.FirstName();
+
+ public string FirstName(Name.Gender gender) => _faker.Name.FirstName(gender);
+
+ public string LastName() => _faker.Name.LastName();
+
+ private static string MapRegionToLocale(GeographicRegion region, int? seed) => region switch
+ {
+ GeographicRegion.NorthAmerica => "en_US",
+ GeographicRegion.Europe => GetRandomEuropeanLocale(seed),
+ GeographicRegion.AsiaPacific => GetRandomAsianLocale(seed),
+ GeographicRegion.LatinAmerica => GetRandomLatinAmericanLocale(seed),
+ GeographicRegion.MiddleEast => GetRandomMiddleEastLocale(seed),
+ GeographicRegion.Africa => GetRandomAfricanLocale(seed),
+ GeographicRegion.Global => "en",
+ _ => "en"
+ };
+
+ private static string GetRandomEuropeanLocale(int? seed)
+ {
+ var locales = new[] { "en_GB", "de", "fr", "es", "it", "nl", "pl", "pt_PT", "sv" };
+ return PickLocale(locales, seed);
+ }
+
+ private static string GetRandomAsianLocale(int? seed)
+ {
+ var locales = new[] { "ja", "ko", "zh_CN", "zh_TW", "vi" };
+ return PickLocale(locales, seed);
+ }
+
+ private static string GetRandomLatinAmericanLocale(int? seed)
+ {
+ var locales = new[] { "es_MX", "pt_BR", "es" };
+ return PickLocale(locales, seed);
+ }
+
+ private static string GetRandomMiddleEastLocale(int? seed)
+ {
+ // Bogus has limited Middle East support; use available Arabic/Turkish locales
+ var locales = new[] { "ar", "tr", "fa" };
+ return PickLocale(locales, seed);
+ }
+
+ private static string GetRandomAfricanLocale(int? seed)
+ {
+ // Bogus has limited African support; use South African English and French (West Africa)
+ var locales = new[] { "en_ZA", "fr" };
+ return PickLocale(locales, seed);
+ }
+
+ private static string PickLocale(string[] locales, int? seed)
+ {
+ var random = seed.HasValue ? new Random(seed.Value) : Random.Shared;
+ return locales[random.Next(locales.Length)];
+ }
+}
diff --git a/util/Seeder/Data/UsernameGenerator.cs b/util/Seeder/Data/CipherUsernameGenerator.cs
similarity index 75%
rename from util/Seeder/Data/UsernameGenerator.cs
rename to util/Seeder/Data/CipherUsernameGenerator.cs
index 42250bc7cd..21a726a8ff 100644
--- a/util/Seeder/Data/UsernameGenerator.cs
+++ b/util/Seeder/Data/CipherUsernameGenerator.cs
@@ -4,9 +4,13 @@ namespace Bit.Seeder.Data;
///
/// Generates deterministic usernames for companies using configurable patterns.
+/// Uses Bogus library for locale-aware name generation while maintaining determinism
+/// through pre-generated arrays indexed by a seed.
///
-internal sealed class UsernameGenerator
+internal sealed class CipherUsernameGenerator
{
+ private const int _namePoolSize = 1500;
+
private readonly Random _random;
private readonly UsernamePattern _pattern;
@@ -15,7 +19,7 @@ internal sealed class UsernameGenerator
private readonly string[] _lastNames;
- public UsernameGenerator(
+ public CipherUsernameGenerator(
int seed,
UsernamePatternType patternType = UsernamePatternType.FirstDotLast,
GeographicRegion? region = null)
@@ -23,12 +27,10 @@ internal sealed class UsernameGenerator
_random = new Random(seed);
_pattern = UsernamePatterns.GetPattern(patternType);
- (_firstNames, _lastNames) = region switch
- {
- GeographicRegion.NorthAmerica => (Names.UsFirstNames, Names.UsLastNames),
- GeographicRegion.Europe => (Names.EuropeanFirstNames, Names.EuropeanLastNames),
- _ => (Names.AllFirstNames, Names.AllLastNames)
- };
+ // Pre-generate arrays from Bogus for deterministic index-based access
+ var provider = new BogusNameProvider(region ?? GeographicRegion.Global, seed);
+ _firstNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.FirstName()).ToArray();
+ _lastNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.LastName()).ToArray();
}
public string Generate(Company company)
diff --git a/util/Seeder/Data/Names.cs b/util/Seeder/Data/Names.cs
deleted file mode 100644
index dc493cd0e9..0000000000
--- a/util/Seeder/Data/Names.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-namespace Bit.Seeder.Data;
-
-///
-/// First and last names organized by region for username generation.
-/// Add new regions by creating arrays and including them in the All* properties.
-///
-internal static class Names
-{
- public static readonly string[] UsFirstNames =
- [
- // Male
- "James", "Robert", "John", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Christopher",
- "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua",
- "Kenneth", "Kevin", "Brian", "George", "Timothy", "Ronald", "Edward", "Jason", "Jeffrey", "Ryan",
- "Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon",
- "Benjamin", "Samuel", "Raymond", "Gregory", "Frank", "Alexander", "Patrick", "Jack", "Dennis", "Jerry",
- // Female
- "Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Elizabeth", "Susan", "Jessica", "Sarah", "Karen",
- "Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle",
- "Dorothy", "Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", "Cynthia",
- "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", "Emma", "Nicole", "Helen",
- "Samantha", "Katherine", "Christine", "Debra", "Rachel", "Carolyn", "Janet", "Catherine", "Maria", "Heather"
- ];
-
- public static readonly string[] UsLastNames =
- [
- "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
- "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
- "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson",
- "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores",
- "Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts",
- "Gomez", "Phillips", "Evans", "Turner", "Diaz", "Parker", "Cruz", "Edwards", "Collins", "Reyes",
- "Stewart", "Morris", "Morales", "Murphy", "Cook", "Rogers", "Gutierrez", "Ortiz", "Morgan", "Cooper",
- "Peterson", "Bailey", "Reed", "Kelly", "Howard", "Ramos", "Kim", "Cox", "Ward", "Richardson",
- "Watson", "Brooks", "Chavez", "Wood", "James", "Bennett", "Gray", "Mendoza", "Ruiz", "Hughes",
- "Price", "Alvarez", "Castillo", "Sanders", "Patel", "Myers", "Long", "Ross", "Foster", "Jimenez"
- ];
-
- public static readonly string[] EuropeanFirstNames =
- [
- // British
- "Oliver", "George", "Harry", "Jack", "Charlie", "Thomas", "Oscar", "William", "James", "Henry",
- "Olivia", "Amelia", "Isla", "Ava", "Emily", "Sophie", "Grace", "Mia", "Poppy", "Ella",
- // German
- "Maximilian", "Alexander", "Paul", "Leon", "Lukas", "Felix", "Noah", "Elias", "Ben", "Finn",
- "Emma", "Hannah", "Mia", "Sofia", "Anna", "Emilia", "Lena", "Marie", "Lea", "Clara",
- // French
- "Gabriel", "Raphael", "Leo", "Louis", "Lucas", "Adam", "Hugo", "Jules", "Arthur", "Nathan",
- "Louise", "Alice", "Chloe", "Ines", "Lea", "Manon", "Rose", "Anna", "Lina", "Mila",
- // Spanish
- "Hugo", "Martin", "Lucas", "Daniel", "Pablo", "Alejandro", "Adrian", "Alvaro", "David", "Mario",
- "Lucia", "Sofia", "Maria", "Martina", "Paula", "Julia", "Daniela", "Valeria", "Alba", "Emma",
- // Italian
- "Leonardo", "Francesco", "Alessandro", "Lorenzo", "Mattia", "Andrea", "Gabriele", "Riccardo", "Tommaso", "Edoardo",
- "Sofia", "Giulia", "Aurora", "Alice", "Ginevra", "Emma", "Giorgia", "Greta", "Beatrice", "Anna"
- ];
-
- public static readonly string[] EuropeanLastNames =
- [
- // British
- "Smith", "Jones", "Williams", "Brown", "Taylor", "Davies", "Wilson", "Evans", "Thomas", "Johnson",
- "Roberts", "Walker", "Wright", "Robinson", "Thompson", "White", "Hughes", "Edwards", "Green", "Hall",
- // German
- "Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann",
- "Schaefer", "Koch", "Bauer", "Richter", "Klein", "Wolf", "Schroeder", "Neumann", "Schwarz", "Zimmermann",
- // French
- "Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau",
- "Simon", "Laurent", "Lefebvre", "Michel", "Garcia", "David", "Bertrand", "Roux", "Vincent", "Fournier",
- // Spanish
- "Garcia", "Rodriguez", "Martinez", "Lopez", "Sanchez", "Gonzalez", "Perez", "Martin", "Gomez", "Ruiz",
- "Hernandez", "Jimenez", "Diaz", "Moreno", "Alvarez", "Munoz", "Romero", "Alonso", "Gutierrez", "Navarro",
- // Italian
- "Rossi", "Russo", "Ferrari", "Esposito", "Bianchi", "Romano", "Colombo", "Ricci", "Marino", "Greco",
- "Bruno", "Gallo", "Conti", "DeLuca", "Costa", "Giordano", "Mancini", "Rizzo", "Lombardi", "Moretti"
- ];
-
- public static readonly string[] AllFirstNames = [.. UsFirstNames, .. EuropeanFirstNames];
-
- public static readonly string[] AllLastNames = [.. UsLastNames, .. EuropeanLastNames];
-}
diff --git a/util/Seeder/Options/OrganizationVaultOptions.cs b/util/Seeder/Options/OrganizationVaultOptions.cs
index 0ebe79cf09..ff1be02f7c 100644
--- a/util/Seeder/Options/OrganizationVaultOptions.cs
+++ b/util/Seeder/Options/OrganizationVaultOptions.cs
@@ -54,4 +54,10 @@ public class OrganizationVaultOptions
/// (25% VeryWeak, 30% Weak, 25% Fair, 15% Strong, 5% VeryStrong).
///
public PasswordStrength PasswordStrength { get; init; } = PasswordStrength.Realistic;
+
+ ///
+ /// Geographic region for culturally-appropriate name generation in cipher usernames.
+ /// Defaults to Global (mixed locales from all regions).
+ ///
+ public GeographicRegion? Region { get; init; }
}
diff --git a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs
index b183fc71d3..43ea0aa156 100644
--- a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs
+++ b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs
@@ -96,7 +96,7 @@ public class OrganizationWithVaultRecipe(
var collectionIds = CreateCollections(organization.Id, orgKeys.Key, options.StructureModel, confirmedOrgUserIds);
CreateGroups(organization.Id, options.Groups, confirmedOrgUserIds);
- CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength);
+ CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength, options.Region);
return organization.Id;
}
@@ -170,10 +170,11 @@ public class OrganizationWithVaultRecipe(
List collectionIds,
int cipherCount,
UsernamePatternType usernamePattern,
- PasswordStrength passwordStrength)
+ PasswordStrength passwordStrength,
+ GeographicRegion? region)
{
var companies = Companies.All;
- var usernameGenerator = new UsernameGenerator(organizationId.GetHashCode(), usernamePattern);
+ var usernameGenerator = new CipherUsernameGenerator(organizationId.GetHashCode(), usernamePattern, region);
var cipherList = Enumerable.Range(0, cipherCount)
.Select(i =>
diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj
index fd6e26c1ee..b38c2cf1e1 100644
--- a/util/Seeder/Seeder.csproj
+++ b/util/Seeder/Seeder.csproj
@@ -19,6 +19,10 @@
+
+
+
+