123 lines
7.8 KiB
Markdown
123 lines
7.8 KiB
Markdown
# Spec: @suaveplan/locale-data
|
|
|
|
**Package:** `@suaveplan/locale-data`
|
|
**Domain:** `i18n/locale-data`
|
|
**Version:** `0.1.0`
|
|
**Status:** Published. Spec heading conformance: `locale-data-spec-conformance` (2026-04-26).
|
|
|
|
## Overview
|
|
|
|
Static ISO 3166-1 country data (209 entries) and ISO 639-1 language data (130+ entries) with typed lookup functions, OTel instrumentation, and Zod schemas.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: Country lookup by ISO 3166-1 alpha-2 code
|
|
|
|
The package SHALL export `getCountry(code: string): CountryItem` performing an O(1) lookup over the static country map. Unknown codes MUST throw `LocaleDataError` with code `LOCALE_DATA.COUNTRY.NOT_FOUND`. The function MUST emit an OTel counter `locale_data.country.lookups` with attribute `result: "found" | "not_found"`.
|
|
|
|
#### Scenario: Known code returns the country record
|
|
|
|
- **WHEN** `getCountry("US")`, `getCountry("DE")`, or `getCountry("JP")` is called
|
|
- **THEN** the corresponding `CountryItem` is returned with all 9 fields populated
|
|
|
|
#### Scenario: Unknown code throws LocaleDataError
|
|
|
|
- **WHEN** `getCountry` is called with a code not in the registry
|
|
- **THEN** a `LocaleDataError` is thrown whose code is `LOCALE_DATA.COUNTRY.NOT_FOUND` AND whose message names the unknown code
|
|
|
|
**Verified by:** `src/countries/countries.test.ts::"returns a known country by ISO code"` AND `src/countries/countries.test.ts::"returns Germany correctly"` AND `src/countries/countries.test.ts::"returns Japan correctly"` AND `src/countries/countries.test.ts::"throws LocaleDataError for unknown code"` AND `src/countries/countries.test.ts::"throws with NOT_FOUND message for unknown code"`
|
|
|
|
### Requirement: Bulk country accessors
|
|
|
|
The package SHALL export `getAllCountries(): Record<string, CountryItem>` returning the entire registry by stable reference (no copy), `getCountriesByContinent(continent: string): CountryItem[]` filtering by 2-letter continent code, and `getCountriesByCurrency(currency: string): CountryItem[]` filtering by currency code with multi-currency entries split on comma.
|
|
|
|
#### Scenario: getAllCountries returns the full registry by reference
|
|
|
|
- **WHEN** `getAllCountries()` is called twice
|
|
- **THEN** both calls return the same object reference (no defensive copy)
|
|
- **AND** the registry includes US, DE, JP, FR
|
|
|
|
#### Scenario: Continent filter narrows by 2-letter code
|
|
|
|
- **WHEN** `getCountriesByContinent("EU")` or `getCountriesByContinent("AS")` is called
|
|
- **THEN** only countries on that continent are returned
|
|
- **WHEN** an unknown continent is given
|
|
- **THEN** an empty array is returned
|
|
|
|
#### Scenario: Currency filter splits comma-separated entries
|
|
|
|
- **WHEN** `getCountriesByCurrency("EUR")` or `getCountriesByCurrency("USD")` is called
|
|
- **THEN** every country whose `currency` field contains the code (including in a comma-separated list) is returned
|
|
- **WHEN** an unknown currency is given
|
|
- **THEN** an empty array is returned
|
|
|
|
**Verified by:** `src/countries/countries.test.ts::"returns all countries as a record"` AND `src/countries/countries.test.ts::"includes US, DE, JP, FR"` AND `src/countries/countries.test.ts::"returns the same reference each call (no copy)"` AND `src/countries/countries.test.ts::"returns only European countries for EU"` AND `src/countries/countries.test.ts::"returns Asian countries for AS"` AND `src/countries/countries.test.ts::"returns empty array for unknown continent"` AND `src/countries/countries.test.ts::"returns countries using EUR"` AND `src/countries/countries.test.ts::"returns countries using USD"` AND `src/countries/countries.test.ts::"returns empty array for unknown currency"` AND `src/countries/countries.test.ts::"supports multi-currency entries (comma-separated)"`
|
|
|
|
### Requirement: Language lookup and bulk accessor
|
|
|
|
The package SHALL export `getLanguage(code: string): LanguageItem` (O(1)) and `getLanguages(): Record<string, LanguageItem>` returning the same reference each call. Unknown codes MUST throw `LocaleDataError` with code `LOCALE_DATA.LANGUAGE.NOT_FOUND`. The lookup MUST emit an OTel counter `locale_data.language.lookups` with `result: "found" | "not_found"`.
|
|
|
|
#### Scenario: getLanguages returns the full registry by reference
|
|
|
|
- **WHEN** `getLanguages()` is called twice
|
|
- **THEN** both calls return the same object reference
|
|
- **AND** the registry includes common languages with native names populated
|
|
|
|
#### Scenario: Known language returns its record
|
|
|
|
- **WHEN** `getLanguage("en")` or `getLanguage("ar")` is called
|
|
- **THEN** the language record is returned with the correct native name
|
|
|
|
#### Scenario: Unknown language throws
|
|
|
|
- **WHEN** `getLanguage` is called with an unknown code
|
|
- **THEN** a `LocaleDataError(LOCALE_DATA.LANGUAGE.NOT_FOUND)` is thrown with the code in the message
|
|
|
|
**Verified by:** `src/languages/languages.test.ts::"returns all languages"` AND `src/languages/languages.test.ts::"returns the same reference each call"` AND `src/languages/languages.test.ts::"includes common languages"` AND `src/languages/languages.test.ts::"includes native names"` AND `src/languages/languages.test.ts::"returns a known language"` AND `src/languages/languages.test.ts::"returns English with correct native name"` AND `src/languages/languages.test.ts::"returns Arabic with correct native name"` AND `src/languages/languages.test.ts::"throws for unknown language code"` AND `src/languages/languages.test.ts::"throws with code in message for unknown language"`
|
|
|
|
### Requirement: RTL language detection
|
|
|
|
The package SHALL export `getRtlLanguages(): readonly string[]` listing the canonical RTL language codes, and `isRtlLanguage(code: string): boolean` which is BCP-47 aware (subtags MUST be stripped before lookup, so `"ar-EG"` matches `"ar"`).
|
|
|
|
#### Scenario: RTL list includes the canonical RTL languages
|
|
|
|
- **WHEN** `getRtlLanguages()` is called
|
|
- **THEN** it returns a non-empty array containing at least `"ar"`, `"he"`, `"fa"`, `"ur"`
|
|
- **AND** every code in the list resolves via `getLanguage`
|
|
|
|
#### Scenario: isRtlLanguage classifies known RTL languages
|
|
|
|
- **WHEN** `isRtlLanguage("ar")`, `isRtlLanguage("he")`, `isRtlLanguage("fa")`, or `isRtlLanguage("ur")` is called
|
|
- **THEN** the result is `true`
|
|
|
|
#### Scenario: isRtlLanguage classifies non-RTL and unknown codes
|
|
|
|
- **WHEN** `isRtlLanguage("en")`, `isRtlLanguage("fr")`, or `isRtlLanguage` is called with an unknown code
|
|
- **THEN** the result is `false`
|
|
|
|
**Verified by:** `src/languages/languages.test.ts::"includes Arabic, Hebrew, Persian, Urdu"` AND `src/languages/languages.test.ts::"all returned codes exist in language map"` AND `src/languages/languages.test.ts::"returns a non-empty array"` AND `src/languages/languages.test.ts::"returns true for Arabic"` AND `src/languages/languages.test.ts::"returns true for Hebrew"` AND `src/languages/languages.test.ts::"returns true for Persian"` AND `src/languages/languages.test.ts::"returns true for Urdu"` AND `src/languages/languages.test.ts::"returns false for English"` AND `src/languages/languages.test.ts::"returns false for French"` AND `src/languages/languages.test.ts::"returns false for unknown code"`
|
|
|
|
## Errors
|
|
|
|
- `LOCALE_DATA_CODES` — `defineCodes("LOCALE_DATA", ["COUNTRY.NOT_FOUND", "LANGUAGE.NOT_FOUND"])`.
|
|
- `LocaleDataError extends SuaveplanError<LocaleDataCode>`.
|
|
|
|
## Types / Schemas
|
|
|
|
- `CountryItemSchema` (Zod) — 9 fields: name, native, phone, continent, capital, currency, languages[], emoji, emojiU.
|
|
- `LanguageItemSchema` (Zod) — 2 fields: name, native.
|
|
- `CountryCodeSchema` — `z.string().length(2).toUpperCase()`.
|
|
- `LanguageCodeSchema` — `z.string().min(2).max(3).toLowerCase()`.
|
|
|
|
## Data Coverage
|
|
|
|
- **Countries:** 209 ISO 3166-1 alpha-2 entries, 7 continent codes.
|
|
- **Languages:** 130+ ISO 639-1 entries, 15 RTL codes (`ar`, `az`, `ba`, `dv`, `fa`, `he`, `iu`, `ks`, `ku`, `ps`, `sd`, `sr`, `tg`, `ur`, `yi`).
|
|
|
|
## Quality Gates
|
|
|
|
- 100% Istanbul coverage (all 4 thresholds)
|
|
- 100% docs coverage (6/6 implementation files)
|
|
- Biome lint clean
|
|
- TypeScript clean
|