Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a4a59c0
fix(address): fixed generate valid state/uf pairs
wellfernandes Mar 22, 2026
be0f3d2
docs(certificate): clarify that 'lang' is unused for Brazilian certs …
wellfernandes Mar 22, 2026
7932dac
fix(translations): register full list of phone area codes for ptbr
wellfernandes Mar 22, 2026
135eeee
refactor(phone): adjusted phone data generation
wellfernandes Mar 22, 2026
c139505
feat(mocker): added phone data generation to mocker
wellfernandes Mar 22, 2026
5d3a919
feat(translations): add safe rand source for thread-safe random numbe…
wellfernandes Mar 22, 2026
c6aa7ce
refactor(mocker): use safe rand source for concurrency safety and rem…
wellfernandes Mar 22, 2026
719e5a4
fix(errors): show only localized error messages to user
wellfernandes Mar 22, 2026
b9408fe
docs: updated main readme
wellfernandes Mar 22, 2026
60d8ba7
docs: updated ptbr readme
wellfernandes Mar 22, 2026
d8a1735
fix: fixed invalid page number error in certificate generator for con…
wellfernandes Mar 22, 2026
aa15d61
docs: updated main readme
wellfernandes Mar 22, 2026
774d7fa
docs: updated ptbr readme
wellfernandes Mar 22, 2026
5229eea
remove: removed all error sentinel files no longer in use
wellfernandes Mar 22, 2026
1c9892c
refactor: removed unnecessary comments and other adjts
wellfernandes Mar 22, 2026
81f1d5f
docs: updated main readme
wellfernandes Mar 22, 2026
642963a
docs: updated ptbr readme
wellfernandes Mar 22, 2026
433609a
docs(readme): adjusted main readme
wellfernandes Mar 22, 2026
4ddc6bc
docs(readme): adjusted ptbr readme
wellfernandes Mar 22, 2026
399d968
refactor(translations): remove unused single-value phone_area_code re…
wellfernandes Mar 22, 2026
bd0fb13
refactor(voteregistration): rename and document NewBrazilianVoteRegis…
wellfernandes Mar 22, 2026
fa266cb
fix(certificate): fixed language code ptbr for error localization
wellfernandes Mar 22, 2026
42549e9
refactor(certificates): simplify method names
wellfernandes Mar 22, 2026
1b554dd
feat: added en-us translation for errors
wellfernandes Mar 22, 2026
d7ebaf9
refactor: adjusted error fallback to use en-us
wellfernandes Mar 22, 2026
7bf9f57
feat(safe-rand): added function for standard thread-safe source for g…
wellfernandes Mar 22, 2026
8bd99b7
refactor(random): standardize use of thread-safe randomness source
wellfernandes Mar 22, 2026
8eefd03
fix(translations): return defensive copy in GetUFMap to prevent exter…
wellfernandes Mar 22, 2026
7280780
refactor(vote-registration): remove unused globalRand and helper funcs
wellfernandes Mar 22, 2026
0fc06df
refactor: removed error sentinels for vote registration
wellfernandes Mar 22, 2026
dd349da
refactor: removed unused errors file
wellfernandes Mar 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,35 @@ func main() {
// Generate a mock address
address, err := mocker.NewAddress()
if err != nil {
// Error messages are localized where translation keys are available
log.Fatal(err)
}
fmt.Printf("Address: %s, %d - %s, %s (%s) - %s\n", address.Street, address.Number, address.City, address.State, address.UF, address.ZIP)

// Generate a mock person
person, err := mocker.NewPerson()
if err != nil {
// Error messages are localized where translation keys are available
log.Fatal(err)
}
fmt.Printf("Person: %s %s, Gender: %s, Age: %d, CPF: %s\n", person.FirstNameMale, person.LastName, person.Gender.Identity, person.Age, person.CPF.Number)

// Generate a mock company
company, err := mocker.NewCompany()
if err != nil {
// Error messages are localized where translation keys are available
log.Fatal(err)
}
fmt.Printf("Company: %s, CNPJ: %s\n", company.BrazilianCompany.Name, company.BrazilianCompany.CNPJ)
}

```

### Error Messages & Localization

Error messages are localized where translation keys are available. When an error occurs (e.g., invalid data, unsupported language, or generation failure), the error message will be presented in the language configured for the `Mocker` instance, if a translation exists. In some cases, fallback or hardcoded errors may occur if translation coverage is incomplete.

You do not need to perform any extra steps for error localization — Mocai handles this automatically for all supported languages where translation keys are present.
```

> **Note:** Each call to a method like `NewPerson()` or `NewAddress()` generates a new mock with random data. The `Mocker` instance is immutable regarding its configuration (language, formatting, random source).
Expand Down
11 changes: 11 additions & 0 deletions docs/localization/pt/README-PT.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,35 @@ func main() {
// Gera um endereço fictício
address, err := mocker.NewAddress()
if err != nil {
// As mensagens de erro são localizadas quando há chave de tradução disponível
log.Fatal(err)
}
fmt.Printf("Endereço: %s, %d - %s, %s (%s) - %s\n", address.Street, address.Number, address.City, address.State, address.UF, address.ZIP)

// Gera uma pessoa fictícia
person, err := mocker.NewPerson()
if err != nil {
// As mensagens de erro são localizadas quando há chave de tradução disponível
log.Fatal(err)
}
fmt.Printf("Pessoa: %s %s, Gênero: %s, Idade: %d, CPF: %s\n", person.FirstNameMale, person.LastName, person.Gender.Identity, person.Age, person.CPF.Number)

// Gera uma empresa fictícia
company, err := mocker.NewCompany()
if err != nil {
// As mensagens de erro são localizadas quando há chave de tradução disponível
log.Fatal(err)
}
fmt.Printf("Empresa: %s, CNPJ: %s\n", company.BrazilianCompany.Name, company.BrazilianCompany.CNPJ)
}

```

### Mensagens de Erro e Localização

As mensagens de erro são localizadas quando há chave de tradução disponível. Quando ocorre um erro (ex: dados inválidos, idioma não suportado ou falha na geração), a mensagem será apresentada no idioma configurado para a instância do `Mocker`, se houver tradução. Em alguns casos, mensagens fixas podem aparecer caso a cobertura de traduções não seja completa.

Não é necessário realizar nenhuma etapa extra para a localização das mensagens de erro — o Mocai faz isso automaticamente para todos os idiomas suportados onde há chave de tradução.
```

> **Nota:** Cada chamada de método como `NewPerson()` ou `NewAddress()` gera um novo mock com dados aleatórios. A instância do `Mocker` é imutável quanto à configuração (idioma, formatação, fonte de aleatoriedade).
Expand Down
12 changes: 4 additions & 8 deletions pkg/mocai/entities/address/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ package address

import "errors"

// Package address defines common errors used during address generation and validation.
// These errors represent specific failure scenarios and should be wrapped with additional
// context when returned.
var (
ErrNoStreets = errors.New("no data available for streets")
ErrNoCities = errors.New("no data available for cities")
ErrNoStates = errors.New("no data available for states")
ErrNoUFs = errors.New("no data available for UFs")
ErrNoZips = errors.New("no data available for zips")
ErrNoStreets = errors.New("address: no streets available")
ErrNoCities = errors.New("address: no cities available")
ErrNoStates = errors.New("address: no states available")
ErrNoZips = errors.New("address: no zip codes available")
)
38 changes: 29 additions & 9 deletions pkg/mocai/entities/address/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,19 @@ func NewAddress(lang string, rnd translations.RandSource) (*Address, error) {
}

func generateAddress(lang string, rnd translations.RandSource) (*Address, error) {
streets := translations.GetList(lang, "address_street")
cities := translations.GetList(lang, "address_city")
states := translations.GetList(lang, "address_state")
ufs := translations.GetList(lang, "address_uf")
zips := translations.GetList(lang, "address_zip")
supportedLang := lang
if translations.GetUFMap(lang) == nil {
supportedLang = "ptbr"
}
streets := translations.GetList(supportedLang, "address_street")
cities := translations.GetList(supportedLang, "address_city")
states := translations.GetList(supportedLang, "address_state")
zips := translations.GetList(supportedLang, "address_zip")

slicesToCheck := []SlicesToCheck{
{streets, ErrNoStreets},
{cities, ErrNoCities},
{states, ErrNoStates},
{ufs, ErrNoUFs},
{zips, ErrNoZips},
}

Expand All @@ -57,11 +59,29 @@ func generateAddress(lang string, rnd translations.RandSource) (*Address, error)
}
}

// performs a random selection using a custom random source
// select the index for state and UF in a paired manner
stateIdx := rnd.Intn(len(states))
state := states[stateIdx]

var uf string

ufMap := translations.GetUFMap(supportedLang)
if ufMap != nil {
uf = ufMap[state]
}
if uf == "" {
ufs := translations.GetList(supportedLang, "address_uf")
if len(ufs) > stateIdx {
uf = ufs[stateIdx]
} else if len(ufs) > 0 {
uf = ufs[rnd.Intn(len(ufs))]
} else {
uf = ""
}
}

street := streets[rnd.Intn(len(streets))]
city := cities[rnd.Intn(len(cities))]
state := states[rnd.Intn(len(states))]
uf := ufs[rnd.Intn(len(ufs))]
zip := zips[rnd.Intn(len(zips))]

return &Address{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,27 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert
}
vitalRecordsOffice := rnd.Intn(899999-100000+1) + 100000
if vitalRecordsOffice < 0 {
return nil, fmt.Errorf("%w: %s", ErrInvalidVitalRecordsOffice, translations.Get(l, "invalid_vital_records_office_number"))
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_vital_records_office_number"))
}
archiveCode := 1
serviceType := 55
birthYear := rnd.Intn(time.Now().Year()-2010+1) + 2010
bookNumber := rnd.Intn(89999-10000+1) + 10000
if bookNumber < 0 {
return nil, fmt.Errorf("%w, %s", ErrInvalidBookNumber, translations.Get(l, "invalid_book_number"))
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_book_number"))
}
pageNumber := rnd.Intn(899-100+1) + 100
if pageNumber < 0 {
return nil, ErrInvalidPageNumber
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_page_number"))
}
termNumber := rnd.Intn(8999999-1000000+1) + 1000000
if termNumber < 0 {
return nil, fmt.Errorf("%w, %s", ErrInvalidTermNumber, translations.Get(l, "invalid_term_number"))
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_term_number"))
}
numberWithoutCheckDigits := fmt.Sprintf("%06d%02d%02d%04d%d%05d%03d%07d",
vitalRecordsOffice, archiveCode, serviceType, birthYear, certificateType, bookNumber, pageNumber, termNumber)
if len(numberWithoutCheckDigits) != 30 {
return nil, fmt.Errorf("%w, %s", ErrInvalidNumberWithoutCheckDigits, translations.Get(l, "invalid_number_without_check_digits"))
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_number_without_check_digits"))
}
checkDigits := calculateCheckDigits(numberWithoutCheckDigits)
certificateNumber := fmt.Sprintf("%s%02s", numberWithoutCheckDigits, checkDigits)
Expand All @@ -122,7 +122,7 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert
vitalRecordsOffice, archiveCode, serviceType, birthYear, certificateType, bookNumber, pageNumber, termNumber, checkDigits)
}
if certificateNumber == "" {
return nil, fmt.Errorf("%w: %s", ErrInvalidCertificate, translations.Get(l, "invalid_certificate"))
return nil, fmt.Errorf("%s", translations.Get(l, "invalid_certificate"))
}
createdBaseCertificate := &BaseCertificate{
VitalRecordsOffice: vitalRecordsOffice,
Expand All @@ -140,6 +140,8 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert
}

func generateBirthCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*BirthCertificate, error) {
// Note: The 'lang' parameter is currently unused because certificate numbers do not depend on language.
// It will be relevant only if textual data (e.g., names, descriptions) is added in the future.
base, err := generateCertificateCustom(rnd, formatted, brazilianBirthCertificateType)
if err != nil {
return nil, err
Expand Down
18 changes: 0 additions & 18 deletions pkg/mocai/entities/certificate/countries/errors.go

This file was deleted.

7 changes: 0 additions & 7 deletions pkg/mocai/entities/cnpj/errors.go

This file was deleted.

6 changes: 3 additions & 3 deletions pkg/mocai/entities/company/countries/brazilian_company.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type BrazilianCompany struct {
func GenerateBrazilianCompany(lang string, formatted bool, rnd translations.RandSource) (BrazilianCompany, error) {
companyNames := translations.GetList(lang, "company_name")
if len(companyNames) == 0 {
return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrNoCompanyNamesAvailable, translations.Get(lang, "no_company_names_available"))
return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "no_company_names_available"))
}
if rnd == nil {
rnd = defaultRandSource()
Expand All @@ -30,10 +30,10 @@ func GenerateBrazilianCompany(lang string, formatted bool, rnd translations.Rand
return BrazilianCompany{}, err
}
if companyName == "" {
return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrGeneratingBrazilianCompany, translations.Get(lang, "error_generating_brazilian_company"))
return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "error_generating_brazilian_company"))
}
if cnpjVal == "" {
return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrGeneratingCNPJ, translations.Get(lang, "invalid_cnpj"))
return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "invalid_cnpj"))
}
createdCompany := BrazilianCompany{
Name: companyName,
Expand Down
12 changes: 0 additions & 12 deletions pkg/mocai/entities/company/countries/errors.go

This file was deleted.

7 changes: 0 additions & 7 deletions pkg/mocai/entities/cpf/errors.go

This file was deleted.

2 changes: 1 addition & 1 deletion pkg/mocai/entities/cpf/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewCPF(lang string, isFormatted bool, rnd translations.RandSource) (*CPF, e
cpfNumber := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(digits)), ""), "[]")
if isFormatted {
if len(cpfNumber) != 11 {
return nil, fmt.Errorf("%w, %s", ErrInvalidCPF, translations.Get(lang, "invalid_cpf"))
return nil, fmt.Errorf("%s", translations.Get(lang, "invalid_cpf"))
}
return &CPF{Number: cpfNumber[:3] + "." + cpfNumber[3:6] + "." + cpfNumber[6:9] + "-" + cpfNumber[9:]}, nil
}
Expand Down
11 changes: 0 additions & 11 deletions pkg/mocai/entities/gender/errors.go

This file was deleted.

2 changes: 1 addition & 1 deletion pkg/mocai/entities/gender/gender.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewGender(lang string, rnd translations.RandSource) (*Gender, error) {
func generateRandomGender(lang string, rnd translations.RandSource) (*Gender, error) {
genders := translations.GetList(lang, "gender")
if len(genders) == 0 {
return nil, fmt.Errorf("%w, %s", ErrNoGenders, translations.Get(lang, "no_data_available_for_genders"))
return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_genders"))
}
if rnd == nil {
rnd = defaultRandSource()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func calculateSPRGDigitCustom(rnd translations.RandSource, lang string) (string,
for i := range 8 {
val, err := strconv.Atoi(string(baseStr[i]))
if err != nil {
return "", fmt.Errorf("%w, %s", ErrToConvertDigit, translations.Get(lang, "error_converting_digit"))
return "", fmt.Errorf("%s", translations.Get(lang, "error_converting_digit"))
}
d[i] = val
}
Expand Down
7 changes: 0 additions & 7 deletions pkg/mocai/entities/nationalid/countries/errors.go

This file was deleted.

12 changes: 0 additions & 12 deletions pkg/mocai/entities/person/errors.go

This file was deleted.

6 changes: 3 additions & 3 deletions pkg/mocai/entities/person/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ func generatePerson(lang string, isFormatted bool, rnd translations.RandSource)
lastNames := translations.GetList(lang, "person_last_name")

if len(firstNamesMale) == 0 || len(firstNamesFemale) == 0 {
return nil, fmt.Errorf("%w, %s", ErrNoFirstNames, translations.Get(lang, "no_data_available_for_first_names"))
return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_first_names"))
}
if len(lastNames) == 0 {
return nil, fmt.Errorf("%w, %s", ErrNoLastNames, translations.Get(lang, "no_data_available_for_last_names"))
return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_last_names"))
}

if rnd == nil {
Expand All @@ -56,7 +56,7 @@ func generatePerson(lang string, isFormatted bool, rnd translations.RandSource)
}

if firstNameMale == "" || firstNameFemale == "" || lastName == "" {
return nil, fmt.Errorf("%w, %s", ErrGeneratingPerson, translations.Get(lang, "error_generating_person"))
return nil, fmt.Errorf("%s", translations.Get(lang, "error_generating_person"))
}

return &Person{
Expand Down
11 changes: 0 additions & 11 deletions pkg/mocai/entities/phone/errors.go

This file was deleted.

7 changes: 4 additions & 3 deletions pkg/mocai/entities/phone/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ func NewPhone(lang string, rnd translations.RandSource) (*Phone, error) {
func generatePhone(lang string, rnd translations.RandSource) (*Phone, error) {
areaCodes := translations.GetList(lang, "phone_area_code")
if len(areaCodes) == 0 {
return nil, fmt.Errorf("%w, %s", ErrNoAreaCodes, translations.Get(lang, "no_data_available_for_area_codes"))
return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_area_codes"))
}
if rnd == nil {
rnd = defaultRandSource()
}

areaCode := areaCodes[rnd.Intn(len(areaCodes))]
number := fmt.Sprintf("9%08d", rnd.Intn(100000000))
if areaCode == "" || number == "" {
return nil, fmt.Errorf("%w, %s", ErrGeneratingPhone, translations.Get(lang, "error_generating_phone"))
if areaCode == "" {
return nil, fmt.Errorf("%s", translations.Get(lang, "error_generating_phone"))
}
return &Phone{
AreaCode: areaCode,
Expand Down
Loading
Loading