Locale System
Phony's locale system uses a multi-layer inheritance architecture that enables efficient organization of locale-specific data while minimizing duplication.
Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ LOCALE INHERITANCE ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PROBLEM: │
│ ───────── │
│ • Same data structures across locales (duplication) │
│ • Some data is universal (HTTP codes, UUIDs) │
│ • Some data is locale-specific (names, addresses) │
│ • Some data is company-specific (internal formats) │
│ │
│ SOLUTION: Three-Layer Inheritance │
│ ───────────────────────────────── │
│ │
│ Layer 3: DOMAIN (most specific) │
│ │ Your company/project overrides │
│ │ Example: @yourcompany/internal │
│ │ │
│ └─▶ inherits from │
│ │
│ Layer 2: LOCALE (language-specific) │
│ │ Locale-specific data │
│ │ Example: @phony/tr_TR, @phony/en_US │
│ │ │
│ └─▶ inherits from │
│ │
│ Layer 1: BASE (universal) │
│ Locale-independent data │
│ Example: @phony/base │
│ │
└─────────────────────────────────────────────────────────────────────────┘Layer 1: Base Package
The base package contains locale-independent generators and data.
Contents
@phony/base/
├── manifest.json
├── schema.pdl.json
│
├── generators/
│ ├── logic/
│ │ ├── uuid.json
│ │ ├── number.json
│ │ ├── datetime.json
│ │ └── sequence.json
│ └── common/
│ ├── boolean.json
│ └── color.json
│
└── lists/
├── internet/
│ ├── http_methods.json
│ ├── http_status_codes.json
│ ├── mime_types.json
│ └── tlds.json
├── standards/
│ ├── iso_countries.json
│ ├── iso_currencies.json
│ ├── iso_languages.json
│ └── timezones.json
└── tech/
├── file_extensions.json
└── protocols.jsonExample: Base Schema
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"name": "Phony Base Package",
"locale_independent": true,
"generators": {
"uuid": {
"type": "logic",
"algorithm": "uuid_v7"
},
"random_int": {
"type": "logic",
"algorithm": "int_between",
"params": { "min": 0, "max": 2147483647 }
},
"timestamp": {
"type": "logic",
"algorithm": "datetime_between",
"params": { "start": "-10years", "end": "now" }
},
"http_method": {
"type": "list",
"source": "lists/internet/http_methods.json",
"locale_independent": true
},
"http_status": {
"type": "list",
"source": "lists/internet/http_status_codes.json",
"locale_independent": true
},
"country_code": {
"type": "list",
"source": "lists/standards/iso_countries.json",
"locale_independent": true
},
"currency_code": {
"type": "list",
"source": "lists/standards/iso_currencies.json",
"locale_independent": true
},
"timezone": {
"type": "list",
"source": "lists/standards/timezones.json",
"locale_independent": true
}
}
}Layer 2: Locale Packages
Locale packages add language-specific models, lists, and templates.
Contents
@phony/tr_TR/
├── manifest.json
├── schema.pdl.json
│
├── models/
│ ├── person/
│ │ ├── first_names.ngram
│ │ ├── last_names.ngram
│ │ └── full_names.ngram
│ ├── business/
│ │ └── company_names.ngram
│ └── address/
│ └── street_names.ngram
│
├── lists/
│ ├── geo/
│ │ ├── cities.json
│ │ ├── districts.json
│ │ ├── neighborhoods.json
│ │ └── regions.json
│ ├── person/
│ │ ├── titles.json
│ │ └── suffixes.json
│ └── address/
│ ├── street_suffixes.json
│ └── building_types.json
│
└── templates/
├── person/
│ └── full_name.json
├── address/
│ ├── street_address.json
│ └── full_address.json
└── contact/
├── phone.json
└── email.jsonExample: Turkish Locale Schema
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"locale": "tr_TR",
"name": "Turkish Locale Package",
"inherits": "@phony/base",
"generators": {
"first_name": {
"type": "model",
"source": "models/person/first_names.ngram"
},
"last_name": {
"type": "model",
"source": "models/person/last_names.ngram"
},
"company_name": {
"type": "model",
"source": "models/business/company_names.ngram"
},
"street_name_base": {
"type": "model",
"source": "models/address/street_names.ngram"
},
"city": {
"type": "list",
"source": "lists/geo/cities.json"
},
"district": {
"type": "list",
"source": "lists/geo/districts.json"
},
"neighborhood": {
"type": "list",
"source": "lists/geo/neighborhoods.json"
},
"street_suffix": {
"type": "list",
"source": "lists/address/street_suffixes.json"
},
"title": {
"type": "list",
"source": "lists/person/titles.json"
},
"full_name": {
"type": "template",
"variants": [
{ "pattern": "{{first_name}} {{last_name}}", "weight": 85 },
{ "pattern": "{{title}} {{first_name}} {{last_name}}", "weight": 15 }
]
},
"street_name": {
"type": "template",
"variants": [
{ "pattern": "{{first_name}} {{street_suffix}}", "weight": 30 },
{ "pattern": "{{last_name}} {{street_suffix}}", "weight": 30 },
{ "pattern": "{{number:1-2000}}. {{street_suffix}}", "weight": 25 },
{ "pattern": "Atatürk {{street_suffix}}", "weight": 10 },
{ "pattern": "Cumhuriyet {{street_suffix}}", "weight": 5 }
]
},
"full_address": {
"type": "template",
"variants": [
{ "pattern": "{{street_name}} No:{{number:1-200}} {{district}}/{{city}}", "weight": 40 },
{ "pattern": "{{street_name}} {{number:1-150}}/{{number:1-20}} {{city}}", "weight": 30 },
{ "pattern": "{{neighborhood}} Mah. {{street_name}} No:{{number:1-100}} {{city}}", "weight": 20 },
{ "pattern": "{{district}} {{city}}", "weight": 10 }
]
},
"phone": {
"type": "template",
"variants": [
{ "pattern": "+90 {{pick(['532','533','535','542','543','544','545','546'])}} {{pattern:### ## ##}}", "weight": 70 },
{ "pattern": "+90 212 {{pattern:### ## ##}}", "weight": 10 },
{ "pattern": "+90 312 {{pattern:### ## ##}}", "weight": 10 },
{ "pattern": "0{{pick(['532','533','535','542','543'])}} {{pattern:### ## ##}}", "weight": 10 }
]
},
"email": {
"type": "template",
"pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@{{pick(['gmail.com','hotmail.com','yahoo.com','outlook.com'])}}",
"unique": true
}
}
}Turkish-Specific Data Examples
lists/geo/cities.json:
[
{ "name": "İstanbul", "plate": "34", "region": "Marmara", "population": 15840900 },
{ "name": "Ankara", "plate": "06", "region": "İç Anadolu", "population": 5747325 },
{ "name": "İzmir", "plate": "35", "region": "Ege", "population": 4425789 },
{ "name": "Bursa", "plate": "16", "region": "Marmara", "population": 3147818 },
{ "name": "Antalya", "plate": "07", "region": "Akdeniz", "population": 2619832 }
]lists/address/street_suffixes.json:
["Sokak", "Sokağı", "Caddesi", "Bulvarı", "Yolu", "Meydanı"]Layer 3: Domain Packages
Domain packages add company/project-specific customizations.
Example: Company Internal Package
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"locale": "tr_TR",
"name": "YourCompany Internal Data",
"inherits": "@phony/tr_TR",
"generators": {
"employee_id": {
"type": "template",
"pattern": "EMP-{{format(now(), 'Y')}}-{{pattern:######}}",
"unique": true
},
"department": {
"type": "list",
"source": "inline",
"values": [
{ "code": "ENG", "name": "Mühendislik" },
{ "code": "MKT", "name": "Pazarlama" },
{ "code": "FIN", "name": "Finans" },
{ "code": "HR", "name": "İnsan Kaynakları" },
{ "code": "OPS", "name": "Operasyon" }
]
},
"company_email": {
"type": "template",
"pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@yourcompany.com",
"unique": true
},
"phone": {
"type": "template",
"variants": [
{ "pattern": "+90 {{pick(['532','533','535'])}} {{pattern:### ## ##}}", "weight": 100 }
]
}
},
"entities": {
"Employee": {
"fields": {
"id": { "generator": "employee_id", "primary_key": true },
"first_name": { "generator": "first_name" },
"last_name": { "generator": "last_name" },
"email": { "generator": "company_email" },
"phone": { "generator": "phone" },
"department_code": { "type": "template", "pattern": "{{department.code}}" },
"hire_date": { "generator": "timestamp" }
}
}
}
}Note: The
phonegenerator overrides the one from@phony/tr_TR. Generators fromfirst_name,last_nameare inherited from tr_TR, whiletimestampis inherited from base.
Resolution Algorithm
When a generator or list is requested, Phony resolves it through the inheritance chain:
┌─────────────────────────────────────────────────────────────────────────┐
│ RESOLUTION ALGORITHM │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Request: {{city}} │
│ │
│ Step 1: Check current package (@yourcompany/internal) │
│ ├─▶ Found? → Use it │
│ └─▶ Not found? → Continue to Step 2 │
│ │
│ Step 2: Check inherited package (@phony/tr_TR) │
│ ├─▶ Found? → Use it ✓ │
│ └─▶ Not found? → Continue to Step 3 │
│ │
│ Step 3: Check base package (@phony/base) │
│ ├─▶ Found? → Use it │
│ └─▶ Not found? → Error: Generator not found │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ EXAMPLES: │
│ │
│ {{city}} │
│ @yourcompany/internal: ✗ Not defined │
│ @phony/tr_TR: ✓ Found → lists/geo/cities.json │
│ │
│ {{uuid}} │
│ @yourcompany/internal: ✗ Not defined │
│ @phony/tr_TR: ✗ Not defined │
│ @phony/base: ✓ Found → logic/uuid.json │
│ │
│ {{employee_id}} │
│ @yourcompany/internal: ✓ Found → template │
│ │
│ {{phone}} │
│ @yourcompany/internal: ✓ Found → OVERRIDES tr_TR version │
│ │
└─────────────────────────────────────────────────────────────────────────┘Locale-Independent Data
Some data is marked as locale_independent and is shared across all locales:
{
"generators": {
"http_method": {
"type": "list",
"source": "lists/internet/http_methods.json",
"locale_independent": true
},
"uuid": {
"type": "logic",
"algorithm": "uuid_v7",
"locale_independent": true
}
}
}When to use locale_independent:
| Data Type | Locale Independent? | Reason |
|---|---|---|
| HTTP methods | Yes | Standards don't vary |
| Country codes | Yes | ISO standard |
| UUIDs | Yes | Algorithm is universal |
| Person names | No | Vary by language |
| Cities | No | Vary by country |
| Address format | No | Patterns vary by country |
| Phone format | No | Formats vary by country |
Multiple Locales in One Project
You can use multiple locales in a single schema:
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"name": "International E-Commerce",
"locale": "en_US",
"imports": ["@phony/tr_TR", "@phony/de_DE"],
"generators": {
"turkish_name": {
"type": "model",
"source": "@phony/tr_TR/models/person/first_names.ngram"
},
"german_name": {
"type": "model",
"source": "@phony/de_DE/models/person/first_names.ngram"
},
"american_name": {
"type": "model",
"source": "models/person/first_names.ngram"
}
},
"entities": {
"User": {
"fields": {
"id": { "generator": "uuid" },
"locale": {
"type": "list",
"source": "inline",
"values": ["tr_TR", "de_DE", "en_US"]
},
"first_name": {
"type": "template",
"pattern": "{{switch(self.locale, tr_TR: turkish_name, de_DE: german_name, default: american_name)}}"
}
}
}
}
}Creating New Locales
Step 1: Create Package Structure
phony packages create my-locale --locale fr_FR --inherits @phony/baseStep 2: Train Models
# Train from French name data
phony train french_names.txt -o my-locale/models/person/first_names.ngram
# Train from French company data
phony train french_companies.txt -o my-locale/models/business/company_names.ngramStep 3: Add Lists
// my-locale/lists/geo/cities.json
[
{ "name": "Paris", "department": "75", "region": "Île-de-France" },
{ "name": "Lyon", "department": "69", "region": "Auvergne-Rhône-Alpes" },
{ "name": "Marseille", "department": "13", "region": "Provence-Alpes-Côte d'Azur" }
]Step 4: Define Templates
{
"generators": {
"full_address": {
"type": "template",
"variants": [
{ "pattern": "{{number:1-200}} {{street_name}}, {{zip}} {{city}}", "weight": 100 }
]
}
}
}Note: French address format is
42 Rue de Rivoli, 75001 Paris
Step 5: Build and Publish
phony packages build my-locale
phony packages publish my-locale-1.0.0.phonyBest Practices
1. Use Inheritance
Good: Inherit and override only what's needed:
{
"inherits": "@phony/tr_TR",
"generators": {
"phone": { "type": "template", "pattern": "..." }
}
}Bad: Duplicating everything from tr_TR instead of inheriting.
2. Mark Locale-Independent Data
Good: Explicit marking:
{
"http_method": {
"type": "list",
"source": "lists/http_methods.json",
"locale_independent": true
}
}Bad: Ambiguous without locale_independent flag.
3. Organize by Domain
lists/
├── geo/ # Geographic data
├── person/ # Person-related
├── commerce/ # E-commerce related
└── address/ # Address components4. Use Metadata in Lists
// Good: Includes useful metadata
[
{ "name": "İstanbul", "plate": "34", "region": "Marmara", "population": 15840900 }
]
// Minimal: Just values (acceptable for simple cases)
["İstanbul", "Ankara", "İzmir"]