world-locale Concepts
Understanding the design and architecture of the world-locale module for country codes and internationalisation.
Module Purpose
The world-locale module provides type-safe representations of ISO 3166-1 country codes with comprehensive metadata. It serves as the foundation for internationalisation and locale-aware operations.
Key responsibilities:
- ISO 3166-1 compliant country codes (Alpha-2, Alpha-3, Numeric/M49)
- Country metadata (names, regions, calling codes)
- Type-safe country code representations
- Efficient lookup and validation operations
ISO 3166-1 Standard
Code Formats
The module supports all three ISO 3166-1 code formats:
| Format | Example | Length | Type |
|---|---|---|---|
| Alpha-2 | GB |
2 letters | Alpha2 |
| Alpha-3 | GBR |
3 letters | Alpha3 |
| Numeric (M49) | 826 |
3 digits | Int |
All three formats reference the same country but serve different use cases:
- Alpha-2: Most common, used in domains, URLs, forms
- Alpha-3: Human-readable, less ambiguous
- Numeric: Language-independent, database keys
Code Relationships
Each country has all three codes:
val uk = Countries.GB
uk.alpha2 // Alpha2 = "GB"
uk.alpha3 // Alpha3 = "GBR"
uk.numeric // Int = 826
The module guarantees consistency across all code formats.
Type-Safe Country Codes
Opaque Type Aliases
Country codes use opaque types for zero-cost type safety:
opaque type Alpha2 = String
opaque type Alpha3 = String
Benefits:
- Compile-time distinction from raw strings
- No runtime allocation overhead
- Cannot accidentally mix code types
- Runtime performance of primitive types
Smart Constructors
Country codes are validated at creation:
// Type-safe lookup
val uk: Option[Country] = Countries.findByAlpha2("GB")
// Predefined constants (no validation needed)
val kenya: Country = Countries.KE
// Invalid codes return None
val invalid: Option[Country] = Countries.findByAlpha2("XX") // None
Invalid codes are rejected, maintaining data integrity.
Country Object Design
Immutable Value Objects
Countries are immutable case classes:
final case class Country(
alpha2: Alpha2,
alpha3: Alpha3,
numeric: Int,
commonName: String,
officialName: Option[String],
// ... additional fields
)
Design properties:
- Immutable: Thread-safe, shareable
- Value semantics: Equality by content
- Complete: All metadata in one place
- Optional fields: Not all countries have all attributes
Singleton Access
Predefined countries are singleton objects:
val uk1 = Countries.GB
val uk2 = Countries.GB
assert(uk1 eq uk2) // Same reference
This provides efficient equality checks and minimal memory usage.
Generated Code Architecture
Compile-Time Generation
Country data is generated during compilation from authoritative sources:
countries-iso3166.csv + supplemental-countries.yml
↓ (sbt sourceGenerators)
Countries.scala (generated)
↓ (compilation)
Bytecode
Generation process:
- Parse CSV and YAML data files
- Validate against ISO 3166-1 standard
- Generate Scala source code
- Compile to optimised bytecode
Data Sources
- Primary:
countries-iso3166.csv(ISO official list) - Supplemental:
supplemental-countries.yml(additional metadata)
This ensures accuracy and compliance with international standards.
Generated Artefacts
The generator produces:
object Countries:
// Singleton country objects
val GB: Country = Country("GB", "GBR", 826, "United Kingdom", ...)
val KE: Country = Country("KE", "KEN", 404, "Kenya", ...)
// ... ~250 countries
// Lookup maps
private val byAlpha2: Map[Alpha2, Country] = ...
private val byAlpha3: Map[Alpha3, Country] = ...
private val byNumeric: Map[Int, Country] = ...
// Lookup methods
def findByAlpha2(code: Alpha2): Option[Country] = ...
def findByAlpha3(code: Alpha3): Option[Country] = ...
def findByM49Code(code: Int): Option[Country] = ...
Everything is generated and optimised at compile time.
Lookup Strategies
Constant-Time Lookups
All lookups use hash maps for O(1) average-case performance:
// All O(1) average-case
Countries.findByAlpha2("GB")
Countries.findByAlpha3("GBR")
Countries.findByM49Code(826)
Case Sensitivity
Alpha codes are case-insensitive during lookup:
Countries.findByAlpha2("GB") // Works
Countries.findByAlpha2("gb") // Also works
Countries.findByAlpha2("Gb") // Still works
This provides a better user experience while maintaining type safety.
Collection Access
All countries are available as a collection:
val all: Seq[Country] = Countries.all
// Filter operations
val european = all.filter(_.region == "Europe")
val large = all.filter(_.population > 100_000_000)
This enables bulk operations and custom filtering.
Error Handling
Option-Based Results
Lookups return Option[Country] for safety:
Countries.findByAlpha2("GB") match
case Some(country) => processCountry(country)
case None => handleInvalidCode()
Validation Patterns
Validate user input before use:
def validateCountryCode(input: String): Either[String, Country] =
Countries.findByAlpha2(input)
.toRight(s"Invalid country code: $input")
This makes error handling explicit and compositional.
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
| Lookup by Alpha-2 | O(1) | Hash map lookup |
| Lookup by Alpha-3 | O(1) | Hash map lookup |
| Lookup by Numeric | O(1) | Hash map lookup |
| Access singleton | O(1) | Direct reference |
| Iterate all countries | O(n) | Linear in country count |
The module is optimised for lookup-heavy workloads.
Best Practices
Prefer Singletons
Use predefined country objects when possible:
// ✓ Good: Direct reference
val uk = Countries.GB
// ✗ Okay: Unnecessary lookup
val uk = Countries.findByAlpha2("GB").get
Handle Missing Countries
Always handle the None case:
// ✓ Good: Explicit handling
val country = Countries.findByAlpha2(code).getOrElse(Countries.UNKNOWN)
// ✗ Bad: Potential crash
val country = Countries.findByAlpha2(code).get // Throws!
Use Pattern Matching
Leverage pattern matching for clarity:
country match
case Countries.GB => handleUK()
case Countries.US => handleUS()
case _ => handleOther()
Integration with Money Module
The Money module uses country information for currency usage:
// Check which currencies a country uses
val currencies: Seq[Currency] = country.currencies
// Find countries using a currency
val gbpCountries: Seq[Country] = Currencies.GBP.usage