Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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")
)
42 changes: 33 additions & 9 deletions pkg/mocai/entities/address/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,23 @@ 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"
}
if rnd == nil {
rnd = translations.NewSafeRandSource(translations.DefaultRandSource())
}

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 +63,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
41 changes: 20 additions & 21 deletions pkg/mocai/entities/certificate/countries/brazilian_certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package countries

import (
"fmt"
"math/rand"
"time"

"github.com/brazzcore/mocai/pkg/mocai/translations"
Expand Down Expand Up @@ -55,23 +54,23 @@ type BrazilianCertificates struct {
}

func NewBrazilCertificatesCustom(lang string, isFormatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) {
cert, err := generateBrazilianCertificatesCustom(lang, isFormatted, rnd)
cert, err := generateBrazilianCertificates(lang, isFormatted, rnd)
if err != nil {
return nil, err
}
return cert, nil
}

func generateBrazilianCertificatesCustom(lang string, formatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) {
createdBirthCertificate, err := generateBirthCertificateCustom(lang, formatted, rnd)
func generateBrazilianCertificates(lang string, formatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) {
createdBirthCertificate, err := generateBirthCertificate(lang, formatted, rnd)
if err != nil {
return nil, err
}
createdMarriageCertificate, err := generateMarriageCertificateCustom(lang, formatted, rnd)
createdMarriageCertificate, err := generateMarriageCertificate(lang, formatted, rnd)
if err != nil {
return nil, err
}
createdDeathCertificate, err := generateDeathCertificateCustom(lang, formatted, rnd)
createdDeathCertificate, err := generateDeathCertificate(lang, formatted, rnd)
if err != nil {
return nil, err
}
Expand All @@ -83,37 +82,37 @@ func generateBrazilianCertificatesCustom(lang string, formatted bool, rnd transl
return createdBrazilianCertificates, nil
}

func generateCertificateCustom(rnd translations.RandSource, formatted bool, certificateType int, lang ...string) (*BaseCertificate, error) {
l := "pt_br"
func generateCertificate(rnd translations.RandSource, formatted bool, certificateType int, lang ...string) (*BaseCertificate, error) {
l := "ptbr"
if len(lang) > 0 && lang[0] != "" {
l = lang[0]
}
if rnd == nil {
rnd = rand.New(rand.NewSource(int64(rand.Int())))
rnd = translations.DefaultRandSource()
}
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 +121,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 @@ -139,8 +138,8 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert
return createdBaseCertificate, nil
}

func generateBirthCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*BirthCertificate, error) {
base, err := generateCertificateCustom(rnd, formatted, brazilianBirthCertificateType)
func generateBirthCertificate(lang string, formatted bool, rnd translations.RandSource) (*BirthCertificate, error) {
base, err := generateCertificate(rnd, formatted, brazilianBirthCertificateType, lang)
if err != nil {
return nil, err
}
Expand All @@ -150,8 +149,8 @@ func generateBirthCertificateCustom(lang string, formatted bool, rnd translation
return createdBirthCertificate, nil
}

func generateMarriageCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*MarriageCertificate, error) {
base, err := generateCertificateCustom(rnd, formatted, brazilianMarriageCertificateType)
func generateMarriageCertificate(lang string, formatted bool, rnd translations.RandSource) (*MarriageCertificate, error) {
base, err := generateCertificate(rnd, formatted, brazilianMarriageCertificateType, lang)
if err != nil {
return nil, err
}
Expand All @@ -161,8 +160,8 @@ func generateMarriageCertificateCustom(lang string, formatted bool, rnd translat
return createdMarriageCertificate, nil
}

func generateDeathCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*DeathCertificate, error) {
base, err := generateCertificateCustom(rnd, formatted, brazilianDeathCertificateType)
func generateDeathCertificate(lang string, formatted bool, rnd translations.RandSource) (*DeathCertificate, error) {
base, err := generateCertificate(rnd, formatted, brazilianDeathCertificateType, lang)
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.

13 changes: 4 additions & 9 deletions pkg/mocai/entities/company/countries/brazilian_company.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package countries

import (
"fmt"
"math/rand"

"github.com/brazzcore/mocai/pkg/mocai/entities/cnpj"
"github.com/brazzcore/mocai/pkg/mocai/translations"
Expand All @@ -18,10 +17,10 @@ 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()
rnd = translations.DefaultRandSource()
}
companyName := companyNames[rnd.Intn(len(companyNames))]

Expand All @@ -30,18 +29,14 @@ 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,
CNPJ: cnpjVal,
}
return createdCompany, nil
}

func defaultRandSource() translations.RandSource {
return rand.New(rand.NewSource(int64(rand.Int())))
}
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.

6 changes: 2 additions & 4 deletions pkg/mocai/entities/cpf/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package cpf

import (
"fmt"
"math/rand"
"strings"
"time"

"github.com/brazzcore/mocai/pkg/mocai/translations"
)
Expand All @@ -16,7 +14,7 @@ type CPF struct {
// NewCPF generates a mock CPF using a custom language and random source
func NewCPF(lang string, isFormatted bool, rnd translations.RandSource) (*CPF, error) {
if rnd == nil {
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
rnd = translations.DefaultRandSource()
}

// generate the first 9 digits
Expand All @@ -35,7 +33,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
Loading
Loading