7.8 KiB
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"), orgetCountry("JP")is called - THEN the corresponding
CountryItemis returned with all 9 fields populated
Scenario: Unknown code throws LocaleDataError
- WHEN
getCountryis called with a code not in the registry - THEN a
LocaleDataErroris thrown whose code isLOCALE_DATA.COUNTRY.NOT_FOUNDAND 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")orgetCountriesByContinent("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")orgetCountriesByCurrency("USD")is called - THEN every country whose
currencyfield 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")orgetLanguage("ar")is called - THEN the language record is returned with the correct native name
Scenario: Unknown language throws
- WHEN
getLanguageis 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"), orisRtlLanguage("ur")is called - THEN the result is
true
Scenario: isRtlLanguage classifies non-RTL and unknown codes
- WHEN
isRtlLanguage("en"),isRtlLanguage("fr"), orisRtlLanguageis 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